Compare commits

..

181 Commits

Author SHA1 Message Date
Antonio Murdaca
416ff71bed bump version to 0.1.15
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-26 17:55:40 +02:00
Jonathan Boulle
4ed4525155 Update README.md 2016-09-26 17:55:40 +02:00
Antonio Murdaca
c813de92d8 Merge pull request #216 from runcom/progress-fix
fix containers/image progress
2016-09-26 17:42:06 +02:00
Antonio Murdaca
0420fa0299 fix containers/image progress
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-26 17:21:08 +02:00
Miloslav Trmač
5b7fcc8eca Merge pull request #215 from runcom/progress
add progress bars during copy
2016-09-26 16:21:04 +02:00
Antonio Murdaca
c84203bdd5 add progress bars during copy
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-26 15:59:37 +02:00
Miloslav Trmač
98bfef9072 Merge pull request #211 from mtrmac/compress
Layer compression
2016-09-19 22:06:14 +02:00
Miloslav Trmač
4ec3b64c84 Add an integration tests for compression during upload
Sadly, most of the cases are disabled for now; hopefully this will get
fixed soon.
2016-09-19 21:39:59 +02:00
Miloslav Trmač
d705644f22 Vendor after merging mtrmac/image:compress 2016-09-19 21:39:46 +02:00
Miloslav Trmač
9c1cb79754 Merge pull request #210 from mtrmac/api-changes
Vendor after merging mtrmac/image:api-changes and update API use
2016-09-19 17:11:22 +02:00
Miloslav Trmač
459ab05b22 Vendor after merging mtrmac/image:api-changes and update API use 2016-09-19 16:44:47 +02:00
Miloslav Trmač
2e7b6c9d14 Merge pull request #205 from mtrmac/tls-verification
TLS verification in docker registries
2016-09-13 19:48:07 +02:00
Miloslav Trmač
2a8ffee621 Flip --tls-verify default to true
Document better what --tls-verify does

... and sprinkle --tls-verify=false over integration tests.
2016-09-13 19:26:21 +02:00
Miloslav Trmač
623865c159 Vendor after merging mtrmac/image:tls-verification 2016-09-13 19:25:42 +02:00
Antonio Murdaca
58ec828eab Merge pull request #204 from mtrmac/registries.d
Create /etc/containers/registries.d in (make install)
2016-09-13 18:30:07 +02:00
Miloslav Trmač
9835ae579b Create /etc/containers/registries.d in (make install) 2016-09-13 18:08:25 +02:00
Antonio Murdaca
14847101c0 Merge pull request #202 from runcom/change-os-uri
Change atomic URI
2016-09-13 17:11:07 +02:00
Antonio Murdaca
3980ac5894 vendor containers/image#81
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-13 16:50:28 +02:00
Miloslav Trmač
d6be447ce9 Merge pull request #170 from mtrmac/docker-lookaside
Implement a lookaside storage for signatures of images in Docker registries
2016-09-12 21:39:39 +02:00
Miloslav Trmač
b6fdea03f2 Add a global --registries.d option to skopeo
This is added pretty much only for integration tests right now;
though, it might be useful also for non-root operation.

Also makes a tiny cleanup of contextFromGlobalOptions, removing a
variable.
2016-09-12 21:13:53 +02:00
Miloslav Trmač
f46da343e2 Vendor after merging in mtrmac/image:docker-lookaside 2016-09-12 21:13:34 +02:00
Antonio Murdaca
d1d1d6533e Merge pull request #201 from runcom/fix-198
vendor containers/image#84
2016-09-12 17:47:00 +02:00
Antonio Murdaca
890c073526 vendor containers/image#84
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-12 17:25:52 +02:00
Antonio Murdaca
7e69022723 Merge pull request #196 from runcom/crane-fix
vendor containers/image to fix RH
2016-09-09 11:55:37 +02:00
Antonio Murdaca
1c16cd5e9d vendor containers/image to fix RH
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-09 11:34:58 +02:00
Miloslav Trmač
362bfc5fe3 Merge pull request #195 from runcom/vendor-cont/images
vendor containers/image, OCI/image-spec
2016-09-08 14:03:43 +02:00
Antonio Murdaca
81d67eab92 vendor containers/image, OCI/image-spec
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-08 13:23:41 +02:00
Miloslav Trmač
fc0c5be08d Merge pull request #192 from rhatdan/install
Refer to the policy file as a trust policy file.
2016-09-07 17:42:29 +02:00
Dan Walsh
824853d85d Refer to the signature trust policy.
The policy file is actualy indicatiting the signatures that the
user trusts.  This patch changes the documentation and error messages
to indicate this trust.
2016-09-07 10:18:14 -04:00
Antonio Murdaca
2c78131d1d Merge pull request #171 from aweiteka/makefile
Fix selinux perms in Makefile binary build
2016-09-06 23:03:55 +02:00
Aaron Weitekamp
157b9c0f3b disable selinux for binary build 2016-09-06 16:28:07 -04:00
Antonio Murdaca
ee89d2c6a4 Merge pull request #190 from runcom/fix-putblob
vendor containers/image for PutBlob returns
2016-09-06 20:10:16 +02:00
Antonio Murdaca
4e40830eae vendor containers/image for PutBlob returns
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-06 19:47:59 +02:00
Miloslav Trmač
46ffaa8e51 Merge pull request #188 from runcom/vendor-image-spec
vendor containers/image and OCI/image-spec
2016-09-06 16:50:18 +02:00
Antonio Murdaca
649ea391a4 vendor containers/image and OCI/image-spec
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-06 16:19:52 +02:00
Miloslav Trmač
4421e7ea2f Merge pull request #187 from mtrmac/api-changes
Update for mtrmac/image:api-changes
2016-09-06 16:03:28 +02:00
Miloslav Trmač
e8794bd9ff Vendor after merging in mtrmac/image:api-changes
... and update for the API changes.
2016-09-06 15:37:39 +02:00
Antonio Murdaca
136fd1d8a6 Merge pull request #185 from mtrmac/remove-signatures
Add --remove-signatures to (skopeo copy)
2016-09-05 19:34:42 +02:00
Miloslav Trmač
f627fc6045 Add --remove-signatures to (skopeo copy)
This is necessary to allow copying signed images into destinations which
don't support signatures.
2016-09-01 22:34:13 +02:00
Miloslav Trmač
7c2a47f8b9 Vendor after merging mtrmac/image:remove-signatures 2016-09-01 22:17:04 +02:00
Antonio Murdaca
1bfb549f7f Merge pull request #182 from runcom/fix-oci
vendor containers/image for oci dest fix
2016-09-01 18:01:44 +02:00
Antonio Murdaca
9914de1bf4 vendor containers/image for oci dest fix
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-09-01 17:38:17 +02:00
Antonio Murdaca
f37d72d964 Merge pull request #175 from mtrmac/copy
Move copy implementation into containers/image
2016-09-01 16:55:34 +02:00
Miloslav Trmač
3e3748a800 Move the core of the (skopeo copy) implementation to containers/image 2016-09-01 16:27:38 +02:00
Miloslav Trmač
61158ce7f4 Vendor after merging mtrmac/image:copy 2016-09-01 16:27:22 +02:00
Miloslav Trmač
7c17614143 Fix an ambiguity in (git reset)
This is necessary to be able to check out a branch named "clone",
otherwise we get
> fatal: ambiguous argument 'copy': both revision and filename
2016-08-31 22:10:47 +02:00
Miloslav Trmač
d24cdcbcf3 Merge pull request #180 from mtrmac/api-changes
Vendor in API changes from https://github.com/containers/image/pull/64
2016-08-31 22:04:20 +02:00
Miloslav Trmač
4055442da5 Vendor after merging mtrmac/image:api-changes
... and update for the API changes.

NOTE: This keeps the old dangerous tlsVerify semantics.
2016-08-31 21:26:42 +02:00
Antonio Murdaca
fb5e5a79f6 Merge pull request #176 from rhatdan/install
Fix install command to create directories
2016-08-25 21:06:50 +02:00
Dan Walsh
88bec961af Fix install command to create directories 2016-08-25 14:37:35 -04:00
Miloslav Trmač
fc843adca9 Merge pull request #158 from mtrmac/copy-signing-integration-tests
Copy signing integration tests
2016-08-25 20:35:39 +02:00
Miloslav Trmač
3d42f226c2 Add integration tests for signature handling in (skopeo copy)
Note the need for openshiftCluster.relaxImageSignerPermissions.
2016-08-25 20:11:31 +02:00
Miloslav Trmač
821f938a11 Merge pull request #157 from mtrmac/verify-on-pull
Verify signatures on pull
2016-08-25 20:02:45 +02:00
Miloslav Trmač
76a14985d6 Implement policy enforcement in (skopeo copy)
Finally, load and enforce the policy.

NOTE that this breaks a simple ./skopeo from a built directory if you
don't have /etc/atomic/policy.json installed for other reasons;
use (./skopeo --policy default-policy.json) instead.
2016-08-25 19:39:21 +02:00
Miloslav Trmač
d4462330a5 Add a default policy file, install it in (make install) and integration tests
(skopeo copy) will soon ALWAYS require a present policy file.  So,
install one by (make install), and ensure that integration tests do so
as well.

Also simplifies the usage of install(1) a bit.
2016-08-25 19:39:21 +02:00
Miloslav Trmač
d5d6bc28f7 Add a new --policy flag.
This ordinarily uses the compiled-in default, but allows per-command
override.  No users yet.

Note that this adds an URL to policy documentation within
containers/image, and that URL does not exist at the moment.
2016-08-25 19:39:15 +02:00
Miloslav Trmač
8826f09cf4 Vendor after merging mtrmac/image:default-policy 2016-08-25 19:36:29 +02:00
Daniel J Walsh
e6886e4afc Merge pull request #173 from mikebrow/auto-completions
add support for completions
2016-08-25 18:13:18 +02:00
Mike Brown
a40d7b53aa add support for completions
Signed-off-by: Mike Brown <brownwm@us.ibm.com>
2016-08-25 10:45:24 -05:00
Miloslav Trmač
e0d44861af Merge pull request #165 from mtrmac/manifest-digest
Improve manifest digest handling
2016-08-25 17:28:59 +02:00
Miloslav Trmač
c236b29c75 Add (skopeo manifest-digest)
A plain sha256sum and the like is insufficient because we need to strip
signatures from v2s1 manifests; so, add a subcommand.

This can be used together with (skopeo inspect --raw) to download a
manifest from a source untrusted to modify it under us; we download a
manifest once using (skopeo inspect --raw), compute a digest using
(skopeo manifest-digest), and then do all future operations using a
digest reference.
2016-08-25 16:49:02 +02:00
Miloslav Trmač
e4315e82b0 Output the original raw manifest in (skopeo inspect --raw)
We need (skopeo inspect --raw > manifest.json) to save the unmodified
original: no extra new lines, no undetected truncation, nothing.
2016-08-25 16:49:02 +02:00
Miloslav Trmač
91b722fec8 Merge pull request #169 from mtrmac/makefile-cleanup
Makefile cleanup
2016-08-25 16:47:12 +02:00
Miloslav Trmač
406ab86104 Clean up and fix minor bugs in DEBUG/GOGCFLAGS handling
* Use “override GOGCFLAGS+=” so that (make GOGCFLAGS=… DEBUG=1)
  does not ignore the appending to GOGCFLAGS
* Move quoting of -gcflags from the variable to its use,
  so that (make GOGCFLAGS=… DEBUG=1) is correctly quoted
* Now that GOGCFLAGS and DEBUG are both handled correctly when
  completely empty, simplify by dropping the DEBUG!=1 branch.
* Beautify the command line by not using DEBUG= if DEBUG is unset.
2016-08-25 15:51:45 +02:00
Antonio Murdaca
2c90120ce6 Merge pull request #146 from mtrmac/update-openshift
Update OpenShift
2016-08-25 12:01:36 +02:00
Miloslav Trmač
47d74dba90 Update OpenShift after the final version of https://github.com/openshift/origin/pull/9181
Uses a tag created after merging that PR.  (git clone -b …) does not
work with commit IDs, and we like to use a released version anyway.
2016-08-22 16:43:07 +02:00
Miloslav Trmač
aafe2a7337 Merge pull request #161 from mikebrow/debug-build
add a source debug build
2016-08-18 17:02:44 +02:00
Mike Brown
63f4f3413f add a source debug build
Signed-off-by: Mike Brown <brownwm@us.ibm.com>
2016-08-18 09:22:42 -05:00
Daniel J Walsh
50f45932f9 Merge pull request #166 from mtrmac/error-pasto
Fix a pasto in an error message
2016-08-16 19:24:25 +02:00
Miloslav Trmač
da298638a2 Fix a pasto in an error message 2016-08-16 18:44:51 +02:00
Miloslav Trmač
cd0cef8442 Merge pull request #160 from mikebrow/make-dependencies
fix dependencies
2016-08-15 16:59:19 +02:00
Mike Brown
1f30fd7bf3 fix dependencies
Signed-off-by: Mike Brown <brownwm@us.ibm.com>
2016-08-13 19:45:32 -05:00
Daniel J Walsh
3da98694a0 Merge pull request #163 from mtrmac/install-dependencies
Make the install-* targets depend on things they are installing
2016-08-12 13:33:12 +02:00
Miloslav Trmač
9abac5b134 Make the install-* targets depend on things they are installing
This ensures that we are not installing e.g. an obsolete version of the
man page after the Markdown version is updated.

Note that this greatly benefits from the "skopeo" target being
non-phony, otherwise (make install) would rebuild the binary.
2016-08-11 18:55:12 +02:00
Miloslav Trmač
ffe92ed2bb Merge pull request #159 from mikebrow/man-build-update
minor cleanup for build issues related to the manual
2016-08-11 15:17:23 +02:00
Mike Brown
6f6c2b9c73 minor cleanup for build issues
Signed-off-by: Mike Brown <brownwm@us.ibm.com>
2016-08-10 19:46:06 -05:00
Miloslav Trmač
e44bd98fa4 Merge pull request #156 from mtrmac/gitignore
Add the generated man page to .gitignore
2016-08-11 00:06:46 +02:00
Miloslav Trmač
e6049802ba Add the generated man page to .gitignore
… and reorder it alphabetically.
2016-08-10 22:58:07 +02:00
Miloslav Trmač
43273caab1 Merge pull request #153 from jwhonce/wip/issue-151
Convert man page to markdown format
2016-08-10 21:18:37 +02:00
Lokesh Mandvekar
ad3e26d042 install manpages using the install-docs target
The MANINSTALL/man1 dir needs to be installed first before installing manpages
into it.

Signed-off-by: Lokesh Mandvekar <lsm5@fedoraproject.org>
2016-08-10 11:52:02 -07:00
Jhon Honce
9a8529667d Convert man page to markdown format
Signed-off-by: Jhon Honce <jhonce@redhat.com>
2016-08-10 11:50:39 -07:00
Miloslav Trmač
6becbb2c66 Merge pull request #149 from mtrmac/docs-and-help
Improve man page and --help
2016-08-09 15:41:27 +02:00
Miloslav Trmač
8a239596a9 Improve --help output
- Use ArgsUsage to document the non-option arguments
- Refer to ArgsUsage placeholders in Usage
- Use named placeholders in flag documentation

Fixes #137, more or less.
2016-08-09 00:40:08 +02:00
Miloslav Trmač
68faefed61 Comprehensively rework the man page
Among other minor changes:
- Do not duplicate synopses of the subcommands; use a generic synopsis
  at the top, and detailed subcommand synopses only when documenting the
  subcommands.
- Use the conventions documented in man-pages(7), in particular using
  italic for replaceable values.
- Add a section documenting the transport:details reference format,
  and list the supported transports.
- Relax the warning about standalone-sign.
2016-08-09 00:40:08 +02:00
Miloslav Trmač
09399a9ac1 Merge pull request #148 from mtrmac/verify-blobs
Verify blobs
2016-08-05 18:44:33 +02:00
Miloslav Trmač
23c96cb998 Verify blobs against the expected digests while copying them.
Note that this requires ImageDestination.PutBlob to fail and delete
any unfinished data if stream.Read() fails.

We do not have to trust PutBlob to correctly handle a validation error,
so we don't; but we can't do the storage cleanup for PutBlob.
2016-08-04 19:58:08 +02:00
Miloslav Trmač
6e2cd739da Vendor after merging mtrmac/image:PutBlob-error-handling 2016-08-04 19:57:40 +02:00
Antonio Murdaca
5197c8dba0 Merge pull request #147 from duglin/contrib
Add a CONTRIBUTING.md file
2016-08-02 15:12:26 +02:00
Doug Davis
e17b1f97ca Add a CONTRIBUTING.md file
Signed-off-by: Doug Davis <dug@us.ibm.com>
2016-08-02 05:46:43 -07:00
Miloslav Trmač
e4982ea82a Merge pull request #93 from mtrmac/openshift-native-signatures
OpenShift native signatures
2016-08-01 22:05:51 +02:00
Miloslav Trmač
c9fbb6c1ab Vendor after merging mtrmac/image:openshift-native-signatures and update API use
Update copy.go for signature implementation change

Now we need to push the manifest first, and only afterwards the
signatures.
2016-08-01 20:44:16 +02:00
Miloslav Trmač
ecc745d124 Merge pull request #138 from mtrmac/location-namespaced-signatures
Use transport abstraction and transport-abstracted references
2016-07-18 21:39:45 +02:00
Miloslav Trmač
b806001e18 Vendor after merging mtrmac/image:reference-abstraction and update API use
directory.NewReference now can fail.
2016-07-18 21:20:51 +02:00
Miloslav Trmač
177463ed03 Merge pull request #144 from mtrmac/reference-abstraction
Reference abstraction
2016-07-18 16:45:59 +02:00
Miloslav Trmač
9ad71d27e0 Vendor after merging mtrmac/image:reference-abstraction and update API use
- Use transports.ParseImageReference instead of dealing with individual
  transports
- CanonicalDockerReference replaced by Reference.DockerReference, can't
  fail but can be unsupported
- directory.NewImageDestination replaced by
  directory.NewReference.NewImageDestination
2016-07-18 16:22:48 +02:00
Miloslav Trmač
5b550a7b37 Merge pull request #143 from mtrmac/docker-references
Clean up Docker reference handling
2016-07-12 15:55:56 +02:00
Miloslav Trmač
8adb5f56de Update for docker-references PR and API changes
Pull in https://github.com/containers/image/pull/37 , and
update for CanonicalDockerReference() returning a reference.Named
2016-07-12 15:24:24 +02:00
Antonio Murdaca
29d76eb5ca Merge pull request #142 from runcom/vendor-contimage
Vendor containers/image
2016-07-04 12:46:10 +02:00
Antonio Murdaca
d7cafe671b Vendor containers/image 9d6b8fc4ae35b9843e4c4397fe0002c8edda7314
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-07-04 12:31:31 +02:00
Antonio Murdaca
ed3016b1c1 Merge pull request #140 from runcom/no-docker-ref
vendor containers/image 5c10ea7c3f0b1f2e36164c15667cc847b1784e16
2016-07-02 12:18:40 +02:00
Antonio Murdaca
09586bb08f vendor containers/image 5c10ea7c3f0b1f2e36164c15667cc847b1784e16
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-07-02 12:02:33 +02:00
Antonio Murdaca
9adb76bf15 Merge pull request #141 from mtrmac/fix-centos-build
Fix integration tests on CentOS
2016-07-01 23:51:37 +02:00
Miloslav Trmač
0cb6cc6222 Fix integration tests on CentOS
This fixes --version integration test on CentOS, as noticed by
https://github.com/projectatomic/skopeo/pull/91 .  The underlying cause
is:
- Makefile builds with -ldflags "-X var=value", while go 1.4.2 only
  supports "-X var value".  This causes CentOS builds to be built
  without the specific commit information
- The --version integration test assumes that commit information will
  always follow the version number.

Changing either one of these would fix the build, changing the
integration test has the advantage that we don't have to use the
obsolete -X syntax and suffer warnings on newer Go versions.
2016-07-01 23:31:07 +02:00
Antonio Murdaca
123891de32 Merge pull request #133 from runcom/oci-3
add possibility to download to OCI image-layout
2016-07-01 22:37:28 +02:00
Antonio Murdaca
6942920ee8 add possibility to download to OCI image-layout
- vendor containers/image c703326038d30c3422168dd9a1a5afaf51740331
- fix copy tests relying on v2s1 manifests

Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-07-01 22:14:14 +02:00
Miloslav Trmač
891c46ed59 Merge pull request #139 from mtrmac/openshift-old-api
Keep using an old version of https://github.com/openshift/origin/pull/9181
2016-07-01 22:08:59 +02:00
Miloslav Trmač
7f9c56ab05 Keep using an old version of https://github.com/openshift/origin/pull/9181
I don’t know how to checkout a specific untagged commit (
9ff4bf43548c758b6767b639b335681285fece48 ) from the original repo, so
I have forked the project and fetched that commit from a cached Docker
image.

We should instead update the containers/image client for the new API ASAP,
and then the github.com/mtrmac/origin repo should be removed.
2016-07-01 20:42:40 +02:00
Antonio Murdaca
a82c64b397 Merge pull request #136 from runcom/fix-version
cmd: skopeo: fix version
2016-06-30 18:02:25 +02:00
Antonio Murdaca
064d37134b cmd: skopeo: fix version
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-06-30 17:46:45 +02:00
Antonio Murdaca
6d7c93acf7 Merge pull request #123 from duglin/modBuild
Build binary in a docker container
2016-06-29 13:43:02 +02:00
Doug Davis
4f7a49ed78 Build binary in a docker container
So that people don't need to install all dependencies just to build.

Make it so that "make binary" does nothing if nothing changed.

Remove ${DEST}

Signed-off-by: Doug Davis <dug@us.ibm.com>
2016-06-29 04:27:54 -07:00
Antonio Murdaca
18223121dd Merge pull request #129 from mtrmac/api-update
Update for changed images.Type API
2016-06-28 20:29:46 +02:00
Miloslav Trmač
fe6c392d45 Update for changed images.Type API 2016-06-28 20:14:15 +02:00
Antonio Murdaca
4558575d9e Merge pull request #127 from runcom/refactor-layers
cmd/skopeo: refactor layers command
2016-06-28 17:41:38 +02:00
Antonio Murdaca
6c4eab8a07 vendor containers/image b95a6b8688d7702cf5906debf87f01cfd849a67a
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-06-28 17:24:20 +02:00
Antonio Murdaca
9900b79eb6 cmd/skopeo: refactor layers command
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-06-28 09:50:50 +02:00
Antonio Murdaca
f420d6867b Merge pull request #126 from runcom/move-containers-image
*: move to containers/image
2016-06-27 17:48:58 +02:00
Antonio Murdaca
2e8bcf65f6 *: move to containers/image
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-06-27 17:17:13 +02:00
Antonio Murdaca
e7a76f750b Merge pull request #124 from duglin/license
Move to Apache 2 license
2016-06-24 23:06:49 +02:00
Doug Davis
de42d88d2c Move to Apache 2 license
Signed-off-by: Doug Davis <dug@us.ibm.com>
2016-06-24 11:35:34 -07:00
Antonio Murdaca
1d5e38454e Merge pull request #121 from vbatts/shorten_build_steps
README: fewer build steps
2016-06-23 23:48:55 +02:00
Vincent Batts
11a0108456 README: fewer build steps
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2016-06-23 15:19:27 -04:00
Antonio Murdaca
1cf2b63483 Merge pull request #103 from mtrmac/image-layer-digests
Move parsing layer digests from copy.go to types.Image
2016-06-23 18:53:06 +02:00
Miloslav Trmač
5b1ca76131 Only copy each layer once in (skopeo layers)
... using the new uniqueLayerDigests().
2016-06-23 18:09:57 +02:00
Miloslav Trmač
a23befcbf4 Add types.Image.LayerDigests, use it in (skopeo copy)
To do so, have (skopeo copy) work with a types.Image, and replace uses
of types.ImageSource with types.Image where possible to allow the
caching in types.Image to work.

This is a slight behavior change:
- The manifest is now processed through fixManifestLayers
- Duplicate layers (created e.g. when a non-filesystem-altering command is used
  in a Dockerfile) are only copied once.
2016-06-23 18:09:57 +02:00
Miloslav Trmač
c81541de0a Rename types.Image.Layers to LayersCommand
The .Layers() method name is too nice to contain this layering
violation; make it more explicit in the naming.
2016-06-23 15:53:14 +02:00
Miloslav Trmač
206a8e3eed Remove a FIXME? about types.Image.Manifest.
Per the discussion in https://github.com/projectatomic/skopeo/pull/73 ,
types.Image.Manifest should not need to expose MIME types:

ImageSource.GetManifest allows supplying MIME types; the intent is
for clients who want to parse the manifests to use an ImageSource.

Clients who want to use skopeo’s parsing should use types.Image, and
then they don’t need to care about MIME types. In fact, types.Image
needs to decide among the various manifest alternatives which one to
parse (and which one to match against the provided or signed manifest
digest). So, Image.Manifest will not be all that useful for parsing the
contents, it is basically useful only for verifying against a digest.
2016-06-23 15:53:14 +02:00
Antonio Murdaca
b3bcf49d46 Merge pull request #112 from runcom/manifest-pkg
move manifests stuff to its own pkg and add OCI mime types
2016-06-23 12:31:39 +02:00
Antonio Murdaca
705f393109 move manifests stuff to its own pkg and add OCI mime types
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-06-23 12:12:48 +02:00
Antonio Murdaca
6841ee321c Merge pull request #26 from mtrmac/signing-unit-tests
Signing unit tests
2016-06-23 11:55:51 +02:00
Antonio Murdaca
a45e8e1e87 Merge pull request #16 from mtrmac/cmd-test
Support for in-process command testing
2016-06-23 11:55:29 +02:00
Miloslav Trmač
ba2dabe62d Add unit tests for standalone-sign and standalone-verify commands 2016-06-22 20:54:18 +02:00
Miloslav Trmač
aa627d0844 Merge branch 'cmd-test' into HEAD 2016-06-22 20:54:17 +02:00
Miloslav Trmač
df076baf56 Use cli.Context.App.Writer in the "inspect" and "standalone-verify" commands
This will make the implementations testable in the future, and prevent
spreading the untestable code via copy&paste.
2016-06-22 20:53:00 +02:00
Miloslav Trmač
59f7abe749 Add infrastructure for testing cli.Command.Action handlers
Also split creation of cli.App from main(), and add a test helper
function.

This does not change behavior at the moment, but will allow writing
tests of the command handlers.
2016-06-22 20:31:04 +02:00
Antonio Murdaca
6bec0699cb Merge pull request #117 from mtrmac/atomic-registry-in-tests
Add Atomic registry integration tests
2016-06-22 18:29:14 +02:00
Miloslav Trmač
7d379cf87a Add integration tests for (skopeo copy) against the Atomic Registry
This builds from the image-signatures-rest branch for
https://github.com/openshift/origin/pull/9181 .

Testing push, pull, streaming.

Does not test working with the other Docker registries built in
Dockerfile; I will leave that to the author of that code :)

Note that this relies on an internet connection for pulling from the
Docker Hub (which is incidentally tested by that); pushing to no Docker
Registry, neither local nor Hub, is tested by this.

The tests only run in a container because the (oc login) / (docker
login)-like code modifies files in a home directory; the new
SKOPEO_CONTAINER_TESTS environment variable should protect against
accidental non-container runs.
2016-06-22 16:19:59 +02:00
Miloslav Trmač
39b06cb31c Add more helpers for running skopeo, use them in existing tests
- consumeAndLogOutputs
- assertSkopeoSucceeds
- assertSkopeoFails
- runCommandWithInput
All of these allow running commands as one-liners with no call-site
error handling, making tests much more readable.

Also modifies TestNoNeedAuthToPrivateRegistryV2ImageNotFound to use
check.Matches instead of manual strings.Contains conditions, which is
shorter and more consistent with the assertSkopeo... calls.
2016-06-22 16:19:59 +02:00
Miloslav Trmač
601f76f96d Fix consumeAndLogOutput
Primarily, make it actually work; reading into a non-zero-capacity but
zero-length slice would just return 0, the goroutine would terminate,
and even the producer of the output could fail with EPIPE/SIGPIPE.

Also make the logged output readable, converting it into a string
instead of a series of hexadecimal byte values.
2016-06-22 16:19:59 +02:00
Miloslav Trmač
2f2a688026 Move ConsumeAndLogOutput to integration/utils.go
This will be used also by non-signing tests.

No code changes besides removing the initial capital letter in the
function name; this is a separate commit only to make reviewing of
future changes to this function easier.
2016-06-22 16:19:59 +02:00
Antonio Murdaca
a2e9d08e38 Merge pull request #116 from runcom/fix-readme.md
update README.md
2016-06-22 16:02:53 +02:00
Antonio Murdaca
b6a38edfcb update README.md
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-06-22 15:52:29 +02:00
Antonio Murdaca
9a92a10bba Merge pull request #114 from GrantSeltzer/Codegangsta-to-urfave-cli
Codegangsta to urfave cli
2016-06-22 15:51:49 +02:00
Grantseltzer
5ae0402bf0 Changed usage of actions to return errors instead of using logrus.Fatal() 2016-06-22 09:42:52 -04:00
Grantseltzer
313dafe928 update github.com/urfave/cli to v1.17.0
Signed-off-by: Antonio Murdaca <runcom@redhat.com>

Updated action function signatures to return errors
2016-06-20 14:57:00 -04:00
Antonio Murdaca
f4ddde7f47 Merge pull request #115 from mtrmac/skopeo.1
Remove /skopeo.1 from .gitignore
2016-06-20 20:46:07 +02:00
Miloslav Trmač
4a2a78b63b Remove /skopeo.1 from .gitignore
/skopeo.1 was a generated file before #35; now this path is not used
(replaced by man1/skopeo.1); if the generated file is left around, it is
obsolete (and confusingly empty).  Remove it from .gitignore to nudge
developers like me to clean up.
2016-06-20 20:35:18 +02:00
Antonio Murdaca
35dd662fea Merge pull request #104 from projectatomic/expose-blob-size
expose blob size
2016-06-15 20:01:24 +02:00
Antonio Murdaca
7769a21cef expose blob size
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-06-15 19:52:16 +02:00
Antonio Murdaca
a50211ce2a Merge pull request #101 from projectatomic/oci-prep-1
Generalize [Get|Put]Layer
2016-06-14 14:01:16 +02:00
Antonio Murdaca
d54a10f490 Image[Source|Destination]: generalize [Get|Put]Layer into [Get|Put]Blob
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-06-14 13:53:53 +02:00
Antonio Murdaca
3098898a98 Merge pull request #99 from mtrmac/fixManifestLayers-docs
Add minimal comments to fixManifestLayers
2016-06-13 18:25:07 +02:00
Miloslav Trmač
cab18e48ad Add minimal comments to fixManifestLayers
This does not really go into why duplicate layers can happen or why it
is worth supporting that; the code originates from
504e67b867 ,
which does not explain either.
2016-06-13 18:07:03 +02:00
Antonio Murdaca
a8a3cc3525 Merge pull request #98 from mtrmac/generic-image
Move docker.genericImage to a separate skopeo/image subpackage
2016-06-13 11:36:58 +02:00
Miloslav Trmač
96d6a58052 Move docker.genericImage to a separate skopeo/image subpackage
... making image.FromSource a public, stable, API.
2016-06-11 10:48:57 +02:00
Miloslav Trmač
e15276232e Make docker.Image unaware of genericImage internals
This will allow us to cleanly move genericImage into a separate package.

This costs an extra pointer, but also allows us to rely on the type
system and drop handling "certainly impossible" errors, worth it just
for this simplification anyway.
2016-06-11 10:48:57 +02:00
Antonio Murdaca
daeb358572 Merge pull request #96 from mtrmac/update-readme
Update README.md
2016-06-11 09:52:31 +02:00
Miloslav Trmač
55622350c4 Show (skopeo copy) and (skopeo delete) in README.md 2016-06-11 03:34:53 +02:00
Miloslav Trmač
29d189b581 Recommend (make check) instead of (make test-integration)
... so that we also run validate-* and unit tests.
2016-06-11 03:20:27 +02:00
Miloslav Trmač
d947d90bf7 Merge pull request #95 from jwhonce/wip/delete-image
Card container_security_113 - Delete image support
2016-06-11 03:02:43 +02:00
Jhon Honce
f3efa063e3 Card container_security_113 - Delete image support
Add support to mark images for deletion from repository

Requires:
  * V2 API and schema
  * registry configured to allow deletes
  * run registry garbage collection to free up disk space

Signed-off-by: Jhon Honce <jhonce@redhat.com>
2016-06-09 15:23:02 -07:00
Antonio Murdaca
0ff261802b Merge pull request #94 from projectatomic/readme-tweak
README.md: fix examples
2016-06-07 18:41:40 +02:00
Antonio Murdaca
fb236c85af README.md: fix examples
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-06-07 18:27:31 +02:00
Antonio Murdaca
9c4ceeb147 Merge pull request #92 from mtrmac/fix-blob-upload
Fix uploading layer blobs to Docker registry
2016-06-06 23:12:48 +02:00
Miloslav Trmač
fc761ed74f Fix uploading layer blobs to Docker registry
Implement a client to the chunked API, instead of the nonexistent
one-shot API (per
2a4deee441
).

Adds a FIXME to DELETE the pending upload on failure; the uploads are
supposed to time out so this is not immediately critical.

Fixes #64 .
2016-06-06 23:00:58 +02:00
Antonio Murdaca
e66541f7d0 Merge pull request #90 from mtrmac/cleanups
Another random cleanup
2016-06-02 21:44:14 +02:00
Miloslav Trmač
000f31fb73 Better test diagnostics 2016-06-02 21:16:56 +02:00
Miloslav Trmač
bc8041add8 Merge pull request #88 from mtrmac/policy-eval
Add a policy evaluation library
2016-06-02 16:25:23 +02:00
Miloslav Trmač
21229685cf Add PolicyContext, with GetSignaturesWithAcceptedAuthor and IsRunningImageAllowed
PolicyContext is intended to be the primary API for skopeo/signature:
supply a policy and an image, and ask specific, well-defined
(preferably yes/no) questions.
2016-06-02 16:12:10 +02:00
Miloslav Trmač
fd9c615d88 Add PolicyRequirement implementations
Also move the declaration of the type from the mostly-public
policy_types.go to policy_eval.go.
2016-06-02 16:12:10 +02:00
Miloslav Trmač
90361256bc Add PolicyReferenceMatch implementations
Also move the declaration of the type from the mostly-public
policy_types.go to policy_eval.go.
2016-06-02 16:12:10 +02:00
Miloslav Trmač
677f711c6c Redefine Policy.Specific scopes to use fully expanded hostname/namespace/repo format
Using the canonical minimized format of Docker references introduces too
many ambiguities.

This also removes some validation of the scope string, but all that was
really doing was rejecting completely invalid input like uppercase.

Sadly it is not qutie obvious that we can detect and reject mistakes like
using "busybox" as a scope instead of the correct
"docker.io/library/busybox".  Perhaps require at least one dot or port
number in the host name?
2016-06-02 16:12:10 +02:00
Miloslav Trmač
488a535aa0 Use callbacks instead of single expected values in verifyAndExtractSignature
To support verification of signatures when more than one key, or more
than one identity, are accepted, have verifyAndExtract signature accept
callbacks (in a struct so that they are explicitly named).

verifyAndExtractSignature now also validates the manifest digest.  It is
intended to become THE SINGLE PLACE where untrusted signature blobs
have signatures verified, are validated against other expectations, and
parsed, and converted into internal data structures available to other
code.

Also:
- Modifies VerifyDockerManifestSignature to use utils.ManifestMatchesDigest.
- Adds a test for Docker reference mismatch in VerifyDockerManifestSignature.
2016-06-02 16:12:10 +02:00
Miloslav Trmač
e2839c38c5 Add a test for valid signature using an unknown public key
(The key was one-time-generated in a temporary directory,
and is, intentionally, not available.)

This is not conceptually related to the rest of the PR, just adding a
missing case to the test, except that the added fixture will be reused
in a prSignedBy test.
2016-06-02 16:12:10 +02:00
Antonio Murdaca
ee7c5ebae9 Merge pull request #75 from mtrmac/matches-manifest-digest
Add docker/utils.ManifestMatchesDigest
2016-06-02 11:27:07 +02:00
Miloslav Trmač
938478e702 Add docker.utils.ManifestMatchesDigest
As opposed to callers just calling utils.ManifestDigest(), this is
a forward-compatible interface, allowing other digest algorithms to
be added in the future.

Right now, we only support SHA-256, so the underlying implementation
does not change anything.
2016-06-01 16:38:11 +02:00
Antonio Murdaca
837fc231a9 Merge pull request #87 from mtrmac/cleanups
Cleanups
2016-05-31 18:15:02 +02:00
Miloslav Trmač
429a4b0aec Do not drop the underlying error message when a Docker reference is invalid 2016-05-31 17:10:34 +02:00
Miloslav Trmač
e332d0e5d7 Fix a typo 2016-05-31 17:10:34 +02:00
Antonio Murdaca
2e917cf146 Merge pull request #86 from projectatomic/bump-again-v0.1.14-dev
bump v0.1.14-dev
2016-05-31 17:03:01 +02:00
Antonio Murdaca
e7020c2d8c bump v0.1.14-dev
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-05-31 16:45:19 +02:00
326 changed files with 27362 additions and 5801 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,3 @@
/skopeo
/skopeo.1
/docs/skopeo.1
/layers-*
/skopeo

142
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,142 @@
# Contributing to Skopeo
We'd love to have you join the community! Below summarizes the processes
that we follow.
## Topics
* [Reporting Issues](#reporting-issues)
* [Submitting Pull Requests](#submitting-pull-requests)
* [Communications](#communications)
* [Becomign a Maintainer](#becoming-a-maintainer)
## Reporting Issues
Before reporting an issue, check our backlog of
[open issues](https://github.com/projectatomic/skopeo/issues)
to see if someone else has already reported it. If so, feel free to add
your scenario, or additional information, to the discussion. Or simply
"subscribe" to it to be notified when it is updated.
If you find a new issue with the project we'd love to hear about it! The most
important aspect of a bug report is that it includes enough information for
us to reproduce it. So, please include as much detail as possible and try
to remove the extra stuff that doesn't really relate to the issue itself.
The easier it is for us to reproduce it, the faster it'll be fixed!
Please don't include any private/sensitive information in your issue!
## Submitting Pull Requests
No Pull Request (PR) is too small! Typos, additional comments in the code,
new testcases, bug fixes, new features, more documentation, ... it's all
welcome!
While bug fixes can first be identified via an "issue", that is not required.
It's ok to just open up a PR with the fix, but make sure you include the same
information you would have included in an issue - like how to reproduce it.
PRs for new features should include some background on what use cases the
new code is trying to address. And, when possible and it makes, try to break-up
larger PRs into smaller ones - it's easier to review smaller
code changes. But, only if those smaller ones make sense as stand-alone PRs.
Regardless of the type of PR, all PRs should include:
* well documented code changes
* additional testcases. Ideally, they should fail w/o your code change applied
* documentation changes
Squash your commits into logical pieces of work that might want to be reviewed
separate from the rest of the PRs. But, squashing down to just one commit is ok
too since in the end the entire PR will be reviewed anyway. When in doubt,
squash.
PRs that fix issues should include a reference like `Closes #XXXX` in the
commit message so that github will automatically close the referenced issue
when the PR is merged.
<!--
All PRs require at least two LGTMs (Looks Good To Me) from maintainers.
-->
### Sign your PRs
The sign-off is a line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are simple: if you can certify
the below (from [developercertificate.org](http://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.
## Communications
For general questions, or dicsussions, please use the
IRC group on `irc.freenode.net` called `container-projects`
that has been setup.
For discussions around issues/bugs and features, you can use the github
[issues](https://github.com/projectatomic/skopeo/issues)
and
[PRs](https://github.com/projectatomic/skopeo/pulls)
tracking system.
<!--
## Becoming a Maintainer
To become a maintainer you must first be nominated by an existing maintainer.
If a majority (>50%) of maintainers agree then the proposal is adopted and
you will be added to the list.
Removing a maintainer requires at least 75% of the remaining maintainers
approval, or if the person requests to be removed then it is automatic.
Normally, a maintainer will only be removed if they are considered to be
inactive for a long period of time or are viewed as disruptive to the community.
The current list of maintainers can be found in the
[MAINTAINERS](MAINTAINERS) file.
-->

View File

@@ -40,6 +40,15 @@ RUN set -x \
< "$DRV1/contrib/boto_header_patch.diff" \
&& dnf -y update && dnf install -y m2crypto
RUN set -x \
&& yum install -y which git tar wget hostname util-linux bsdtar socat ethtool device-mapper iptables tree findutils nmap-ncat e2fsprogs xfsprogs lsof docker iproute \
&& export GOPATH=$(mktemp -d) \
&& git clone -b v1.3.0-alpha.3 git://github.com/openshift/origin "$GOPATH/src/github.com/openshift/origin" \
&& (cd "$GOPATH/src/github.com/openshift/origin" && make clean build && make all WHAT=cmd/dockerregistry) \
&& cp -a "$GOPATH/src/github.com/openshift/origin/_output/local/bin/linux"/*/* /usr/local/bin \
&& cp "$GOPATH/src/github.com/openshift/origin/images/dockerregistry/config.yml" /atomic-registry-config.yml \
&& mkdir /registry
ENV GOPATH /usr/share/gocode:/go
ENV PATH $GOPATH/bin:/usr/share/gocode/bin:$PATH
RUN go get github.com/golang/lint/golint

5
Dockerfile.build Normal file
View File

@@ -0,0 +1,5 @@
FROM ubuntu:16.04
RUN apt-get update
RUN apt-get install -y golang git-core libgpgme11-dev
ENV GOPATH=/
WORKDIR /src/github.com/projectatomic/skopeo

468
LICENSE
View File

@@ -1,339 +1,189 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
Preamble
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
1. Definitions.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
The precise terms and conditions for copying, distribution and
modification follow.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
END OF TERMS AND CONDITIONS
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
https://www.apache.org/licenses/LICENSE-2.0
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,12 +1,18 @@
.PHONY: all binary build clean install install-binary shell test-integration
.PHONY: all binary build-container build-local clean install install-binary install-completions shell test-integration
export GO15VENDOREXPERIMENT=1
PREFIX ?= ${DESTDIR}/usr
INSTALLDIR=${PREFIX}/bin
MANINSTALLDIR=${PREFIX}/share/man
# TODO(runcom)
#BASHINSTALLDIR=${PREFIX}/share/bash-completion/completions
CONTAINERSSYSCONFIGDIR=${DESTDIR}/etc/containers
REGISTRIESDDIR=${CONTAINERSSYSCONFIGDIR}/registries.d
BASHINSTALLDIR=${PREFIX}/share/bash-completion/completions
GO_MD2MAN ?= /usr/bin/go-md2man
ifeq ($(DEBUG), 1)
override GOGCFLAGS += -N -l
endif
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
DOCKER_IMAGE := skopeo-dev$(if $(GIT_BRANCH),:$(GIT_BRANCH))
@@ -23,26 +29,50 @@ DOCKER_RUN_DOCKER := $(DOCKER_FLAGS) "$(DOCKER_IMAGE)"
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null || true)
all: binary
MANPAGES_MD = $(wildcard docs/*.md)
# make all DEBUG=1
# Note: Uses the -N -l go compiler options to disable compiler optimizations
# and inlining. Using these build options allows you to subsequently
# use source debugging tools like delve.
all: binary docs
# Build a docker image (skopeobuild) that has everything we need to build.
# Then do the build and the output (skopeo) should appear in current dir
binary: cmd/skopeo
docker build ${DOCKER_BUILD_ARGS} -f Dockerfile.build -t skopeobuildimage .
docker run --rm --security-opt label:disable -v $$(pwd):/src/github.com/projectatomic/skopeo \
skopeobuildimage make binary-local $(if $(DEBUG),DEBUG=$(DEBUG))
# Build w/o using Docker containers
binary-local:
go build -ldflags "-X main.gitCommit=${GIT_COMMIT}" -gcflags "$(GOGCFLAGS)" -o skopeo ./cmd/skopeo
binary:
go build -ldflags "-X main.gitCommit=${GIT_COMMIT}" -o ${DEST}skopeo ./cmd/skopeo
build-container:
docker build ${DOCKER_BUILD_ARGS} -t "$(DOCKER_IMAGE)" .
docs/%.1: docs/%.1.md
$(GO_MD2MAN) -in $< -out $@.tmp && touch $@.tmp && mv $@.tmp $@
.PHONY: docs
docs: $(MANPAGES_MD:%.md=%)
clean:
rm -f skopeo
rm -f skopeo docs/*.1
install: install-binary
install -m 644 man1/skopeo.1 ${MANINSTALLDIR}/man1/
# TODO(runcom)
#install -m 644 completion/bash/skopeo ${BASHINSTALLDIR}/
install: install-binary install-docs install-completions
install -D -m 644 default-policy.json ${CONTAINERSSYSCONFIGDIR}/policy.json
install -d -m 755 ${REGISTRIESDDIR}
install-binary:
install -d -m 0755 ${INSTALLDIR}
install -m 755 skopeo ${INSTALLDIR}
install-binary: ./skopeo
install -D -m 755 skopeo ${INSTALLDIR}/skopeo
install-docs: docs/skopeo.1
install -D -m 644 docs/skopeo.1 ${MANINSTALLDIR}/man1/skopeo.1
install-completions:
install -m 644 -D hack/make/bash_autocomplete ${BASHINSTALLDIR}/skopeo
shell: build-container
$(DOCKER_RUN_DOCKER) bash
@@ -51,7 +81,7 @@ check: validate test-unit test-integration
# The tests can run out of entropy and block in containers, so replace /dev/random.
test-integration: build-container
$(DOCKER_RUN_DOCKER) bash -c 'rm -f /dev/random; ln -sf /dev/urandom /dev/random; hack/make.sh test-integration'
$(DOCKER_RUN_DOCKER) bash -c 'rm -f /dev/random; ln -sf /dev/urandom /dev/random; SKOPEO_CONTAINER_TESTS=1 hack/make.sh test-integration'
test-unit: build-container
# Just call (make test unit-local) here instead of worrying about environment differences, e.g. GO15VENDOREXPERIMENT.
@@ -60,7 +90,7 @@ test-unit: build-container
validate: build-container
$(DOCKER_RUN_DOCKER) hack/make.sh validate-git-marks validate-gofmt validate-lint validate-vet
# This target is only intended for development, e.g. executing it from an IDE. Use (make test) for CI or pre-release testing.
# This target is only intended for development, e.g. executing it from an IDE. Use (make test) for CI or pre-release testing.
test-all-local: validate-local test-unit-local
validate-local:

109
README.md
View File

@@ -1,47 +1,66 @@
skopeo [![Build Status](https://travis-ci.org/projectatomic/skopeo.svg?branch=master)](https://travis-ci.org/projectatomic/skopeo)
=
_Please be aware `skopeo` is still work in progress_
_Please be aware `skopeo` is still work in progress and it currently supports only registry API V2_
`skopeo` is a command line utility which is able to _inspect_ a repository on a Docker registry and fetch images layers.
`skopeo` is a command line utility for various operations on container images and image repositories.
Inspecting a repository
-
`skopeo` is able to _inspect_ a repository on a Docker registry and fetch images layers.
By _inspect_ I mean it fetches the repository's manifest and it is able to show you a `docker inspect`-like
json output about a whole repository or a tag. This tool, in contrast to `docker inspect`, helps you gather useful information about
a repository or a tag before pulling it (using disk space) - e.g. - which tags are available for the given repository? which labels the image has?
Examples:
```sh
# show repository's labels of rhel7:latest
$ skopeo inspect docker://registry.access.redhat.com/rhel7 | jq '.Config.Labels'
# show properties of fedora:latest
$ skopeo inspect docker://docker.io/fedora
{
"Architecture": "x86_64",
"Authoritative_Registry": "registry.access.redhat.com",
"BZComponent": "rhel-server-docker",
"Build_Host": "rcm-img04.build.eng.bos.redhat.com",
"Name": "rhel7/rhel",
"Release": "38",
"Vendor": "Red Hat, Inc.",
"Version": "7.2"
"Name": "docker.io/library/fedora",
"Tag": "latest",
"Digest": "sha256:cfd8f071bf8da7a466748f522406f7ae5908d002af1b1a1c0dcf893e183e5b32",
"RepoTags": [
"20",
"21",
"22",
"23",
"heisenbug",
"latest",
"rawhide"
],
"Created": "2016-03-04T18:40:02.92155334Z",
"DockerVersion": "1.9.1",
"Labels": {},
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:236608c7b546e2f4e7223526c74fc71470ba06d46ec82aeb402e704bfdee02a2",
"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
]
}
# show repository's tags
$ skopeo inspect docker://docker.io/fedora | jq '.RepoTags'
[
"20",
"21",
"22",
"23",
"heisenbug",
"latest",
"rawhide"
]
# show image's digest
# show unverifed image's digest
$ skopeo inspect docker://docker.io/fedora:rawhide | jq '.Digest'
"sha256:905b4846938c8aef94f52f3e41a11398ae5b40f5855fb0e40ed9c157e721d7f8"
```
# show image's label "Name"
$ skopeo inspect docker://registry.access.redhat.com/rhel7 | jq '.Config.Labels.Name'
"rhel7/rhel"
Copying images
-
`skopeo` can copy container images between various storage mechanisms,
e.g. Docker registries (including the Docker Hub), the Atomic Registry,
and local directories:
```sh
$ skopeo copy docker://busybox:1-glibc atomic:myns/unsigned:streaming
$ skopeo copy docker://busybox:latest dir:existingemptydirectory
```
Deleting images
-
For example,
```sh
$ skopeo delete docker://localhost:5000/imagename:latest
```
Private registries with authentication
@@ -81,25 +100,19 @@ you'll get an error. You can fix this by either logging in (via `docker login`)
and `--password`.
Building
-
To build `skopeo` you need at least Go 1.5 because it uses the latest `GO15VENDOREXPERIMENT` flag. Also, make sure to clone the repository in your `GOPATH` - otherwise compilation fails.
To build the manual you will need go-md2man.
```sh
$ cd $GOPATH/src
$ mkdir -p github.com/projectatomic
$ cd projectatomic
$ git clone https://github.com/projectatomic/skopeo
$ cd skopeo && make binary
$ sudo apt-get install go-md2man
```
To build the `skopeo` binary you need at least Go 1.5 because it uses the latest `GO15VENDOREXPERIMENT` flag. Also, make sure to clone the repository in your `GOPATH` - otherwise compilation fails.
```sh
$ git clone https://github.com/projectatomic/skopeo $GOPATH/src/github.com/projectatomic/skopeo
$ cd $GOPATH/src/github.com/projectatomic/skopeo && make all
```
You may need to install additional development packages: gpgme-devel and libassuan-devel
```sh
# dnf install gpgme-devel libassuan-devel
```
Man:
-
To build the man page you need [`go-md2man`](https://github.com/cpuguy83/go-md2man) available on your system, then:
```
$ make man
$ dnf install gpgme-devel libassuan-devel
```
Installing
-
@@ -111,18 +124,11 @@ $ sudo make install
```sh
sudo dnf install skopeo
```
Tests
-
_You need Docker installed on your system in order to run the test suite_
```sh
$ make test-integration
```
TODO
-
- update README with `layers` command
- list all images on registry?
- registry v2 search?
- download layers in parallel and support docker load tar(s)
- support output to docker load tar(s)
- show repo tags via flag or when reference isn't tagged or digested
- add tests (integration with deployed registries in container - Docker-like)
- support rkt/appc image spec
@@ -133,4 +139,5 @@ NOT TODO
License
-
GPLv2
skopeo is licensed under the Apache License, Version 2.0. See
[LICENSE](LICENSE) for the full license text.

View File

@@ -1,120 +1,58 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/projectatomic/skopeo/docker/utils"
"github.com/projectatomic/skopeo/signature"
"github.com/containers/image/copy"
"github.com/containers/image/transports"
"github.com/urfave/cli"
)
// FIXME: Also handle schema2, and put this elsewhere:
// docker.go contains similar code, more sophisticated
// (at the very least the deduplication should be reused from there).
func manifestLayers(manifest []byte) ([]string, error) {
man := manifestSchema1{}
if err := json.Unmarshal(manifest, &man); err != nil {
return nil, err
}
layers := []string{}
for _, layer := range man.FSLayers {
layers = append(layers, layer.BlobSum)
}
return layers, nil
}
// FIXME: this is a copy from docker_image.go and does not belong here.
type manifestSchema1 struct {
Name string
Tag string
FSLayers []struct {
BlobSum string `json:"blobSum"`
} `json:"fsLayers"`
History []struct {
V1Compatibility string `json:"v1Compatibility"`
} `json:"history"`
// TODO(runcom) verify the downloaded manifest
//Signature []byte `json:"signature"`
}
func copyHandler(context *cli.Context) {
func copyHandler(context *cli.Context) error {
if len(context.Args()) != 2 {
logrus.Fatal("Usage: copy source destination")
return errors.New("Usage: copy source destination")
}
src, err := parseImageSource(context, context.Args()[0])
policyContext, err := getPolicyContext(context)
if err != nil {
logrus.Fatalf("Error initializing %s: %s", context.Args()[0], err.Error())
return fmt.Errorf("Error loading trust policy: %v", err)
}
defer policyContext.Destroy()
dest, err := parseImageDestination(context, context.Args()[1])
srcRef, err := transports.ParseImageName(context.Args()[0])
if err != nil {
logrus.Fatalf("Error initializing %s: %s", context.Args()[1], err.Error())
return fmt.Errorf("Invalid source name %s: %v", context.Args()[0], err)
}
destRef, err := transports.ParseImageName(context.Args()[1])
if err != nil {
return fmt.Errorf("Invalid destination name %s: %v", context.Args()[1], err)
}
signBy := context.String("sign-by")
removeSignatures := context.Bool("remove-signatures")
manifest, _, err := src.GetManifest([]string{utils.DockerV2Schema1MIMEType})
if err != nil {
logrus.Fatalf("Error reading manifest: %s", err.Error())
}
layers, err := manifestLayers(manifest)
if err != nil {
logrus.Fatalf("Error parsing manifest: %s", err.Error())
}
for _, layer := range layers {
stream, err := src.GetLayer(layer)
if err != nil {
logrus.Fatalf("Error reading layer %s: %s", layer, err.Error())
}
defer stream.Close()
if err := dest.PutLayer(layer, stream); err != nil {
logrus.Fatalf("Error writing layer: %s", err.Error())
}
}
sigs, err := src.GetSignatures()
if err != nil {
logrus.Fatalf("Error reading signatures: %s", err.Error())
}
if signBy != "" {
mech, err := signature.NewGPGSigningMechanism()
if err != nil {
logrus.Fatalf("Error initializing GPG: %s", err.Error())
}
dockerReference, err := dest.CanonicalDockerReference()
if err != nil {
logrus.Fatalf("Error determining canonical Docker reference: %s", err.Error())
}
newSig, err := signature.SignDockerManifest(manifest, dockerReference, mech, signBy)
if err != nil {
logrus.Fatalf("Error creating signature: %s", err.Error())
}
sigs = append(sigs, newSig)
}
if err := dest.PutSignatures(sigs); err != nil {
logrus.Fatalf("Error writing signatures: %s", err.Error())
}
// FIXME: We need to call PutManifest after PutLayer and PutSignatures. This seems ugly; move to a "set properties" + "commit" model?
if err := dest.PutManifest(manifest); err != nil {
logrus.Fatalf("Error writing manifest: %s", err.Error())
}
return copy.Image(contextFromGlobalOptions(context), policyContext, destRef, srcRef, &copy.Options{
RemoveSignatures: removeSignatures,
SignBy: signBy,
ReportWriter: os.Stdout,
})
}
var copyCmd = cli.Command{
Name: "copy",
Action: copyHandler,
Name: "copy",
Usage: "Copy an image from one location to another",
ArgsUsage: "SOURCE-IMAGE DESTINATION-IMAGE",
Action: copyHandler,
// FIXME: Do we need to namespace the GPG aspect?
Flags: []cli.Flag{
cli.BoolFlag{
Name: "remove-signatures",
Usage: "Do not copy signatures from SOURCE-IMAGE",
},
cli.StringFlag{
Name: "sign-by",
Usage: "sign the image using a GPG key with the specified fingerprint",
Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`",
},
},
}

32
cmd/skopeo/delete.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"errors"
"fmt"
"github.com/containers/image/transports"
"github.com/urfave/cli"
)
func deleteHandler(context *cli.Context) error {
if len(context.Args()) != 1 {
return errors.New("Usage: delete imageReference")
}
ref, err := transports.ParseImageName(context.Args()[0])
if err != nil {
return fmt.Errorf("Invalid source name %s: %v", context.Args()[0], err)
}
if err := ref.DeleteImage(contextFromGlobalOptions(context)); err != nil {
return err
}
return nil
}
var deleteCmd = cli.Command{
Name: "delete",
Usage: "Delete image IMAGE-NAME",
ArgsUsage: "IMAGE-NAME",
Action: deleteHandler,
}

View File

@@ -5,16 +5,15 @@ import (
"fmt"
"time"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/projectatomic/skopeo/docker"
"github.com/projectatomic/skopeo/docker/utils"
"github.com/containers/image/docker"
"github.com/containers/image/manifest"
"github.com/urfave/cli"
)
// inspectOutput is the output format of (skopeo inspect), primarily so that we can format it with a simple json.MarshalIndent.
type inspectOutput struct {
Name string `json:",omitempty"`
Tag string
Tag string `json:",omitempty"`
Digest string
RepoTags []string
Created time.Time
@@ -26,30 +25,36 @@ type inspectOutput struct {
}
var inspectCmd = cli.Command{
Name: "inspect",
Usage: "inspect images on a registry",
Name: "inspect",
Usage: "Inspect image IMAGE-NAME",
ArgsUsage: "IMAGE-NAME",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "raw",
Usage: "output raw manifest",
},
},
Action: func(c *cli.Context) {
Action: func(c *cli.Context) error {
img, err := parseImage(c)
if err != nil {
logrus.Fatal(err)
return err
}
rawManifest, err := img.Manifest()
defer img.Close()
rawManifest, _, err := img.Manifest()
if err != nil {
logrus.Fatal(err)
return err
}
if c.Bool("raw") {
fmt.Println(string(rawManifest))
return
_, err := c.App.Writer.Write(rawManifest)
if err != nil {
return fmt.Errorf("Error writing manifest to standard output: %v", err)
}
return nil
}
imgInspect, err := img.Inspect()
if err != nil {
logrus.Fatal(err)
return err
}
outputData := inspectOutput{
Name: "", // Possibly overridden for a docker.Image.
@@ -63,24 +68,22 @@ var inspectCmd = cli.Command{
Os: imgInspect.Os,
Layers: imgInspect.Layers,
}
outputData.Digest, err = utils.ManifestDigest(rawManifest)
outputData.Digest, err = manifest.Digest(rawManifest)
if err != nil {
logrus.Fatalf("Error computing manifest digest: %s", err.Error())
return fmt.Errorf("Error computing manifest digest: %v", err)
}
if dockerImg, ok := img.(*docker.Image); ok {
outputData.Name, err = dockerImg.SourceRefFullName()
if err != nil {
logrus.Fatalf("Error getting expanded repository name: %s", err.Error())
}
outputData.Name = dockerImg.SourceRefFullName()
outputData.RepoTags, err = dockerImg.GetRepositoryTags()
if err != nil {
logrus.Fatalf("Error determining repository tags: %s", err.Error())
return fmt.Errorf("Error determining repository tags: %v", err)
}
}
out, err := json.MarshalIndent(outputData, "", " ")
if err != nil {
logrus.Fatal(err)
return err
}
fmt.Println(string(out))
fmt.Fprintln(c.App.Writer, string(out))
return nil
},
}

View File

@@ -1,21 +1,97 @@
package main
import (
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"io/ioutil"
"strings"
"github.com/containers/image/directory"
"github.com/containers/image/image"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
"github.com/urfave/cli"
)
// TODO(runcom): document args and usage
var layersCmd = cli.Command{
Name: "layers",
Usage: "get images layers",
Action: func(c *cli.Context) {
img, err := parseImage(c)
Name: "layers",
Usage: "Get layers of IMAGE-NAME",
ArgsUsage: "IMAGE-NAME",
Action: func(c *cli.Context) error {
rawSource, err := parseImageSource(c, c.Args()[0], []string{
// TODO: skopeo layers only support these now
// eventually we'll remove this command altogether...
manifest.DockerV2Schema1SignedMediaType,
manifest.DockerV2Schema1MediaType,
})
if err != nil {
logrus.Fatal(err)
return err
}
if err := img.Layers(c.Args().Tail()...); err != nil {
logrus.Fatal(err)
src := image.FromSource(rawSource)
defer src.Close()
blobDigests := c.Args().Tail()
if len(blobDigests) == 0 {
layers, err := src.LayerInfos()
if err != nil {
return err
}
seenLayers := map[string]struct{}{}
for _, info := range layers {
if _, ok := seenLayers[info.Digest]; !ok {
blobDigests = append(blobDigests, info.Digest)
seenLayers[info.Digest] = struct{}{}
}
}
configInfo, err := src.ConfigInfo()
if err != nil {
return err
}
if configInfo.Digest != "" {
blobDigests = append(blobDigests, configInfo.Digest)
}
}
tmpDir, err := ioutil.TempDir(".", "layers-")
if err != nil {
return err
}
tmpDirRef, err := directory.NewReference(tmpDir)
if err != nil {
return err
}
dest, err := tmpDirRef.NewImageDestination(nil)
if err != nil {
return err
}
defer dest.Close()
for _, digest := range blobDigests {
if !strings.HasPrefix(digest, "sha256:") {
digest = "sha256:" + digest
}
r, blobSize, err := rawSource.GetBlob(digest)
if err != nil {
return err
}
if _, err := dest.PutBlob(r, types.BlobInfo{Digest: digest, Size: blobSize}); err != nil {
r.Close()
return err
}
r.Close()
}
manifest, _, err := src.Manifest()
if err != nil {
return err
}
if err := dest.PutManifest(manifest); err != nil {
return err
}
if err := dest.Commit(); err != nil {
return err
}
return nil
},
}

View File

@@ -5,27 +5,26 @@ import (
"os"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/containers/image/signature"
"github.com/projectatomic/skopeo/version"
"github.com/urfave/cli"
)
// gitCommit will be the hash that the binary was built from
// and will be populated by the Makefile
var gitCommit = ""
const (
usage = `interact with registries`
)
func main() {
// createApp returns a cli.App to be run or tested.
func createApp() *cli.App {
app := cli.NewApp()
app.EnableBashCompletion = true
app.Name = "skopeo"
if gitCommit != "" {
app.Version = fmt.Sprintf("%s commit: %s", version.Version, gitCommit)
} else {
app.Version = version.Version
}
app.Usage = usage
app.Usage = "Various operations with container images and container image registries"
// TODO(runcom)
//app.EnableBashCompletion = true
app.Flags = []cli.Flag{
@@ -36,21 +35,31 @@ func main() {
cli.StringFlag{
Name: "username",
Value: "",
Usage: "registry username",
Usage: "use `USERNAME` for accessing the registry",
},
cli.StringFlag{
Name: "password",
Value: "",
Usage: "registry password",
Usage: "use `PASSWORD` for accessing the registry",
},
cli.StringFlag{
Name: "cert-path",
Value: "",
Usage: "Certificates path to connect to the given registry (cert.pem, key.pem)",
Usage: "use certificates at `PATH` (cert.pem, key.pem) to connect to the registry",
},
cli.BoolFlag{
cli.BoolTFlag{
Name: "tls-verify",
Usage: "Whether to verify certificates or not",
Usage: "require HTTPS and verify certificates when talking to docker registries (defaults to true)",
},
cli.StringFlag{
Name: "policy",
Value: "",
Usage: "Path to a trust policy file",
},
cli.StringFlag{
Name: "registries.d",
Value: "",
Usage: "use registry configuration files in `DIR` (e.g. for docker signature storage)",
},
}
app.Before = func(c *cli.Context) error {
@@ -63,10 +72,33 @@ func main() {
copyCmd,
inspectCmd,
layersCmd,
deleteCmd,
manifestDigestCmd,
standaloneSignCmd,
standaloneVerifyCmd,
}
return app
}
func main() {
app := createApp()
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}
// getPolicyContext handles the global "policy" flag.
func getPolicyContext(c *cli.Context) (*signature.PolicyContext, error) {
policyPath := c.GlobalString("policy")
var policy *signature.Policy // This could be cached across calls, if we had an application context.
var err error
if policyPath == "" {
policy, err = signature.DefaultPolicy(nil)
} else {
policy, err = signature.NewPolicyFromFile(policyPath)
}
if err != nil {
return nil, err
}
return signature.NewPolicyContext(policy)
}

14
cmd/skopeo/main_test.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import "bytes"
// runSkopeo creates an app object and runs it with args, with an implied first "skopeo".
// Returns output intended for stdout and the returned error, if any.
func runSkopeo(args ...string) (string, error) {
app := createApp()
stdout := bytes.Buffer{}
app.Writer = &stdout
args = append([]string{"skopeo"}, args...)
err := app.Run(args)
return stdout.String(), err
}

35
cmd/skopeo/manifest.go Normal file
View File

@@ -0,0 +1,35 @@
package main
import (
"errors"
"fmt"
"io/ioutil"
"github.com/containers/image/manifest"
"github.com/urfave/cli"
)
func manifestDigest(context *cli.Context) error {
if len(context.Args()) != 1 {
return errors.New("Usage: skopeo manifest-digest manifest")
}
manifestPath := context.Args()[0]
man, err := ioutil.ReadFile(manifestPath)
if err != nil {
return fmt.Errorf("Error reading manifest from %s: %v", manifestPath, err)
}
digest, err := manifest.Digest(man)
if err != nil {
return fmt.Errorf("Error computing digest: %v", err)
}
fmt.Fprintf(context.App.Writer, "%s\n", digest)
return nil
}
var manifestDigestCmd = cli.Command{
Name: "manifest-digest",
Usage: "Compute a manifest digest of a file",
ArgsUsage: "MANIFEST",
Action: manifestDigest,
}

View File

@@ -0,0 +1,31 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestManifestDigest(t *testing.T) {
// Invalid command-line arguments
for _, args := range [][]string{
{},
{"a1", "a2"},
} {
out, err := runSkopeo(append([]string{"manifest-digest"}, args...)...)
assertTestFailed(t, out, err, "Usage")
}
// Error reading manifest
out, err := runSkopeo("manifest-digest", "/this/doesnt/exist")
assertTestFailed(t, out, err, "/this/doesnt/exist")
// Error computing manifest
out, err = runSkopeo("manifest-digest", "fixtures/v2s1-invalid-signatures.manifest.json")
assertTestFailed(t, out, err, "computing digest")
// Success
out, err = runSkopeo("manifest-digest", "fixtures/image.manifest.json")
assert.NoError(t, err)
assert.Equal(t, fixturesTestImageManifestDigest+"\n", out)
}

View File

@@ -1,18 +1,18 @@
package main
import (
"errors"
"fmt"
"io/ioutil"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/projectatomic/skopeo/signature"
"github.com/containers/image/signature"
"github.com/urfave/cli"
)
func standaloneSign(context *cli.Context) {
func standaloneSign(context *cli.Context) error {
outputFile := context.String("output")
if len(context.Args()) != 3 || outputFile == "" {
logrus.Fatal("Usage: skopeo standalone-sign manifest docker-reference key-fingerprint -o signature")
return errors.New("Usage: skopeo standalone-sign manifest docker-reference key-fingerprint -o signature")
}
manifestPath := context.Args()[0]
dockerReference := context.Args()[1]
@@ -20,38 +20,40 @@ func standaloneSign(context *cli.Context) {
manifest, err := ioutil.ReadFile(manifestPath)
if err != nil {
logrus.Fatalf("Error reading %s: %s", manifestPath, err.Error())
return fmt.Errorf("Error reading %s: %v", manifestPath, err)
}
mech, err := signature.NewGPGSigningMechanism()
if err != nil {
logrus.Fatalf("Error initializing GPG: %s", err.Error())
return fmt.Errorf("Error initializing GPG: %v", err)
}
signature, err := signature.SignDockerManifest(manifest, dockerReference, mech, fingerprint)
if err != nil {
logrus.Fatalf("Error creating signature: %s", err.Error())
return fmt.Errorf("Error creating signature: %v", err)
}
if err := ioutil.WriteFile(outputFile, signature, 0644); err != nil {
logrus.Fatalf("Error writing signature to %s: %s", outputFile, err.Error())
return fmt.Errorf("Error writing signature to %s: %v", outputFile, err)
}
return nil
}
var standaloneSignCmd = cli.Command{
Name: "standalone-sign",
Usage: "Create a signature using local files",
Action: standaloneSign,
Name: "standalone-sign",
Usage: "Create a signature using local files",
ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT",
Action: standaloneSign,
Flags: []cli.Flag{
cli.StringFlag{
Name: "output, o",
Usage: "output signature file name",
Usage: "output the signature to `SIGNATURE`",
},
},
}
func standaloneVerify(context *cli.Context) {
func standaloneVerify(context *cli.Context) error {
if len(context.Args()) != 4 {
logrus.Fatal("Usage: skopeo standalone-verify manifest docker-reference key-fingerprint signature")
return errors.New("Usage: skopeo standalone-verify manifest docker-reference key-fingerprint signature")
}
manifestPath := context.Args()[0]
expectedDockerReference := context.Args()[1]
@@ -60,27 +62,29 @@ func standaloneVerify(context *cli.Context) {
unverifiedManifest, err := ioutil.ReadFile(manifestPath)
if err != nil {
logrus.Fatalf("Error reading manifest from %s: %s", signaturePath, err.Error())
return fmt.Errorf("Error reading manifest from %s: %v", manifestPath, err)
}
unverifiedSignature, err := ioutil.ReadFile(signaturePath)
if err != nil {
logrus.Fatalf("Error reading signature from %s: %s", signaturePath, err.Error())
return fmt.Errorf("Error reading signature from %s: %v", signaturePath, err)
}
mech, err := signature.NewGPGSigningMechanism()
if err != nil {
logrus.Fatalf("Error initializing GPG: %s", err.Error())
return fmt.Errorf("Error initializing GPG: %v", err)
}
sig, err := signature.VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest, expectedDockerReference, mech, expectedFingerprint)
if err != nil {
logrus.Fatalf("Error verifying signature: %s", err.Error())
return fmt.Errorf("Error verifying signature: %v", err)
}
fmt.Printf("Signature verified, digest %s\n", sig.DockerManifestDigest)
fmt.Fprintf(context.App.Writer, "Signature verified, digest %s\n", sig.DockerManifestDigest)
return nil
}
var standaloneVerifyCmd = cli.Command{
Name: "standalone-verify",
Usage: "Verify a signature using local files",
Action: standaloneVerify,
Name: "standalone-verify",
Usage: "Verify a signature using local files",
ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT SIGNATURE",
Action: standaloneVerify,
}

126
cmd/skopeo/signing_test.go Normal file
View File

@@ -0,0 +1,126 @@
package main
import (
"io/ioutil"
"os"
"testing"
"github.com/containers/image/signature"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
// fixturesTestImageManifestDigest is the Docker manifest digest of "image.manifest.json"
fixturesTestImageManifestDigest = "sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"
// fixturesTestKeyFingerprint is the fingerprint of the private key.
fixturesTestKeyFingerprint = "1D8230F6CDB6A06716E414C1DB72F2188BB46CC8"
)
// Test that results of runSkopeo failed with nothing on stdout, and substring
// within the error message.
func assertTestFailed(t *testing.T, stdout string, err error, substring string) {
assert.Error(t, err)
assert.Empty(t, stdout)
assert.Contains(t, err.Error(), substring)
}
func TestStandaloneSign(t *testing.T) {
manifestPath := "fixtures/image.manifest.json"
dockerReference := "testing/manifest"
os.Setenv("GNUPGHOME", "fixtures")
defer os.Unsetenv("GNUPGHOME")
// Invalid command-line arguments
for _, args := range [][]string{
{},
{"a1", "a2"},
{"a1", "a2", "a3"},
{"a1", "a2", "a3", "a4"},
{"-o", "o", "a1", "a2"},
{"-o", "o", "a1", "a2", "a3", "a4"},
} {
out, err := runSkopeo(append([]string{"standalone-sign"}, args...)...)
assertTestFailed(t, out, err, "Usage")
}
// Error reading manifest
out, err := runSkopeo("standalone-sign", "-o", "/dev/null",
"/this/doesnt/exist", dockerReference, fixturesTestKeyFingerprint)
assertTestFailed(t, out, err, "/this/doesnt/exist")
// Invalid Docker reference
out, err = runSkopeo("standalone-sign", "-o", "/dev/null",
manifestPath, "" /* empty reference */, fixturesTestKeyFingerprint)
assertTestFailed(t, out, err, "empty signature content")
// Unknown key. (FIXME? The error is 'Error creating signature: End of file")
out, err = runSkopeo("standalone-sign", "-o", "/dev/null",
manifestPath, dockerReference, "UNKNOWN GPG FINGERPRINT")
assert.Error(t, err)
assert.Empty(t, out)
// Error writing output
out, err = runSkopeo("standalone-sign", "-o", "/dev/full",
manifestPath, dockerReference, fixturesTestKeyFingerprint)
assertTestFailed(t, out, err, "/dev/full")
// Success
sigOutput, err := ioutil.TempFile("", "sig")
require.NoError(t, err)
defer os.Remove(sigOutput.Name())
out, err = runSkopeo("standalone-sign", "-o", sigOutput.Name(),
manifestPath, dockerReference, fixturesTestKeyFingerprint)
assert.NoError(t, err)
assert.Empty(t, out)
sig, err := ioutil.ReadFile(sigOutput.Name())
require.NoError(t, err)
manifest, err := ioutil.ReadFile(manifestPath)
require.NoError(t, err)
mech, err := signature.NewGPGSigningMechanism()
require.NoError(t, err)
verified, err := signature.VerifyDockerManifestSignature(sig, manifest, dockerReference, mech, fixturesTestKeyFingerprint)
assert.NoError(t, err)
assert.Equal(t, dockerReference, verified.DockerReference)
assert.Equal(t, fixturesTestImageManifestDigest, verified.DockerManifestDigest)
}
func TestStandaloneVerify(t *testing.T) {
manifestPath := "fixtures/image.manifest.json"
signaturePath := "fixtures/image.signature"
dockerReference := "testing/manifest"
os.Setenv("GNUPGHOME", "fixtures")
defer os.Unsetenv("GNUPGHOME")
// Invalid command-line arguments
for _, args := range [][]string{
{},
{"a1", "a2", "a3"},
{"a1", "a2", "a3", "a4", "a5"},
} {
out, err := runSkopeo(append([]string{"standalone-verify"}, args...)...)
assertTestFailed(t, out, err, "Usage")
}
// Error reading manifest
out, err := runSkopeo("standalone-verify", "/this/doesnt/exist",
dockerReference, fixturesTestKeyFingerprint, signaturePath)
assertTestFailed(t, out, err, "/this/doesnt/exist")
// Error reading signature
out, err = runSkopeo("standalone-verify", manifestPath,
dockerReference, fixturesTestKeyFingerprint, "/this/doesnt/exist")
assertTestFailed(t, out, err, "/this/doesnt/exist")
// Error verifying signature
out, err = runSkopeo("standalone-verify", manifestPath,
dockerReference, fixturesTestKeyFingerprint, "fixtures/corrupt.signature")
assertTestFailed(t, out, err, "Error verifying signature")
// Success
out, err = runSkopeo("standalone-verify", manifestPath,
dockerReference, fixturesTestKeyFingerprint, signaturePath)
assert.NoError(t, err)
assert.Equal(t, "Signature verified, digest "+fixturesTestImageManifestDigest+"\n", out)
}

View File

@@ -1,74 +1,39 @@
package main
import (
"fmt"
"strings"
"github.com/codegangsta/cli"
"github.com/projectatomic/skopeo/directory"
"github.com/projectatomic/skopeo/docker"
"github.com/projectatomic/skopeo/openshift"
"github.com/projectatomic/skopeo/types"
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/urfave/cli"
)
const (
// atomicPrefix is the URL-like schema prefix used for Atomic registry image references.
atomicPrefix = "atomic:"
// dockerPrefix is the URL-like schema prefix used for Docker image references.
dockerPrefix = "docker://"
// directoryPrefix is the URL-like schema prefix used for local directories (for debugging)
directoryPrefix = "dir:"
)
// contextFromGlobalOptions returns a types.SystemContext depending on c.
func contextFromGlobalOptions(c *cli.Context) *types.SystemContext {
tlsVerify := c.GlobalBoolT("tls-verify")
return &types.SystemContext{
RegistriesDirPath: c.GlobalString("registries.d"),
DockerCertPath: c.GlobalString("cert-path"),
DockerInsecureSkipTLSVerify: !tlsVerify,
}
}
// ParseImage converts image URL-like string to an initialized handler for that image.
// The caller must call .Close() on the returned Image.
func parseImage(c *cli.Context) (types.Image, error) {
var (
imgName = c.Args().First()
certPath = c.GlobalString("cert-path")
tlsVerify = c.GlobalBool("tls-verify")
)
switch {
case strings.HasPrefix(imgName, dockerPrefix):
return docker.NewDockerImage(strings.TrimPrefix(imgName, dockerPrefix), certPath, tlsVerify)
//case strings.HasPrefix(img, appcPrefix):
//
case strings.HasPrefix(imgName, directoryPrefix):
src := directory.NewDirImageSource(strings.TrimPrefix(imgName, directoryPrefix))
return docker.GenericImageFromSource(src), nil
imgName := c.Args().First()
ref, err := transports.ParseImageName(imgName)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("no valid prefix provided")
return ref.NewImage(contextFromGlobalOptions(c))
}
// parseImageSource converts image URL-like string to an ImageSource.
func parseImageSource(c *cli.Context, name string) (types.ImageSource, error) {
var (
certPath = c.GlobalString("cert-path")
tlsVerify = c.GlobalBool("tls-verify") // FIXME!! defaults to false?
)
switch {
case strings.HasPrefix(name, dockerPrefix):
return docker.NewDockerImageSource(strings.TrimPrefix(name, dockerPrefix), certPath, tlsVerify)
case strings.HasPrefix(name, atomicPrefix):
return openshift.NewOpenshiftImageSource(strings.TrimPrefix(name, atomicPrefix), certPath, tlsVerify)
case strings.HasPrefix(name, directoryPrefix):
return directory.NewDirImageSource(strings.TrimPrefix(name, directoryPrefix)), nil
// requestedManifestMIMETypes is as in types.ImageReference.NewImageSource.
// The caller must call .Close() on the returned ImageSource.
func parseImageSource(c *cli.Context, name string, requestedManifestMIMETypes []string) (types.ImageSource, error) {
ref, err := transports.ParseImageName(name)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("Unrecognized image reference %s", name)
}
// parseImageDestination converts image URL-like string to an ImageDestination.
func parseImageDestination(c *cli.Context, name string) (types.ImageDestination, error) {
var (
certPath = c.GlobalString("cert-path")
tlsVerify = c.GlobalBool("tls-verify") // FIXME!! defaults to false?
)
switch {
case strings.HasPrefix(name, dockerPrefix):
return docker.NewDockerImageDestination(strings.TrimPrefix(name, dockerPrefix), certPath, tlsVerify)
case strings.HasPrefix(name, atomicPrefix):
return openshift.NewOpenshiftImageDestination(strings.TrimPrefix(name, atomicPrefix), certPath, tlsVerify)
case strings.HasPrefix(name, directoryPrefix):
return directory.NewDirImageDestination(strings.TrimPrefix(name, directoryPrefix)), nil
}
return nil, fmt.Errorf("Unrecognized image reference %s", name)
return ref.NewImageSource(contextFromGlobalOptions(c), requestedManifestMIMETypes)
}

7
default-policy.json Normal file
View File

@@ -0,0 +1,7 @@
{
"default": [
{
"type": "insecureAcceptAnything"
}
]
}

View File

@@ -1,113 +0,0 @@
package directory
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/projectatomic/skopeo/types"
)
// manifestPath returns a path for the manifest within a directory using our conventions.
func manifestPath(dir string) string {
return filepath.Join(dir, "manifest.json")
}
// manifestPath returns a path for a layer tarball within a directory using our conventions.
func layerPath(dir string, digest string) string {
// FIXME: Should we keep the digest identification?
return filepath.Join(dir, strings.TrimPrefix(digest, "sha256:")+".tar")
}
// manifestPath returns a path for a signature within a directory using our conventions.
func signaturePath(dir string, index int) string {
return filepath.Join(dir, fmt.Sprintf("signature-%d", index+1))
}
type dirImageDestination struct {
dir string
}
// NewDirImageDestination returns an ImageDestination for writing to an existing directory.
func NewDirImageDestination(dir string) types.ImageDestination {
return &dirImageDestination{dir}
}
func (d *dirImageDestination) CanonicalDockerReference() (string, error) {
return "", fmt.Errorf("Can not determine canonical Docker reference for a local directory")
}
func (d *dirImageDestination) PutManifest(manifest []byte) error {
return ioutil.WriteFile(manifestPath(d.dir), manifest, 0644)
}
func (d *dirImageDestination) PutLayer(digest string, stream io.Reader) error {
layerFile, err := os.Create(layerPath(d.dir, digest))
if err != nil {
return err
}
defer layerFile.Close()
if _, err := io.Copy(layerFile, stream); err != nil {
return err
}
if err := layerFile.Sync(); err != nil {
return err
}
return nil
}
func (d *dirImageDestination) PutSignatures(signatures [][]byte) error {
for i, sig := range signatures {
if err := ioutil.WriteFile(signaturePath(d.dir, i), sig, 0644); err != nil {
return err
}
}
return nil
}
type dirImageSource struct {
dir string
}
// NewDirImageSource returns an ImageSource reading from an existing directory.
func NewDirImageSource(dir string) types.ImageSource {
return &dirImageSource{dir}
}
// IntendedDockerReference returns the full, unambiguous, Docker reference for this image, _as specified by the user_
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
// May be "" if unknown.
func (s *dirImageSource) IntendedDockerReference() string {
return ""
}
// it's up to the caller to determine the MIME type of the returned manifest's bytes
func (s *dirImageSource) GetManifest(_ []string) ([]byte, string, error) {
m, err := ioutil.ReadFile(manifestPath(s.dir))
if err != nil {
return nil, "", err
}
return m, "", err
}
func (s *dirImageSource) GetLayer(digest string) (io.ReadCloser, error) {
return os.Open(layerPath(s.dir, digest))
}
func (s *dirImageSource) GetSignatures() ([][]byte, error) {
signatures := [][]byte{}
for i := 0; ; i++ {
signature, err := ioutil.ReadFile(signaturePath(s.dir, i))
if err != nil {
if os.IsNotExist(err) {
break
}
return nil, err
}
signatures = append(signatures, signature)
}
return signatures, nil
}

24
doc.go
View File

@@ -1,24 +0,0 @@
// Package skopeo provides libraries and commands to interact with containers images.
//
// package main
//
// import (
// "fmt"
//
// "github.com/projectatomic/skopeo/docker"
// )
//
// func main() {
// img, err := docker.NewDockerImage("fedora", "", false)
// if err != nil {
// panic(err)
// }
// b, err := img.Manifest()
// if err != nil {
// panic(err)
// }
// fmt.Printf("%s", string(b))
// }
//
// TODO(runcom)
package skopeo

View File

@@ -1,110 +0,0 @@
package docker
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"github.com/Sirupsen/logrus"
"github.com/projectatomic/skopeo/docker/utils"
"github.com/projectatomic/skopeo/reference"
"github.com/projectatomic/skopeo/types"
)
type dockerImageDestination struct {
ref reference.Named
tag string
c *dockerClient
}
// NewDockerImageDestination creates a new ImageDestination for the specified image and connection specification.
func NewDockerImageDestination(img, certPath string, tlsVerify bool) (types.ImageDestination, error) {
ref, tag, err := parseDockerImageName(img)
if err != nil {
return nil, err
}
c, err := newDockerClient(ref.Hostname(), certPath, tlsVerify)
if err != nil {
return nil, err
}
return &dockerImageDestination{
ref: ref,
tag: tag,
c: c,
}, nil
}
func (d *dockerImageDestination) CanonicalDockerReference() (string, error) {
return fmt.Sprintf("%s:%s", d.ref.Name(), d.tag), nil
}
func (d *dockerImageDestination) PutManifest(manifest []byte) error {
// FIXME: This only allows upload by digest, not creating a tag. See the
// corresponding comment in NewOpenshiftImageDestination.
digest, err := utils.ManifestDigest(manifest)
if err != nil {
return err
}
url := fmt.Sprintf(manifestURL, d.ref.RemoteName(), digest)
headers := map[string][]string{}
mimeType := utils.GuessManifestMIMEType(manifest)
if mimeType != "" {
headers["Content-Type"] = []string{mimeType}
}
res, err := d.c.makeRequest("PUT", url, headers, bytes.NewReader(manifest))
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
body, err := ioutil.ReadAll(res.Body)
if err == nil {
logrus.Debugf("Error body %s", string(body))
}
logrus.Debugf("Error uploading manifest, status %d, %#v", res.StatusCode, res)
return fmt.Errorf("Error uploading manifest to %s, status %d", url, res.StatusCode)
}
return nil
}
func (d *dockerImageDestination) PutLayer(digest string, stream io.Reader) error {
checkURL := fmt.Sprintf(blobsURL, d.ref.RemoteName(), digest)
logrus.Debugf("Checking %s", checkURL)
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == http.StatusOK && res.Header.Get("Docker-Content-Digest") == digest {
logrus.Debugf("... already exists, not uploading")
return nil
}
logrus.Debugf("... failed, status %d", res.StatusCode)
// FIXME? Chunked upload, progress reporting, etc.
uploadURL := fmt.Sprintf(blobUploadURL, d.ref.RemoteName(), digest)
logrus.Debugf("Uploading %s", uploadURL)
// FIXME: Set Content-Length?
res, err = d.c.makeRequest("POST", uploadURL, map[string][]string{"Content-Type": {"application/octet-stream"}}, stream)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
logrus.Debugf("Error uploading, status %d", res.StatusCode)
return fmt.Errorf("Error uploading to %s, status %d", uploadURL, res.StatusCode)
}
return nil
}
func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error {
if len(signatures) != 0 {
return fmt.Errorf("Pushing signatures to a Docker Registry is not supported")
}
return nil
}

View File

@@ -1,96 +0,0 @@
package docker
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"github.com/Sirupsen/logrus"
"github.com/projectatomic/skopeo/reference"
"github.com/projectatomic/skopeo/types"
)
type errFetchManifest struct {
statusCode int
body []byte
}
func (e errFetchManifest) Error() string {
return fmt.Sprintf("error fetching manifest: status code: %d, body: %s", e.statusCode, string(e.body))
}
type dockerImageSource struct {
ref reference.Named
tag string
c *dockerClient
}
// newDockerImageSource is the same as NewDockerImageSource, only it returns the more specific *dockerImageSource type.
func newDockerImageSource(img, certPath string, tlsVerify bool) (*dockerImageSource, error) {
ref, tag, err := parseDockerImageName(img)
if err != nil {
return nil, err
}
c, err := newDockerClient(ref.Hostname(), certPath, tlsVerify)
if err != nil {
return nil, err
}
return &dockerImageSource{
ref: ref,
tag: tag,
c: c,
}, nil
}
// NewDockerImageSource creates a new ImageSource for the specified image and connection specification.
func NewDockerImageSource(img, certPath string, tlsVerify bool) (types.ImageSource, error) {
return newDockerImageSource(img, certPath, tlsVerify)
}
// IntendedDockerReference returns the full, unambiguous, Docker reference for this image, _as specified by the user_
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
// May be "" if unknown.
func (s *dockerImageSource) IntendedDockerReference() string {
return fmt.Sprintf("%s:%s", s.ref.Name(), s.tag)
}
func (s *dockerImageSource) GetManifest(mimetypes []string) ([]byte, string, error) {
url := fmt.Sprintf(manifestURL, s.ref.RemoteName(), s.tag)
// TODO(runcom) set manifest version header! schema1 for now - then schema2 etc etc and v1
// TODO(runcom) NO, switch on the resulter manifest like Docker is doing
headers := make(map[string][]string)
headers["Accept"] = mimetypes
res, err := s.c.makeRequest("GET", url, headers, nil)
if err != nil {
return nil, "", err
}
defer res.Body.Close()
manblob, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, "", err
}
if res.StatusCode != http.StatusOK {
return nil, "", errFetchManifest{res.StatusCode, manblob}
}
// We might validate manblob against the Docker-Content-Digest header here to protect against transport errors.
return manblob, res.Header.Get("Content-Type"), nil
}
func (s *dockerImageSource) GetLayer(digest string) (io.ReadCloser, error) {
url := fmt.Sprintf(blobsURL, s.ref.RemoteName(), digest)
logrus.Infof("Downloading %s", url)
res, err := s.c.makeRequest("GET", url, nil, nil)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
// print url also
return nil, fmt.Errorf("Invalid status code returned when fetching blob %d", res.StatusCode)
}
return res.Body, nil
}
func (s *dockerImageSource) GetSignatures() ([][]byte, error) {
return [][]byte{}, nil
}

View File

@@ -1,20 +0,0 @@
package docker
import "github.com/projectatomic/skopeo/reference"
// parseDockerImageName converts a string into a reference and tag value.
func parseDockerImageName(img string) (reference.Named, string, error) {
ref, err := reference.ParseNamed(img)
if err != nil {
return nil, "", err
}
ref = reference.WithDefaultTag(ref)
var tag string
switch x := ref.(type) {
case reference.Canonical:
tag = x.Digest().String()
case reference.NamedTagged:
tag = x.Tag()
}
return ref, tag, nil
}

View File

@@ -1,261 +0,0 @@
package docker
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"regexp"
"strings"
"time"
"github.com/projectatomic/skopeo/directory"
"github.com/projectatomic/skopeo/docker/utils"
"github.com/projectatomic/skopeo/types"
)
var (
validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
)
// genericImage is a general set of utilities for working with container images,
// whatever is their underlying location (i.e. dockerImageSource-independent).
type genericImage struct {
src types.ImageSource
cachedManifest []byte // Private cache for Manifest(); nil if not yet known.
cachedSignatures [][]byte // Private cache for Signatures(); nil if not yet known.
}
// GenericImageFromSource returns a types.Image implementation for source.
// NOTE: This is currently an internal testing helper, do not rely on this as
// a stable API. There might be an ImageFromSource eventually, but it would not be
// in the skopeo/docker package.
func GenericImageFromSource(src types.ImageSource) types.Image {
return &genericImage{src: src}
}
// IntendedDockerReference returns the full, unambiguous, Docker reference for this image, _as specified by the user_
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
// May be "" if unknown.
func (i *genericImage) IntendedDockerReference() string {
return i.src.IntendedDockerReference()
}
// Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need.
func (i *genericImage) Manifest() ([]byte, error) {
if i.cachedManifest == nil {
m, _, err := i.src.GetManifest([]string{utils.DockerV2Schema1MIMEType})
if err != nil {
return nil, err
}
i.cachedManifest = m
}
return i.cachedManifest, nil
}
// Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
func (i *genericImage) Signatures() ([][]byte, error) {
if i.cachedSignatures == nil {
sigs, err := i.src.GetSignatures()
if err != nil {
return nil, err
}
i.cachedSignatures = sigs
}
return i.cachedSignatures, nil
}
func (i *genericImage) Inspect() (*types.ImageInspectInfo, error) {
// TODO(runcom): unused version param for now, default to docker v2-1
m, err := i.getSchema1Manifest()
if err != nil {
return nil, err
}
ms1, ok := m.(*manifestSchema1)
if !ok {
return nil, fmt.Errorf("error retrivieng manifest schema1")
}
v1 := &v1Image{}
if err := json.Unmarshal([]byte(ms1.History[0].V1Compatibility), v1); err != nil {
return nil, err
}
return &types.ImageInspectInfo{
Tag: ms1.Tag,
DockerVersion: v1.DockerVersion,
Created: v1.Created,
Labels: v1.Config.Labels,
Architecture: v1.Architecture,
Os: v1.OS,
Layers: ms1.GetLayers(),
}, nil
}
type config struct {
Labels map[string]string
}
type v1Image struct {
// Config is the configuration of the container received from the client
Config *config `json:"config,omitempty"`
// DockerVersion specifies version on which image is built
DockerVersion string `json:"docker_version,omitempty"`
// Created timestamp when image was created
Created time.Time `json:"created"`
// Architecture is the hardware that the image is build and runs on
Architecture string `json:"architecture,omitempty"`
// OS is the operating system used to build and run the image
OS string `json:"os,omitempty"`
}
// TODO(runcom)
func (i *genericImage) DockerTar() ([]byte, error) {
return nil, nil
}
// will support v1 one day...
type manifest interface {
String() string
GetLayers() []string
}
type manifestSchema1 struct {
Name string
Tag string
FSLayers []struct {
BlobSum string `json:"blobSum"`
} `json:"fsLayers"`
History []struct {
V1Compatibility string `json:"v1Compatibility"`
} `json:"history"`
// TODO(runcom) verify the downloaded manifest
//Signature []byte `json:"signature"`
}
func (m *manifestSchema1) GetLayers() []string {
layers := make([]string, len(m.FSLayers))
for i, layer := range m.FSLayers {
layers[i] = layer.BlobSum
}
return layers
}
func (m *manifestSchema1) String() string {
return fmt.Sprintf("%s-%s", sanitize(m.Name), sanitize(m.Tag))
}
func sanitize(s string) string {
return strings.Replace(s, "/", "-", -1)
}
func (i *genericImage) getSchema1Manifest() (manifest, error) {
manblob, err := i.Manifest()
if err != nil {
return nil, err
}
mschema1 := &manifestSchema1{}
if err := json.Unmarshal(manblob, mschema1); err != nil {
return nil, err
}
if err := fixManifestLayers(mschema1); err != nil {
return nil, err
}
// TODO(runcom): verify manifest schema 1, 2 etc
//if len(m.FSLayers) != len(m.History) {
//return nil, fmt.Errorf("length of history not equal to number of layers for %q", ref.String())
//}
//if len(m.FSLayers) == 0 {
//return nil, fmt.Errorf("no FSLayers in manifest for %q", ref.String())
//}
return mschema1, nil
}
func (i *genericImage) Layers(layers ...string) error {
m, err := i.getSchema1Manifest()
if err != nil {
return err
}
tmpDir, err := ioutil.TempDir(".", "layers-"+m.String()+"-")
if err != nil {
return err
}
dest := directory.NewDirImageDestination(tmpDir)
data, err := json.Marshal(m)
if err != nil {
return err
}
if err := dest.PutManifest(data); err != nil {
return err
}
if len(layers) == 0 {
layers = m.GetLayers()
}
for _, l := range layers {
if !strings.HasPrefix(l, "sha256:") {
l = "sha256:" + l
}
if err := i.getLayer(dest, l); err != nil {
return err
}
}
return nil
}
func (i *genericImage) getLayer(dest types.ImageDestination, digest string) error {
stream, err := i.src.GetLayer(digest)
if err != nil {
return err
}
defer stream.Close()
return dest.PutLayer(digest, stream)
}
func fixManifestLayers(manifest *manifestSchema1) error {
type imageV1 struct {
ID string
Parent string
}
imgs := make([]*imageV1, len(manifest.FSLayers))
for i := range manifest.FSLayers {
img := &imageV1{}
if err := json.Unmarshal([]byte(manifest.History[i].V1Compatibility), img); err != nil {
return err
}
imgs[i] = img
if err := validateV1ID(img.ID); err != nil {
return err
}
}
if imgs[len(imgs)-1].Parent != "" {
return errors.New("Invalid parent ID in the base layer of the image.")
}
// check general duplicates to error instead of a deadlock
idmap := make(map[string]struct{})
var lastID string
for _, img := range imgs {
// skip IDs that appear after each other, we handle those later
if _, exists := idmap[img.ID]; img.ID != lastID && exists {
return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID)
}
lastID = img.ID
idmap[lastID] = struct{}{}
}
// backwards loop so that we keep the remaining indexes after removing items
for i := len(imgs) - 2; i >= 0; i-- {
if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue
manifest.FSLayers = append(manifest.FSLayers[:i], manifest.FSLayers[i+1:]...)
manifest.History = append(manifest.History[:i], manifest.History[i+1:]...)
} else if imgs[i].Parent != imgs[i+1].ID {
return fmt.Errorf("Invalid parent ID. Expected %v, got %v.", imgs[i+1].ID, imgs[i].Parent)
}
}
return nil
}
func validateV1ID(id string) error {
if ok := validHex.MatchString(id); !ok {
return fmt.Errorf("image ID %q is invalid", id)
}
return nil
}

View File

@@ -1,5 +0,0 @@
{
"schemaVersion": 99999,
"name": "mitr/noversion-nonsense",
"tag": "latest"
}

View File

@@ -1,56 +0,0 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 2094,
"digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783",
"platform": {
"architecture": "ppc64le",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 1922,
"digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd",
"platform": {
"architecture": "amd64",
"os": "linux",
"features": [
"sse"
]
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 2084,
"digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2",
"platform": {
"architecture": "s390x",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 2084,
"digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40",
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "armv7"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 2090,
"digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a",
"platform": {
"architecture": "arm64",
"os": "linux",
"variant": "armv8"
}
}
]
}

View File

@@ -1,44 +0,0 @@
{
"schemaVersion": 1,
"name": "mitr/buxybox",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
{
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
},
{
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
},
{
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
}
],
"history": [
{
"v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
},
{
"v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
},
{
"v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
}
],
"signatures": [
{
"header": {
"jwk": {
"crv": "P-256",
"kid": "OZ45:U3IG:TDOI:PMBD:NGP2:LDIW:II2U:PSBI:MMCZ:YZUP:TUUO:XPZT",
"kty": "EC",
"x": "ReC5c0J9tgXSdUL4_xzEt5RsD8kFt2wWSgJcpAcOQx8",
"y": "3sBGEqQ3ZMeqPKwQBAadN2toOUEASha18xa0WwsDF-M"
},
"alg": "ES256"
},
"signature": "dV1paJ3Ck1Ph4FcEhg_frjqxdlGdI6-ywRamk6CvMOcaOEUdCWCpCPQeBQpD2N6tGjkoG1BbstkFNflllfenCw",
"protected": "eyJmb3JtYXRMZW5ndGgiOjU0NzgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNC0xOFQyMDo1NDo0MloifQ"
}
]
}

View File

@@ -1,26 +0,0 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32654,
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 16724,
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 73109,
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
}
]
}

View File

@@ -1,10 +0,0 @@
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
},
"layers": [
]
}

View File

@@ -1,8 +0,0 @@
package utils
const (
// TestV2S2ManifestDigest is the Docker manifest digest of "v2s2.manifest.json"
TestV2S2ManifestDigest = "sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"
// TestV2S1ManifestDigest is the Docker manifest digest of "v2s1.manifest.json"
TestV2S1ManifestDigest = "sha256:077594da70fc17ec2c93cfa4e6ed1fcc26992851fb2c71861338aaf4aa9e41b1"
)

View File

@@ -1,66 +0,0 @@
package utils
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"github.com/docker/libtrust"
)
// FIXME: Should we just use docker/distribution and docker/docker implementations directly?
const (
// DockerV2Schema1MIMEType MIME type represents Docker manifest schema 1
DockerV2Schema1MIMEType = "application/vnd.docker.distribution.manifest.v1+json"
// DockerV2Schema2MIMEType MIME type represents Docker manifest schema 2
DockerV2Schema2MIMEType = "application/vnd.docker.distribution.manifest.v2+json"
// DockerV2ListMIMEType MIME type represents Docker manifest schema 2 list
DockerV2ListMIMEType = "application/vnd.docker.distribution.manifest.list.v2+json"
)
// GuessManifestMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized.
// FIXME? We should, in general, prefer out-of-band MIME type instead of blindly parsing the manifest,
// but we may not have such metadata available (e.g. when the manifest is a local file).
func GuessManifestMIMEType(manifest []byte) string {
// A subset of manifest fields; the rest is silently ignored by json.Unmarshal.
// Also docker/distribution/manifest.Versioned.
meta := struct {
MediaType string `json:"mediaType"`
SchemaVersion int `json:"schemaVersion"`
}{}
if err := json.Unmarshal(manifest, &meta); err != nil {
return ""
}
switch meta.MediaType {
case DockerV2Schema2MIMEType, DockerV2ListMIMEType: // A recognized type.
return meta.MediaType
}
switch meta.SchemaVersion {
case 1:
return DockerV2Schema1MIMEType
case 2: // Really should not happen, meta.MediaType should have been set. But given the data, this is our best guess.
return DockerV2Schema2MIMEType
}
return ""
}
// ManifestDigest returns the a digest of a docker manifest, with any necessary implied transformations like stripping v1s1 signatures.
func ManifestDigest(manifest []byte) (string, error) {
if GuessManifestMIMEType(manifest) == DockerV2Schema1MIMEType {
sig, err := libtrust.ParsePrettySignature(manifest, "signatures")
if err != nil {
return "", err
}
manifest, err = sig.Payload()
if err != nil {
// Coverage: This should never happen, libtrust's Payload() can fail only if joseBase64UrlDecode() fails, on a string
// that libtrust itself has josebase64UrlEncode()d
return "", err
}
}
hash := sha256.Sum256(manifest)
return "sha256:" + hex.EncodeToString(hash[:]), nil
}

View File

@@ -1,58 +0,0 @@
package utils
import (
"io/ioutil"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGuessManifestMIMEType(t *testing.T) {
cases := []struct {
path string
mimeType string
}{
{"v2s2.manifest.json", DockerV2Schema2MIMEType},
{"v2list.manifest.json", DockerV2ListMIMEType},
{"v2s1.manifest.json", DockerV2Schema1MIMEType},
{"v2s1-invalid-signatures.manifest.json", DockerV2Schema1MIMEType},
{"v2s2nomime.manifest.json", DockerV2Schema2MIMEType}, // It is unclear whether this one is legal, but we should guess v2s2 if anything at all.
{"unknown-version.manifest.json", ""},
{"non-json.manifest.json", ""}, // Not a manifest (nor JSON) at all
}
for _, c := range cases {
manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path))
require.NoError(t, err)
mimeType := GuessManifestMIMEType(manifest)
assert.Equal(t, c.mimeType, mimeType)
}
}
func TestManifestDigest(t *testing.T) {
cases := []struct {
path string
digest string
}{
{"v2s2.manifest.json", TestV2S2ManifestDigest},
{"v2s1.manifest.json", TestV2S1ManifestDigest},
}
for _, c := range cases {
manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path))
require.NoError(t, err)
digest, err := ManifestDigest(manifest)
require.NoError(t, err)
assert.Equal(t, c.digest, digest)
}
manifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json")
require.NoError(t, err)
digest, err := ManifestDigest(manifest)
assert.Error(t, err)
digest, err = ManifestDigest([]byte{})
require.NoError(t, err)
assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", digest)
}

230
docs/skopeo.1.md Normal file
View File

@@ -0,0 +1,230 @@
% SKOPEO(1) Skopeo Man Pages
% Jhon Honce
% August 2016
# NAME
skopeo -- Various operations with container images images and container image registries
# SYNOPSIS
**skopeo** [_global options_] _command_ [_command options_]
# DESCRIPTION
`skopeo` is a command line utility providing various operations with container images and container image registries. For example, it is able to inspect a repository on a Docker registry and fetch image. It fetches the repository's manifest and it is able to show you a `docker inspect`-like json output about a whole repository or a tag. This tool, in contrast to `docker inspect`, helps you gather useful information about a repository or a tag without requiring you to run `docker pull` - e.g. - which tags are available for the given repository? which labels the image has?
It also allows you to copy container images between various registries, possibly converting them as necessary, and to sign and verify images.
## IMAGE NAMES
Most commands refer to container images, using a _transport_`:`_details_ format. The following formats are supported:
**atomic:**_namespace_**/**_stream_**:**_tag_
An image in the current project of the current default Atomic
Registry. The current project and Atomic Registry instance are by
default read from `$HOME/.kube/config`, which is set e.g. using
`(oc login)`.
**dir:**_path_
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)`.
**oci:**_path_**:**_tag_
An image _tag_ in a directory compliant with "Open Container Image
Layout Specification" at _path_.
# OPTIONS
**--debug** enable debug output
**--username** _username_ for accessing the registry
**--password** _password_ for accessing the registry
**--cert-path** _path_ Use certificates at _path_ (cert.pem, key.pem) to connect to the registry
**--policy** _path-to-policy_ Path to a policy.json file to use for verifying signatures and deciding whether an image is trusted, overriding the default trust policy file.
**--registries.d** _dir_ use registry configuration files in _dir_ (e.g. for docker signature storage), overriding the default path.
**--tls-verify** _bool-value_ Require HTTPS and verify certificates when talking to docker registries (defaults to true)
**--help**|**-h** Show help
**--version**|**-v** print the version number
# COMMANDS
## skopeo copy
**skopeo copy** [**--sign-by=**_key-ID_] _source-image destination-image_
Copy an image (manifest, filesystem layers, signatures) from one location to another.
Uses the system's trust policy to validate images, rejects images not trusted by the policy.
_source-image_ use the "image name" format described above
_destination-image_ use the "image name" format described above
**--remove-signatures** do not copy signatures, if any, from _source-image_. Necessary when copying a signed image to a destination which does not support signatures.
**--sign-by=**_key-id_ add a signature using that key ID for an image name corresponding to _destination-image_
Existing signatures, if any, are preserved as well.
## skopeo delete
**skopeo delete** _image-name_
Mark _image-name_ for deletion. To release the allocated disk space, you need to execute the docker registry garabage collector. E.g.,
```sh
$ docker exec -it registry bin/registry garbage-collect /etc/docker/registry/config.yml
```
Additionally, the registry must allow deletions by setting `REGISTRY_STORAGE_DELETE_ENABLED=true` for the registry daemon.
## skopeo inspect
**skopeo inspect** [**--raw**] _image-name_
Return low-level information about _image-name_ in a registry
**--raw** output raw manifest, default is to format in JSON
_image-name_ name of image to retrieve information about
## skopeo layers
**skopeo layers** _image-name_
Get image layers of _image-name_
_image-name_ name of the image to retrieve layers
## skopeo manifest-digest
**skopeo manifest-digest** _manifest-file_
Compute a manifest digest of _manifest-file_ and write it to standard output.
## skopeo standalone-sign
**skopeo standalone-sign** _manifest docker-reference key-fingerprint_ **--output**|**-o** _signature_
This is primarily a debugging tool, or useful for special cases,
and usually should not be a part of your normal operational workflow; use `skopeo copy --sign-by` instead to publish and sign an image in one step.
_manifest_ Path to a file containing the image manifest
_docker-reference_ A docker reference to identify the image with
_key-fingerprint_ Key identity to use for signing
**--output**|**-o** output file
## skopeo standalone-verify
**skopeo standalone-verify** _manifest docker-reference key-fingerprint signature_
Verify a signature using local files, digest will be printed on success.
_manifest_ Path to a file containing the image manifest
_docker-reference_ A docker reference expected to identify the image in the signature
_key-fingerprint_ Expected identity of the signing key
_signature_ Path to signature file
**Note:** If you do use this, make sure that the image can not be changed at the source location between the times of its verification and use.
## skopeo help
show help for `skopeo`
# FILES
**/etc/containers/policy.json**
Default trust policy file, if **--policy** is not specified.
The policy format is documented in https://github.com/containers/image/blob/master/docs/policy.json.md .
**/etc/containers/registries.d**
Default directory containing registry configuration, if **--registries.d** is not specified.
The contents of this directory are documented in https://github.com/containers/image/blob/master/docs/registries.d.md .
# EXAMPLES
## skopeo copy
To copy the layers of the docker.io busybox image to a local directory:
```sh
$ mkdir -p /var/lib/images/busybox
$ skopeo copy docker://busybox:latest dir:/var/lib/images/busybox
$ ls /var/lib/images/busybox/*
/tmp/busybox/2b8fd9751c4c0f5dd266fcae00707e67a2545ef34f9a29354585f93dac906749.tar
/tmp/busybox/manifest.json
/tmp/busybox/8ddc19f16526912237dd8af81971d5e4dd0587907234be2b83e249518d5b673f.tar
```
To copy and sign an image:
```sh
$ skopeo copy --sign-by dev@example.com atomic:example/busybox:streaming atomic:example/busybox:gold
```
## skopeo delete
Mark image example/pause for deletion from the registry.example.com registry:
```sh
$ skopeo delete --force docker://registry.example.com/example/pause:latest
```
See above for additional details on using the command **delete**.
## skopeo inspect
To review information for the image fedora from the docker.io registry:
```sh
$ skopeo inspect docker://docker.io/fedora
{
"Name": "docker.io/library/fedora",
"Digest": "sha256:a97914edb6ba15deb5c5acf87bd6bd5b6b0408c96f48a5cbd450b5b04509bb7d",
"RepoTags": [
"20",
"21",
"22",
"23",
"24",
"heisenbug",
"latest",
"rawhide"
],
"Created": "2016-06-20T19:33:43.220526898Z",
"DockerVersion": "1.10.3",
"Labels": {},
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:7c91a140e7a1025c3bc3aace4c80c0d9933ac4ee24b8630a6b0b5d8b9ce6b9d4"
]
}
```
## skopeo layers
Another method to retrieve the layers for the busybox image from the docker.io registry:
```sh
$ skopeo layers docker://busybox
$ ls layers-500650331/
8ddc19f16526912237dd8af81971d5e4dd0587907234be2b83e249518d5b673f.tar
manifest.json
a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.tar
```
## skopeo manifest-digest
```sh
$ skopeo manifest-digest manifest.json
sha256:a59906e33509d14c036c8678d687bd4eec81ed7c4b8ce907b888c607f6a1e0e6
```
## skopeo standalone-sign
```sh
$ skopeo standalone-sign busybox-manifest.json registry.example.com/example/busybox 1D8230F6CDB6A06716E414C1DB72F2188BB46CC8 --output busybox.signature
$
```
See `skopeo copy` above for the preferred method of signing images.
## skopeo standalone-verify
```sh
$ skopeo standalone-verify busybox-manifest.json registry.example.com/example/busybox 1D8230F6CDB6A06716E414C1DB72F2188BB46CC8 busybox.signature
Signature verified, digest sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55
```
# AUTHORS
Antonio Murdaca <runcom@redhat.com>, Miloslav Trmac <mitr@redhat.com>, Jhon Honce <jhonce@redhat.com>

View File

@@ -36,7 +36,7 @@ clone() {
case "$vcs" in
git)
git clone --quiet --no-checkout "$url" "$target"
( cd "$target" && git checkout --quiet "$rev" && git reset --quiet --hard "$rev" )
( cd "$target" && git checkout --quiet "$rev" && git reset --quiet --hard "$rev" -- )
;;
hg)
hg clone --quiet --updaterev "$rev" "$url" "$target"

View File

@@ -0,0 +1,14 @@
#! /bin/bash
: ${PROG:=$(basename ${BASH_SOURCE})}
_cli_bash_autocomplete() {
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _cli_bash_autocomplete $PROG

View File

@@ -8,8 +8,8 @@ bundle_test_integration() {
# subshell so that we can export PATH without breaking other things
(
make binary
make install-binary
make binary-local
make install
export GO15VENDOREXPERIMENT=1
bundle_test_integration
) 2>&1

View File

@@ -5,16 +5,27 @@ cd "$(dirname "$BASH_SOURCE")/.."
rm -rf vendor/
source 'hack/.vendor-helpers.sh'
clone git github.com/codegangsta/cli v1.2.0
clone git github.com/urfave/cli v1.17.0
clone git github.com/containers/image master
clone git gopkg.in/cheggaaa/pb.v1 ad4efe000aa550bb54918c06ebbadc0ff17687b9 https://github.com/cheggaaa/pb
clone git github.com/Sirupsen/logrus v0.10.0
clone git github.com/go-check/check v1
clone git github.com/stretchr/testify v1.1.3
clone git github.com/davecgh/go-spew master
clone git github.com/pmezard/go-difflib master
clone git github.com/docker/docker 0f5c9d301b9b1cca66b3ea0f9dec3b5317d3686d
# docker deps from https://github.com/docker/docker/blob/v1.11.2/hack/vendor.sh
clone git github.com/docker/docker v1.11.2
clone git github.com/docker/engine-api v0.3.3
clone git github.com/docker/go-connections v0.2.0
clone git github.com/vbatts/tar-split v0.9.11
clone git github.com/gorilla/context 14f550f51a
clone git github.com/docker/go-units 651fc226e7441360384da338d0fd37f2440ffbe3
clone git golang.org/x/net master https://github.com/golang/net.git
# end docker deps
clone git github.com/docker/distribution master
clone git github.com/docker/libtrust master
clone git github.com/opencontainers/runc master
clone git github.com/opencontainers/image-spec master
clone git github.com/mtrmac/gpgme master
# openshift/origin' k8s dependencies as of OpenShift v1.1.5
clone git github.com/golang/glog 44145f04b68cf362d9c4df2182967c2275eaefed

View File

@@ -3,10 +3,10 @@ package main
import (
"fmt"
"os/exec"
"strings"
"testing"
"github.com/go-check/check"
"github.com/projectatomic/skopeo/version"
)
const (
@@ -15,8 +15,6 @@ const (
privateRegistryURL2 = "127.0.0.1:5002"
privateRegistryURL3 = "127.0.0.1:5003"
privateRegistryURL4 = "127.0.0.1:5004"
skopeoBinary = "skopeo"
)
func Test(t *testing.T) {
@@ -73,51 +71,35 @@ func (s *SkopeoSuite) TearDownTest(c *check.C) {
//func skopeoCmd()
func (s *SkopeoSuite) TestVersion(c *check.C) {
out, err := exec.Command(skopeoBinary, "--version").CombinedOutput()
c.Assert(err, check.IsNil, check.Commentf(string(out)))
wanted := skopeoBinary + " version "
if !strings.Contains(string(out), wanted) {
c.Fatalf("wanted %s, got %s", wanted, string(out))
}
wanted := fmt.Sprintf(".*%s version %s.*", skopeoBinary, version.Version)
assertSkopeoSucceeds(c, wanted, "--version")
}
var (
errFetchManifest = "error fetching manifest: status code: %s"
const (
errFetchManifestRegexp = ".*error fetching manifest: status code: %s.*"
)
func (s *SkopeoSuite) TestCanAuthToPrivateRegistryV2WithoutDockerCfg(c *check.C) {
// TODO(runcom)
c.Skip("we need to restore --username --password flags!")
out, err := exec.Command(skopeoBinary, "--docker-cfg=''", "--username="+s.regV2WithAuth.username, "--password="+s.regV2WithAuth.password, "inspect", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url)).CombinedOutput()
c.Assert(err, check.NotNil, check.Commentf(string(out)))
wanted := fmt.Sprintf(errFetchManifest, "401")
if !strings.Contains(string(out), wanted) {
c.Fatalf("wanted %s, got %s", wanted, string(out))
}
wanted := fmt.Sprintf(errFetchManifestRegexp, "401")
assertSkopeoFails(c, wanted, "--docker-cfg=''", "--username="+s.regV2WithAuth.username, "--password="+s.regV2WithAuth.password, "inspect", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url))
}
func (s *SkopeoSuite) TestNeedAuthToPrivateRegistryV2WithoutDockerCfg(c *check.C) {
// TODO(runcom): mock the empty docker-cfg by removing it in the test itself (?)
c.Skip("mock empty docker config")
out, err := exec.Command(skopeoBinary, "--docker-cfg=''", "inspect", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url)).CombinedOutput()
c.Assert(err, check.NotNil, check.Commentf(string(out)))
wanted := fmt.Sprintf(errFetchManifest, "401")
if !strings.Contains(string(out), wanted) {
c.Fatalf("wanted %s, got %s", wanted, string(out))
}
wanted := fmt.Sprintf(errFetchManifestRegexp, "401")
assertSkopeoFails(c, wanted, "--docker-cfg=''", "inspect", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url))
}
// TODO(runcom): as soon as we can push to registries ensure you can inspect here
// not just get image not found :)
func (s *SkopeoSuite) TestNoNeedAuthToPrivateRegistryV2ImageNotFound(c *check.C) {
out, err := exec.Command(skopeoBinary, "inspect", fmt.Sprintf("docker://%s/busybox:latest", s.regV2.url)).CombinedOutput()
out, err := exec.Command(skopeoBinary, "--tls-verify=false", "inspect", fmt.Sprintf("docker://%s/busybox:latest", s.regV2.url)).CombinedOutput()
c.Assert(err, check.NotNil, check.Commentf(string(out)))
wanted := fmt.Sprintf(errFetchManifest, "404")
if !strings.Contains(string(out), wanted) {
c.Fatalf("wanted %s, got %s", wanted, string(out))
}
wanted = fmt.Sprintf(errFetchManifest, "401")
if strings.Contains(string(out), wanted) {
c.Fatalf("not wanted %s, got %s", wanted, string(out))
}
wanted := fmt.Sprintf(errFetchManifestRegexp, "404")
c.Assert(string(out), check.Matches, "(?s)"+wanted) // (?s) : '.' will also match newlines
wanted = fmt.Sprintf(errFetchManifestRegexp, "401")
c.Assert(string(out), check.Not(check.Matches), "(?s)"+wanted) // (?s) : '.' will also match newlines
}

293
integration/copy_test.go Normal file
View File

@@ -0,0 +1,293 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/containers/image/manifest"
"github.com/go-check/check"
)
func init() {
check.Suite(&CopySuite{})
}
const v2DockerRegistryURL = "localhost:5555"
type CopySuite struct {
cluster *openshiftCluster
registry *testRegistryV2
gpgHome string
}
func (s *CopySuite) SetUpSuite(c *check.C) {
if os.Getenv("SKOPEO_CONTAINER_TESTS") != "1" {
c.Skip("Not running in a container, refusing to affect user state")
}
s.cluster = startOpenshiftCluster(c) // FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place.
for _, stream := range []string{"unsigned", "personal", "official", "naming", "cosigned", "compression"} {
isJSON := fmt.Sprintf(`{
"kind": "ImageStream",
"apiVersion": "v1",
"metadata": {
"name": "%s"
},
"spec": {}
}`, stream)
runCommandWithInput(c, isJSON, "oc", "create", "-f", "-")
}
s.registry = setupRegistryV2At(c, v2DockerRegistryURL, false, false) // FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place.
gpgHome, err := ioutil.TempDir("", "skopeo-gpg")
c.Assert(err, check.IsNil)
s.gpgHome = gpgHome
os.Setenv("GNUPGHOME", s.gpgHome)
for _, key := range []string{"personal", "official"} {
batchInput := fmt.Sprintf("Key-Type: RSA\nName-Real: Test key - %s\nName-email: %s@example.com\n%%commit\n",
key, key)
runCommandWithInput(c, batchInput, gpgBinary, "--batch", "--gen-key")
out := combinedOutputOfCommand(c, gpgBinary, "--armor", "--export", fmt.Sprintf("%s@example.com", key))
err := ioutil.WriteFile(filepath.Join(s.gpgHome, fmt.Sprintf("%s-pubkey.gpg", key)),
[]byte(out), 0600)
c.Assert(err, check.IsNil)
}
}
func (s *CopySuite) TearDownSuite(c *check.C) {
if s.gpgHome != "" {
os.RemoveAll(s.gpgHome)
}
if s.registry != nil {
s.registry.Close()
}
if s.cluster != nil {
s.cluster.tearDown()
}
}
// preparePolicyFixture applies edits to fixtures/policy.json and returns a path to the temporary file.
// Callers should defer os.Remove(the_returned_path)
func preparePolicyFixture(c *check.C, edits map[string]string) string {
commands := []string{}
for template, value := range edits {
commands = append(commands, fmt.Sprintf("s,%s,%s,g", template, value))
}
json := combinedOutputOfCommand(c, "sed", strings.Join(commands, "; "), "fixtures/policy.json")
file, err := ioutil.TempFile("", "policy.json")
c.Assert(err, check.IsNil)
path := file.Name()
_, err = file.Write([]byte(json))
c.Assert(err, check.IsNil)
err = file.Close()
c.Assert(err, check.IsNil)
return path
}
// The most basic (skopeo copy) use:
func (s *CopySuite) TestCopySimple(c *check.C) {
dir1, err := ioutil.TempDir("", "copy-1")
c.Assert(err, check.IsNil)
defer os.RemoveAll(dir1)
dir2, err := ioutil.TempDir("", "copy-2")
c.Assert(err, check.IsNil)
defer os.RemoveAll(dir2)
// FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection.
// "pull": docker: → dir:
assertSkopeoSucceeds(c, "", "copy", "docker://estesp/busybox:latest", "dir:"+dir1)
// "push": dir: → atomic:
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--debug", "copy", "dir:"+dir1, "atomic:localhost:5000/myns/unsigned:unsigned")
// The result of pushing and pulling is an unmodified image.
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/unsigned:unsigned", "dir:"+dir2)
out := combinedOutputOfCommand(c, "diff", "-urN", dir1, dir2)
c.Assert(out, check.Equals, "")
// docker v2s2 -> OCI image layout
// ociDest will be created by oci: if it doesn't exist
// so don't create it here to exercise auto-creation
ociDest := "busybox-latest"
defer os.RemoveAll(ociDest)
assertSkopeoSucceeds(c, "", "copy", "docker://busybox:latest", "oci:"+ociDest)
_, err = os.Stat(ociDest)
c.Assert(err, check.IsNil)
// FIXME: Also check pushing to docker://
}
// Streaming (skopeo copy)
func (s *CopySuite) TestCopyStreaming(c *check.C) {
dir1, err := ioutil.TempDir("", "streaming-1")
c.Assert(err, check.IsNil)
defer os.RemoveAll(dir1)
dir2, err := ioutil.TempDir("", "streaming-2")
c.Assert(err, check.IsNil)
defer os.RemoveAll(dir2)
// FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection.
// streaming: docker: → atomic:
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--debug", "copy", "docker://estesp/busybox:amd64", "atomic:localhost:5000/myns/unsigned:streaming")
// Compare (copies of) the original and the copy:
assertSkopeoSucceeds(c, "", "copy", "docker://estesp/busybox:amd64", "dir:"+dir1)
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/unsigned:streaming", "dir:"+dir2)
// The manifests will have different JWS signatures; so, compare the manifests by digests, which
// strips the signatures, and remove them, comparing the rest file by file.
digests := []string{}
for _, dir := range []string{dir1, dir2} {
manifestPath := filepath.Join(dir, "manifest.json")
m, err := ioutil.ReadFile(manifestPath)
c.Assert(err, check.IsNil)
digest, err := manifest.Digest(m)
c.Assert(err, check.IsNil)
digests = append(digests, digest)
err = os.Remove(manifestPath)
c.Assert(err, check.IsNil)
c.Logf("Manifest file %s (digest %s) removed", manifestPath, digest)
}
c.Assert(digests[0], check.Equals, digests[1])
out := combinedOutputOfCommand(c, "diff", "-urN", dir1, dir2)
c.Assert(out, check.Equals, "")
// FIXME: Also check pushing to docker://
}
// --sign-by and --policy copy, primarily using atomic:
func (s *CopySuite) TestCopySignatures(c *check.C) {
dir, err := ioutil.TempDir("", "signatures-dest")
c.Assert(err, check.IsNil)
defer os.RemoveAll(dir)
dirDest := "dir:" + dir
policy := preparePolicyFixture(c, map[string]string{"@keydir@": s.gpgHome})
defer os.Remove(policy)
// type: reject
assertSkopeoFails(c, ".*Source image rejected: Running image docker://busybox:latest is rejected by policy.*",
"--policy", policy, "copy", "docker://busybox:latest", dirDest)
// type: insecureAcceptAnything
assertSkopeoSucceeds(c, "", "--policy", policy, "copy", "docker://openshift/hello-openshift", dirDest)
// type: signedBy
// Sign the images
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "--sign-by", "personal@example.com", "docker://busybox:1.23", "atomic:localhost:5000/myns/personal:personal")
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "--sign-by", "official@example.com", "docker://busybox:1.23.2", "atomic:localhost:5000/myns/official:official")
// Verify that we can pull them
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5000/myns/personal:personal", dirDest)
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5000/myns/official:official", dirDest)
// Verify that mis-signed images are rejected
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/personal:personal", "atomic:localhost:5000/myns/official:attack")
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/official:official", "atomic:localhost:5000/myns/personal:attack")
assertSkopeoFails(c, ".*Source image rejected: Invalid GPG signature.*",
"--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5000/myns/personal:attack", dirDest)
assertSkopeoFails(c, ".*Source image rejected: Invalid GPG signature.*",
"--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5000/myns/official:attack", dirDest)
// Verify that signed identity is verified.
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/official:official", "atomic:localhost:5000/myns/naming:test1")
assertSkopeoFails(c, ".*Source image rejected: Signature for identity localhost:5000/myns/official:official is not accepted.*",
"--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5000/myns/naming:test1", dirDest)
// signedIdentity works
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/official:official", "atomic:localhost:5000/myns/naming:naming")
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5000/myns/naming:naming", dirDest)
// Verify that cosigning requirements are enforced
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/official:official", "atomic:localhost:5000/myns/cosigned:cosigned")
assertSkopeoFails(c, ".*Source image rejected: Invalid GPG signature.*",
"--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5000/myns/cosigned:cosigned", dirDest)
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "--sign-by", "personal@example.com", "atomic:localhost:5000/myns/official:official", "atomic:localhost:5000/myns/cosigned:cosigned")
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "copy", "atomic:localhost:5000/myns/cosigned:cosigned", dirDest)
}
// --policy copy for dir: sources
func (s *CopySuite) TestCopyDirSignatures(c *check.C) {
topDir, err := ioutil.TempDir("", "dir-signatures-top")
c.Assert(err, check.IsNil)
defer os.RemoveAll(topDir)
topDirDest := "dir:" + topDir
for _, suffix := range []string{"/dir1", "/dir2", "/restricted/personal", "/restricted/official", "/restricted/badidentity", "/dest"} {
err := os.MkdirAll(topDir+suffix, 0755)
c.Assert(err, check.IsNil)
}
// Note the "/@dirpath@": The value starts with a slash so that it is not rejected in other tests which do not replace it,
// but we must ensure that the result is a canonical path, not something starting with a "//".
policy := preparePolicyFixture(c, map[string]string{"@keydir@": s.gpgHome, "/@dirpath@": topDir + "/restricted"})
defer os.Remove(policy)
// Get some images.
assertSkopeoSucceeds(c, "", "copy", "docker://estesp/busybox:armfh", topDirDest+"/dir1")
assertSkopeoSucceeds(c, "", "copy", "docker://estesp/busybox:s390x", topDirDest+"/dir2")
// Sign the images. By coping fom a topDirDest/dirN, also test that non-/restricted paths
// use the dir:"" default of insecureAcceptAnything.
// (For signing, we must push to atomic: to get a Docker identity to use in the signature.)
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "copy", "--sign-by", "personal@example.com", topDirDest+"/dir1", "atomic:localhost:5000/myns/personal:dirstaging")
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "copy", "--sign-by", "official@example.com", topDirDest+"/dir2", "atomic:localhost:5000/myns/official:dirstaging")
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/personal:dirstaging", topDirDest+"/restricted/personal")
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/official:dirstaging", topDirDest+"/restricted/official")
// type: signedBy, with a signedIdentity override (necessary because dir: identities can't be signed)
// Verify that correct images are accepted
assertSkopeoSucceeds(c, "", "--policy", policy, "copy", topDirDest+"/restricted/official", topDirDest+"/dest")
// ... and that mis-signed images are rejected.
assertSkopeoFails(c, ".*Source image rejected: Invalid GPG signature.*",
"--policy", policy, "copy", topDirDest+"/restricted/personal", topDirDest+"/dest")
// Verify that the signed identity is verified.
assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "copy", "--sign-by", "official@example.com", topDirDest+"/dir1", "atomic:localhost:5000/myns/personal:dirstaging2")
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/personal:dirstaging2", topDirDest+"/restricted/badidentity")
assertSkopeoFails(c, ".*Source image rejected: .*Signature for identity localhost:5000/myns/personal:dirstaging2 is not accepted.*",
"--policy", policy, "copy", topDirDest+"/restricted/badidentity", topDirDest+"/dest")
}
// Compression during copy
func (s *CopySuite) TestCopyCompression(c *check.C) {
const uncompresssedLayerFile = "160d823fdc48e62f97ba62df31e55424f8f5eb6b679c865eec6e59adfe304710.tar"
topDir, err := ioutil.TempDir("", "compression-top")
c.Assert(err, check.IsNil)
defer os.RemoveAll(topDir)
for i, t := range []struct{ fixture, remote string }{
//{"uncompressed-image-s1", "docker://" + v2DockerRegistryURL + "/compression/compression:s1"}, // FIXME: depends on push to tag working
//{"uncompressed-image-s2", "docker://" + v2DockerRegistryURL + "/compression/compression:s2"}, // FIXME: depends on push to tag working
{"uncompressed-image-s1", "atomic:localhost:5000/myns/compression:s1"},
//{"uncompressed-image-s2", "atomic:localhost:5000/myns/compression:s2"}, // FIXME: The unresolved "MANIFEST_UNKNOWN"/"unexpected end of JSON input" failure
} {
dir := filepath.Join(topDir, fmt.Sprintf("case%d", i))
err := os.MkdirAll(dir, 0755)
c.Assert(err, check.IsNil)
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "dir:fixtures/"+t.fixture, t.remote)
assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", t.remote, "dir:"+dir)
// The original directory contained an uncompressed file, the copy after pushing and pulling doesn't (we use a different name for the compressed file).
_, err = os.Lstat(filepath.Join("fixtures", t.fixture, uncompresssedLayerFile))
c.Assert(err, check.IsNil)
_, err = os.Lstat(filepath.Join(dir, uncompresssedLayerFile))
c.Assert(err, check.NotNil)
c.Assert(os.IsNotExist(err), check.Equals, true)
// All pulled layers are smaller than the uncompressed size of uncompresssedLayerFile. (Note that this includes the manifest in s2, but that works out OK).
dirf, err := os.Open(dir)
c.Assert(err, check.IsNil)
fis, err := dirf.Readdir(-1)
c.Assert(err, check.IsNil)
for _, fi := range fis {
if strings.HasSuffix(fi.Name(), ".tar") {
c.Assert(fi.Size() < 2048, check.Equals, true)
}
}
}
}

View File

@@ -0,0 +1,84 @@
{
"default": [
{
"type": "reject"
}
],
"transports": {
"docker": {
"docker.io/openshift": [
{
"type": "insecureAcceptAnything"
}
]
},
"dir": {
"/@dirpath@": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "@keydir@/official-pubkey.gpg",
"signedIdentity": {
"type": "exactRepository",
"dockerRepository": "localhost:5000/myns/official"
}
}
],
"": [
{
"type": "insecureAcceptAnything"
}
]
},
"atomic": {
"localhost:5000/myns/personal": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "@keydir@/personal-pubkey.gpg"
}
],
"localhost:5000/myns/official": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "@keydir@/official-pubkey.gpg"
}
],
"localhost:5000/myns/naming:test1": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "@keydir@/official-pubkey.gpg"
}
],
"localhost:5000/myns/naming:naming": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "@keydir@/official-pubkey.gpg",
"signedIdentity": {
"type": "exactRepository",
"dockerRepository": "localhost:5000/myns/official"
}
}
],
"localhost:5000/myns/cosigned:cosigned": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "@keydir@/official-pubkey.gpg",
"signedIdentity": {
"type": "exactRepository",
"dockerRepository": "localhost:5000/myns/official"
}
},
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "@keydir@/personal-pubkey.gpg"
}
]
}
}
}

View File

@@ -0,0 +1,32 @@
{
"schemaVersion": 1,
"name": "nonempty",
"tag": "nonempty",
"architecture": "amd64",
"fsLayers": [
{
"blobSum": "sha256:160d823fdc48e62f97ba62df31e55424f8f5eb6b679c865eec6e59adfe304710"
}
],
"history": [
{
"v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"59c20544b2f4\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container\":\"59c20544b2f4ad7a8639433bacb1ec215b7dad4a7bf1a83b5ab4679329a46c1d\",\"container_config\":{\"Hostname\":\"59c20544b2f4\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:14f49faade3db5e596826746d9ed3dfd658490c16c4d61d4886726153ad0591a in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"created\":\"2016-09-19T18:23:54.9949213Z\",\"docker_version\":\"1.10.3\",\"id\":\"4c224eac5061bb85f523ca4e3316618fd7921a80fe94286979667b1edb8e1bdd\",\"os\":\"linux\"}"
}
],
"signatures": [
{
"header": {
"jwk": {
"crv": "P-256",
"kid": "DGWZ:GAUM:WCOC:IMDL:D67M:CEI6:YTVH:M2CM:5HX4:FYDD:77OD:D3F7",
"kty": "EC",
"x": "eprZNqLO9mHZ4Z4GxefucEgov_1gwEi9lehpJR2suRo",
"y": "wIr2ucNg32ROfVCkR_8A5VbBJ-mFmsoIUVa6vt8lIxM"
},
"alg": "ES256"
},
"signature": "bvTLWW4YVFRjAanN1EJqwQw60fWSWJPxcGO3UZGFI_gyV6ucGdW4x7jyYL6g06sg925s9cy0wN1lw91CCFv4BA",
"protected": "eyJmb3JtYXRMZW5ndGgiOjE0ODcsImZvcm1hdFRhaWwiOiJDbjBLIiwidGltZSI6IjIwMTYtMDktMTlUMTg6NDM6MzNaIn0"
}
]
}

View File

@@ -0,0 +1 @@
{"architecture":"amd64","config":{"Hostname":"59c20544b2f4","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"59c20544b2f4ad7a8639433bacb1ec215b7dad4a7bf1a83b5ab4679329a46c1d","container_config":{"Hostname":"59c20544b2f4","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ADD file:14f49faade3db5e596826746d9ed3dfd658490c16c4d61d4886726153ad0591a in /"],"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2016-09-19T18:23:54.9949213Z","docker_version":"1.10.3","history":[{"created":"2016-09-19T18:23:54.9949213Z","created_by":"/bin/sh -c #(nop) ADD file:14f49faade3db5e596826746d9ed3dfd658490c16c4d61d4886726153ad0591a in /"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:160d823fdc48e62f97ba62df31e55424f8f5eb6b679c865eec6e59adfe304710"]}}

View File

@@ -0,0 +1,16 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/octet-stream",
"size": 1272,
"digest": "sha256:86ce150e65c72b30f885c261449d18b7c6832596916e7f654e08377b5a67b4ff"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2048,
"digest": "sha256:160d823fdc48e62f97ba62df31e55424f8f5eb6b679c865eec6e59adfe304710"
}
]
}

193
integration/openshift.go Normal file
View File

@@ -0,0 +1,193 @@
package main
import (
"bufio"
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/docker/docker/pkg/homedir"
"github.com/go-check/check"
)
// openshiftCluster is an OpenShift API master and integrated registry
// running on localhost.
type openshiftCluster struct {
c *check.C
workingDir string
master *exec.Cmd
registry *exec.Cmd
}
// startOpenshiftCluster creates a new openshiftCluster.
// WARNING: This affects state in users' home directory! Only run
// in isolated test environment.
func startOpenshiftCluster(c *check.C) *openshiftCluster {
cluster := &openshiftCluster{c: c}
dir, err := ioutil.TempDir("", "openshift-cluster")
cluster.c.Assert(err, check.IsNil)
cluster.workingDir = dir
cluster.startMaster()
cluster.startRegistry()
cluster.ocLoginToProject()
cluster.dockerLogin()
cluster.relaxImageSignerPermissions()
return cluster
}
// startMaster starts the OpenShift master (etcd+API server) and waits for it to be ready, or terminates on failure.
func (c *openshiftCluster) startMaster() {
c.master = exec.Command("openshift", "start", "master")
c.master.Dir = c.workingDir
stdout, err := c.master.StdoutPipe()
// Send both to the same pipe. This might cause the two streams to be mixed up,
// but logging actually goes only to stderr - this primarily ensure we log any
// unexpected output to stdout.
c.master.Stderr = c.master.Stdout
err = c.master.Start()
c.c.Assert(err, check.IsNil)
portOpen, terminatePortCheck := newPortChecker(c.c, 8443)
defer func() {
c.c.Logf("Terminating port check")
terminatePortCheck <- true
}()
terminateLogCheck := make(chan bool, 1)
logCheckFound := make(chan bool)
go func() {
defer func() {
c.c.Logf("Log checker exiting")
}()
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
c.c.Logf("Log line: %s", line)
if strings.Contains(line, "Started Origin Controllers") {
logCheckFound <- true
return
// FIXME? We stop reading from stdout; could this block the master?
}
// Note: we can block before we get here.
select {
case <-terminateLogCheck:
c.c.Logf("terminated")
return
default:
// Do not block here and read the next line.
}
}
logCheckFound <- false
}()
defer func() {
c.c.Logf("Terminating log check")
terminateLogCheck <- true
}()
gotPortCheck := false
gotLogCheck := false
for !gotPortCheck || !gotLogCheck {
c.c.Logf("Waiting for master")
select {
case <-portOpen:
c.c.Logf("port check done")
gotPortCheck = true
case found := <-logCheckFound:
c.c.Logf("log check done, found: %t", found)
if !found {
c.c.Fatal("log check done, success message not found")
}
gotLogCheck = true
}
}
c.c.Logf("OK, master started!")
}
// startRegistry starts the OpenShift registry and waits for it to be ready, or terminates on failure.
func (c *openshiftCluster) startRegistry() {
//KUBECONFIG=openshift.local.config/master/openshift-registry.kubeconfig DOCKER_REGISTRY_URL=127.0.0.1:5000
c.registry = exec.Command("dockerregistry", "/atomic-registry-config.yml")
c.registry.Dir = c.workingDir
c.registry.Env = os.Environ()
c.registry.Env = modifyEnviron(c.registry.Env, "KUBECONFIG", "openshift.local.config/master/openshift-registry.kubeconfig")
c.registry.Env = modifyEnviron(c.registry.Env, "DOCKER_REGISTRY_URL", "127.0.0.1:5000")
consumeAndLogOutputs(c.c, "registry", c.registry)
err := c.registry.Start()
c.c.Assert(err, check.IsNil)
portOpen, terminatePortCheck := newPortChecker(c.c, 5000)
defer func() {
terminatePortCheck <- true
}()
c.c.Logf("Waiting for registry to start")
<-portOpen
c.c.Logf("OK, Registry port open")
}
// ocLogin runs (oc login) and (oc new-project) on the cluster, or terminates on failure.
func (c *openshiftCluster) ocLoginToProject() {
c.c.Logf("oc login")
cmd := exec.Command("oc", "login", "--certificate-authority=openshift.local.config/master/ca.crt", "-u", "myuser", "-p", "mypw", "https://localhost:8443")
cmd.Dir = c.workingDir
out, err := cmd.CombinedOutput()
c.c.Assert(err, check.IsNil, check.Commentf("%s", out))
c.c.Assert(string(out), check.Matches, "(?s).*Login successful.*") // (?s) : '.' will also match newlines
outString := combinedOutputOfCommand(c.c, "oc", "new-project", "myns")
c.c.Assert(outString, check.Matches, `(?s).*Now using project "myns".*`) // (?s) : '.' will also match newlines
}
// dockerLogin simulates (docker login) to the cluster, or terminates on failure.
// We do not run (docker login) directly, because that requires a running daemon and a docker package.
func (c *openshiftCluster) dockerLogin() {
dockerDir := filepath.Join(homedir.Get(), ".docker")
err := os.Mkdir(dockerDir, 0700)
c.c.Assert(err, check.IsNil)
out := combinedOutputOfCommand(c.c, "oc", "config", "view", "-o", "json", "-o", "jsonpath={.users[*].user.token}")
c.c.Logf("oc config value: %s", out)
configJSON := fmt.Sprintf(`{
"auths": {
"localhost:5000": {
"auth": "%s",
"email": "unused"
}
}
}`, base64.StdEncoding.EncodeToString([]byte("unused:"+out)))
err = ioutil.WriteFile(filepath.Join(dockerDir, "config.json"), []byte(configJSON), 0600)
c.c.Assert(err, check.IsNil)
}
// relaxImageSignerPermissions opens up the system:image-signer permissions so that
// anyone can work with signatures
// FIXME: This also allows anyone to DoS anyone else; this design is really not all
// that workable, but it is the best we can do for now.
func (c *openshiftCluster) relaxImageSignerPermissions() {
cmd := exec.Command("oadm", "policy", "add-cluster-role-to-group", "system:image-signer", "system:authenticated")
cmd.Dir = c.workingDir
cmd.Env = os.Environ()
cmd.Env = modifyEnviron(cmd.Env, "KUBECONFIG", "openshift.local.config/master/admin.kubeconfig")
out, err := cmd.CombinedOutput()
c.c.Assert(err, check.IsNil, check.Commentf("%s", string(out)))
c.c.Assert(string(out), check.Equals, "")
}
// tearDown stops the cluster services and deletes (only some!) of the state.
func (c *openshiftCluster) tearDown() {
if c.registry != nil && c.registry.Process != nil {
c.registry.Process.Kill()
}
if c.master != nil && c.master.Process != nil {
c.master.Process.Kill()
}
if c.workingDir != "" {
os.RemoveAll(c.workingDir)
}
}

View File

@@ -2,7 +2,7 @@ package main
import (
"errors"
"io"
"fmt"
"io/ioutil"
"os"
"os/exec"
@@ -35,26 +35,6 @@ func findFingerprint(lineBytes []byte) (string, error) {
return "", errors.New("No fingerprint found")
}
// ConsumeAndLogOutput takes (f, err) from an exec.*Pipe(), and causes all output to it to be logged to c.
func ConsumeAndLogOutput(c *check.C, id string, f io.ReadCloser, err error) {
c.Assert(err, check.IsNil)
go func() {
defer func() {
f.Close()
c.Logf("Output %s: Closed", id)
}()
buf := make([]byte, 0, 1024)
for {
c.Logf("Output %s: waiting", id)
n, err := f.Read(buf)
c.Logf("Output %s: got %d,%#v: %#v", id, n, err, buf[:n])
if n <= 0 {
break
}
}
}()
}
func (s *SigningSuite) SetUpTest(c *check.C) {
_, err := exec.LookPath(skopeoBinary)
c.Assert(err, check.IsNil)
@@ -63,21 +43,7 @@ func (s *SigningSuite) SetUpTest(c *check.C) {
c.Assert(err, check.IsNil)
os.Setenv("GNUPGHOME", s.gpgHome)
cmd := exec.Command(gpgBinary, "--homedir", s.gpgHome, "--batch", "--gen-key")
stdin, err := cmd.StdinPipe()
c.Assert(err, check.IsNil)
stdout, err := cmd.StdoutPipe()
ConsumeAndLogOutput(c, "gen-key stdout", stdout, err)
stderr, err := cmd.StderrPipe()
ConsumeAndLogOutput(c, "gen-key stderr", stderr, err)
err = cmd.Start()
c.Assert(err, check.IsNil)
_, err = stdin.Write([]byte("Key-Type: RSA\nName-Real: Testing user\n%commit\n"))
c.Assert(err, check.IsNil)
err = stdin.Close()
c.Assert(err, check.IsNil)
err = cmd.Wait()
c.Assert(err, check.IsNil)
runCommandWithInput(c, "Key-Type: RSA\nName-Real: Testing user\n%commit\n", gpgBinary, "--homedir", s.gpgHome, "--batch", "--gen-key")
lines, err := exec.Command(gpgBinary, "--homedir", s.gpgHome, "--with-colons", "--no-permission-warning", "--fingerprint").Output()
c.Assert(err, check.IsNil)
@@ -102,13 +68,10 @@ func (s *SigningSuite) TestSignVerifySmoke(c *check.C) {
sigOutput, err := ioutil.TempFile("", "sig")
c.Assert(err, check.IsNil)
defer os.Remove(sigOutput.Name())
out, err := exec.Command(skopeoBinary, "standalone-sign", "-o", sigOutput.Name(),
manifestPath, dockerReference, s.fingerprint).CombinedOutput()
c.Assert(err, check.IsNil, check.Commentf("%s", out))
c.Assert(string(out), check.Equals, "")
assertSkopeoSucceeds(c, "^$", "standalone-sign", "-o", sigOutput.Name(),
manifestPath, dockerReference, s.fingerprint)
out, err = exec.Command(skopeoBinary, "standalone-verify", manifestPath,
dockerReference, s.fingerprint, sigOutput.Name()).CombinedOutput()
c.Assert(err, check.IsNil, check.Commentf("%s", out))
c.Assert(string(out), check.Equals, "Signature verified, digest "+TestImageManifestDigest+"\n")
expected := fmt.Sprintf("^Signature verified, digest %s\n$", TestImageManifestDigest)
assertSkopeoSucceeds(c, expected, "standalone-verify", manifestPath,
dockerReference, s.fingerprint, sigOutput.Name())
}

146
integration/utils.go Normal file
View File

@@ -0,0 +1,146 @@
package main
import (
"io"
"net"
"os/exec"
"strings"
"time"
"github.com/go-check/check"
)
const skopeoBinary = "skopeo"
// consumeAndLogOutputStream takes (f, err) from an exec.*Pipe(), and causes all output to it to be logged to c.
func consumeAndLogOutputStream(c *check.C, id string, f io.ReadCloser, err error) {
c.Assert(err, check.IsNil)
go func() {
defer func() {
f.Close()
c.Logf("Output %s: Closed", id)
}()
buf := make([]byte, 1024)
for {
c.Logf("Output %s: waiting", id)
n, err := f.Read(buf)
c.Logf("Output %s: got %d,%#v: %s", id, n, err, strings.TrimSuffix(string(buf[:n]), "\n"))
if n <= 0 {
break
}
}
}()
}
// consumeAndLogOutputs causes all output to stdout and stderr from an *exec.Cmd to be logged to c
func consumeAndLogOutputs(c *check.C, id string, cmd *exec.Cmd) {
stdout, err := cmd.StdoutPipe()
consumeAndLogOutputStream(c, id+" stdout", stdout, err)
stderr, err := cmd.StderrPipe()
consumeAndLogOutputStream(c, id+" stderr", stderr, err)
}
// combinedOutputOfCommand runs a command as if exec.Command().CombinedOutput(), verifies that the exit status is 0, and returns the output,
// or terminates c on failure.
func combinedOutputOfCommand(c *check.C, name string, args ...string) string {
c.Logf("Running %s %s", name, strings.Join(args, " "))
out, err := exec.Command(name, args...).CombinedOutput()
c.Assert(err, check.IsNil, check.Commentf("%s", out))
return string(out)
}
// assertSkopeoSucceeds runs a skopeo command as if exec.Command().CombinedOutput, verifies that the exit status is 0,
// and optionally that the output matches a multi-line regexp if it is nonempty;
// or terminates c on failure
func assertSkopeoSucceeds(c *check.C, regexp string, args ...string) {
c.Logf("Running %s %s", skopeoBinary, strings.Join(args, " "))
out, err := exec.Command(skopeoBinary, args...).CombinedOutput()
c.Assert(err, check.IsNil, check.Commentf("%s", out))
if regexp != "" {
c.Assert(string(out), check.Matches, "(?s)"+regexp) // (?s) : '.' will also match newlines
}
}
// assertSkopeoFails runs a skopeo command as if exec.Command().CombinedOutput, verifies that the exit status is 0,
// and that the output matches a multi-line regexp;
// or terminates c on failure
func assertSkopeoFails(c *check.C, regexp string, args ...string) {
c.Logf("Running %s %s", skopeoBinary, strings.Join(args, " "))
out, err := exec.Command(skopeoBinary, args...).CombinedOutput()
c.Assert(err, check.NotNil, check.Commentf("%s", out))
c.Assert(string(out), check.Matches, "(?s)"+regexp) // (?s) : '.' will also match newlines
}
// runCommandWithInput runs a command as if exec.Command(), sending it the input to stdin,
// and verifies that the exit status is 0, or terminates c on failure.
func runCommandWithInput(c *check.C, input string, name string, args ...string) {
c.Logf("Running %s %s", name, strings.Join(args, " "))
cmd := exec.Command(name, args...)
consumeAndLogOutputs(c, name+" "+strings.Join(args, " "), cmd)
stdin, err := cmd.StdinPipe()
c.Assert(err, check.IsNil)
err = cmd.Start()
c.Assert(err, check.IsNil)
_, err = stdin.Write([]byte(input))
c.Assert(err, check.IsNil)
err = stdin.Close()
c.Assert(err, check.IsNil)
err = cmd.Wait()
c.Assert(err, check.IsNil)
}
// isPortOpen returns true iff the specified port on localhost is open.
func isPortOpen(port int) bool {
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port})
if err != nil {
return false
}
conn.Close()
return true
}
// newPortChecker sets up a portOpen channel which will receive true after the specified port is open.
// The checking can be aborted by sending a value to the terminate channel, which the caller should
// always do using
// defer func() {terminate <- true}()
func newPortChecker(c *check.C, port int) (portOpen <-chan bool, terminate chan<- bool) {
portOpenBidi := make(chan bool)
// Buffered, so that sending a terminate request after the goroutine has exited does not block.
terminateBidi := make(chan bool, 1)
go func() {
defer func() {
c.Logf("Port checker for port %d exiting", port)
}()
for {
c.Logf("Checking for port %d...", port)
if isPortOpen(port) {
c.Logf("Port %d open", port)
portOpenBidi <- true
return
}
c.Logf("Sleeping for port %d", port)
sleepChan := time.After(100 * time.Millisecond)
select {
case <-sleepChan: // Try again
c.Logf("Sleeping for port %d done, will retry", port)
case <-terminateBidi:
c.Logf("Check for port %d terminated", port)
return
}
}
}()
return portOpenBidi, terminateBidi
}
// modifyEnviron modifies os.Environ()-like list of name=value assignments to set name to value.
func modifyEnviron(env []string, name, value string) []string {
prefix := name + "="
res := []string{}
for _, e := range env {
if !strings.HasPrefix(e, prefix) {
res = append(res, e)
}
}
return append(res, prefix+value)
}

View File

@@ -1,118 +0,0 @@
.\" To review this file formatted
.\" groff -man -Tascii skopeo.1
.\"
.de FN
\fI\|\\$1\|\fP
..
.TH "skopeo" "1" "2016-04-21" "Linux" "Linux Programmer's Manual"
.SH NAME
skopeo \(em Inspect Docker images and repositories on registries
.SH SYNOPSIS
\fBskopeo copy\fR [\fB--sign-by=\fRkey-ID] source-location destination-location
.PP
\fBskopeo inspect\fR image-name [\fB--raw\fR]
.PP
\fBskopeo layers\fR image-name
.PP
\fBskopeo standalone-sign\fR manifest docker-reference key-fingerprint \%\fB--output\fR|\fB-o\fR signature
.PP
\fBskopeo standalone-verify\fR manifest docker-reference key-fingerprint \%signature
.PP
\fBskopeo help\fR [command]
.SH DESCRIPTION
\fBskopeo\fR is a command line utility which is able to inspect a repository on a Docker registry and fetch images
layers. It fetches the repository's manifest and it is able to show you a \fBdocker inspect\fR-like json output about a
whole repository or a tag. This tool, in contrast to \fBdocker inspect\fR, helps you gather useful information about a
repository or a tag without requiring you to run \fBdocker pull\fR - e.g. - which tags are available for the given
repository? which labels the image has?
.SH OPTIONS
.B "--debug"
enable debug output
.PP
.B "--username"
Username to use to authenicate to the given registry
.PP
.B --password
Password to use to authenicate to the given registry
.PP
.B "--cert-path"
Path to certificates to use to authenicate to the given registry (cert.pem, key.pem)
.PP
.B "--tls-verify"
Whether to verify certificates or not
.PP
.B "--help, -h"
Show help
.PP
.B "--version, -v"
print the version number
.SH COMMANDS
.TP
.B copy
Copy an image (manifest, filesystem layers, signatures) from one location to another.
.sp
.B source-location
and
.B destination-location
can be \fBdocker://\fRdocker-reference, \fBdir:\fRlocal-path, or \fBatomic:\fRimagestream-name\fB:\fRtag .
.sp
\fB\-\-sign\-by=\fRkey-id
Add a signature by the specified key ID for image name corresponding to \fBdestination-location\fR.
Existing signatures, if any, are preserved as well.
.TP
.B inspect
Return low-level information on images in a registry
.sp
.B image-name
name of image to retrieve information about
.br
.B "--raw"
output raw manifest, default is to format in JSON
.TP
.B layers
Get image layers
.sp
.B image-name
name of the image to retrieve layers
.TP
.B standalone-sign
Create a signature using local files.
This is primarily a debugging tool and should not be part of your normal operational workflow.
.sp
.B manifest
path to file containing manifest of image
.br
.B docker-reference
docker reference of blob to be signed
.br
.B key-fingerprint
key identity to use for signing
.br
.B ""--output, -o"
write signature to given file
.TP
.B standalone-verify
Verify a signature using local files, digest will be printed on success.
This is primarily a debugging tool and should not be part of your normal operational workflow.
.sp
.B manifest
Path to file containing manifest of image
.br
.B docker-reference
docker reference of signed blob
.br
.B key-fingerprint
key identity to use for verification
.br
.B signature
Path to file containing signature
.TP
.B help
show help for \fBskopeo\fR
.SH AUTHORS
Antonio Murdaca <runcom@redhat.com>
.br
Miloslav Trmac <mitr@redhat.com>
.br
Jhon Honce <jhonce@redhat.com>

View File

@@ -1,398 +0,0 @@
package openshift
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/Sirupsen/logrus"
"github.com/projectatomic/skopeo/docker"
"github.com/projectatomic/skopeo/docker/utils"
"github.com/projectatomic/skopeo/types"
"github.com/projectatomic/skopeo/version"
)
// openshiftClient is configuration for dealing with a single image stream, for reading or writing.
type openshiftClient struct {
// Values from Kubernetes configuration
baseURL *url.URL
httpClient *http.Client
bearerToken string // "" if not used
username string // "" if not used
password string // if username != ""
// Values specific to this image
namespace string
stream string
tag string
}
// FIXME: Is imageName like this a good way to refer to OpenShift images?
var imageNameRegexp = regexp.MustCompile("^([^:/]*)/([^:/]*):([^:/]*)$")
// newOpenshiftClient creates a new openshiftClient for the specified image.
func newOpenshiftClient(imageName string) (*openshiftClient, error) {
// Overall, this is modelled on openshift/origin/pkg/cmd/util/clientcmd.New().ClientConfig() and openshift/origin/pkg/client.
cmdConfig := defaultClientConfig()
logrus.Debugf("cmdConfig: %#v", cmdConfig)
restConfig, err := cmdConfig.ClientConfig()
if err != nil {
return nil, err
}
// REMOVED: SetOpenShiftDefaults (values are not overridable in config files, so hard-coded these defaults.)
logrus.Debugf("restConfig: %#v", restConfig)
baseURL, httpClient, err := restClientFor(restConfig)
if err != nil {
return nil, err
}
logrus.Debugf("URL: %#v", *baseURL)
m := imageNameRegexp.FindStringSubmatch(imageName)
if m == nil || len(m) != 4 {
return nil, fmt.Errorf("Invalid image reference %s, %#v", imageName, m)
}
return &openshiftClient{
baseURL: baseURL,
httpClient: httpClient,
bearerToken: restConfig.BearerToken,
username: restConfig.Username,
password: restConfig.Password,
namespace: m[1],
stream: m[2],
tag: m[3],
}, nil
}
// doRequest performs a correctly authenticated request to a specified path, and returns response body or an error object.
func (c *openshiftClient) doRequest(method, path string, requestBody []byte) ([]byte, error) {
url := *c.baseURL
url.Path = path
var requestBodyReader io.Reader
if requestBody != nil {
logrus.Debugf("Will send body: %s", requestBody)
requestBodyReader = bytes.NewReader(requestBody)
}
req, err := http.NewRequest(method, url.String(), requestBodyReader)
if err != nil {
return nil, err
}
if len(c.bearerToken) != 0 {
req.Header.Set("Authorization", "Bearer "+c.bearerToken)
} else if len(c.username) != 0 {
req.SetBasicAuth(c.username, c.password)
}
req.Header.Set("Accept", "application/json, */*")
req.Header.Set("User-Agent", fmt.Sprintf("skopeo/%s", version.Version))
if requestBody != nil {
req.Header.Set("Content-Type", "application/json")
}
logrus.Debugf("%s %s", method, url)
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
logrus.Debugf("Got body: %s", body)
// FIXME: Just throwing this useful information away only to try to guess later...
logrus.Debugf("Got content-type: %s", res.Header.Get("Content-Type"))
var status status
statusValid := false
if err := json.Unmarshal(body, &status); err == nil && len(status.Status) > 0 {
statusValid = true
}
switch {
case res.StatusCode == http.StatusSwitchingProtocols: // FIXME?! No idea why this weird case exists in k8s.io/kubernetes/pkg/client/restclient.
if statusValid && status.Status != "Success" {
return nil, errors.New(status.Message)
}
case res.StatusCode >= http.StatusOK && res.StatusCode <= http.StatusPartialContent:
// OK.
default:
if statusValid {
return nil, errors.New(status.Message)
}
return nil, fmt.Errorf("HTTP error: status code: %d, body: %s", res.StatusCode, string(body))
}
return body, nil
}
// canonicalDockerReference returns a canonical reference we use for signing OpenShift images.
// FIXME: This is, strictly speaking, a namespace conflict with images placed in a Docker registry running on the same host.
// Do we need to do something else, perhaps disambiguate (port number?) or namespace Docker and OpenShift separately?
func (c *openshiftClient) canonicalDockerReference() string {
return fmt.Sprintf("%s/%s/%s:%s", c.baseURL.Host, c.namespace, c.stream, c.tag)
}
// convertDockerImageReference takes an image API DockerImageReference value and returns a reference we can actually use;
// currently OpenShift stores the cluster-internal service IPs here, which are unusable from the outside.
func (c *openshiftClient) convertDockerImageReference(ref string) (string, error) {
parts := strings.SplitN(ref, "/", 2)
if len(parts) != 2 {
return "", fmt.Errorf("Invalid format of docker reference %s: missing '/'", ref)
}
// Sanity check that the reference is at least plausibly similar, i.e. uses the hard-coded port we expect.
if !strings.HasSuffix(parts[0], ":5000") {
return "", fmt.Errorf("Invalid format of docker reference %s: expecting port 5000", ref)
}
return c.dockerRegistryHostPart() + "/" + parts[1], nil
}
// dockerRegistryHostPart returns the host:port of the embedded Docker Registry API endpoint
// FIXME: There seems to be no way to discover the correct:host port using the API, so hard-code our knowledge
// about how the OpenShift Atomic Registry is configured, per examples/atomic-registry/run.sh:
// -p OPENSHIFT_OAUTH_PROVIDER_URL=https://${INSTALL_HOST}:8443,COCKPIT_KUBE_URL=https://${INSTALL_HOST},REGISTRY_HOST=${INSTALL_HOST}:5000
func (c *openshiftClient) dockerRegistryHostPart() string {
return strings.SplitN(c.baseURL.Host, ":", 2)[0] + ":5000"
}
type openshiftImageSource struct {
client *openshiftClient
// Values specific to this image
certPath string // Only for parseDockerImageSource
tlsVerify bool // Only for parseDockerImageSource
// State
docker types.ImageSource // The Docker Registry endpoint, or nil if not resolved yet
imageStreamImageName string // Resolved image identifier, or "" if not known yet
}
// NewOpenshiftImageSource creates a new ImageSource for the specified image and connection specification.
func NewOpenshiftImageSource(imageName, certPath string, tlsVerify bool) (types.ImageSource, error) {
client, err := newOpenshiftClient(imageName)
if err != nil {
return nil, err
}
return &openshiftImageSource{
client: client,
certPath: certPath,
tlsVerify: tlsVerify,
}, nil
}
// IntendedDockerReference returns the full, unambiguous, Docker reference for this image, _as specified by the user_
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
// May be "" if unknown.
func (s *openshiftImageSource) IntendedDockerReference() string {
return s.client.canonicalDockerReference()
}
func (s *openshiftImageSource) GetManifest(mimetypes []string) ([]byte, string, error) {
if err := s.ensureImageIsResolved(); err != nil {
return nil, "", err
}
return s.docker.GetManifest(mimetypes)
}
func (s *openshiftImageSource) GetLayer(digest string) (io.ReadCloser, error) {
if err := s.ensureImageIsResolved(); err != nil {
return nil, err
}
return s.docker.GetLayer(digest)
}
func (s *openshiftImageSource) GetSignatures() ([][]byte, error) {
return nil, nil
}
// ensureImageIsResolved sets up s.docker and s.imageStreamImageName
func (s *openshiftImageSource) ensureImageIsResolved() error {
if s.docker != nil {
return nil
}
// FIXME: validate components per validation.IsValidPathSegmentName?
path := fmt.Sprintf("/oapi/v1/namespaces/%s/imagestreams/%s", s.client.namespace, s.client.stream)
body, err := s.client.doRequest("GET", path, nil)
if err != nil {
return err
}
// Note: This does absolutely no kind/version checking or conversions.
var is imageStream
if err := json.Unmarshal(body, &is); err != nil {
return err
}
var te *tagEvent
for _, tag := range is.Status.Tags {
if tag.Tag != s.client.tag {
continue
}
if len(tag.Items) > 0 {
te = &tag.Items[0]
break
}
}
if te == nil {
return fmt.Errorf("No matching tag found")
}
logrus.Debugf("tag event %#v", te)
dockerRef, err := s.client.convertDockerImageReference(te.DockerImageReference)
if err != nil {
return err
}
logrus.Debugf("Resolved reference %#v", dockerRef)
d, err := docker.NewDockerImageSource(dockerRef, s.certPath, s.tlsVerify)
if err != nil {
return err
}
s.docker = d
s.imageStreamImageName = te.Image
return nil
}
type openshiftImageDestination struct {
client *openshiftClient
docker types.ImageDestination // The Docker Registry endpoint
}
// NewOpenshiftImageDestination creates a new ImageDestination for the specified image and connection specification.
func NewOpenshiftImageDestination(imageName, certPath string, tlsVerify bool) (types.ImageDestination, error) {
client, err := newOpenshiftClient(imageName)
if err != nil {
return nil, err
}
// FIXME: Should this always use a digest, not a tag? Uploading to Docker by tag requires the tag _inside_ the manifest to match,
// i.e. a single signed image cannot be available under multiple tags. But with types.ImageDestination, we don't know
// the manifest digest at this point.
dockerRef := fmt.Sprintf("%s/%s/%s:%s", client.dockerRegistryHostPart(), client.namespace, client.stream, client.tag)
docker, err := docker.NewDockerImageDestination(dockerRef, certPath, tlsVerify)
if err != nil {
return nil, err
}
return &openshiftImageDestination{
client: client,
docker: docker,
}, nil
}
func (d *openshiftImageDestination) CanonicalDockerReference() (string, error) {
return d.client.canonicalDockerReference(), nil
}
func (d *openshiftImageDestination) PutManifest(manifest []byte) error {
// Note: This does absolutely no kind/version checking or conversions.
manifestDigest, err := utils.ManifestDigest(manifest)
if err != nil {
return err
}
// FIXME: We can't do what respositorymiddleware.go does because we don't know the internal address. Does any of this matter?
dockerImageReference := fmt.Sprintf("%s/%s/%s@%s", d.client.dockerRegistryHostPart(), d.client.namespace, d.client.stream, manifestDigest)
ism := imageStreamMapping{
typeMeta: typeMeta{
Kind: "ImageStreamMapping",
APIVersion: "v1",
},
objectMeta: objectMeta{
Namespace: d.client.namespace,
Name: d.client.stream,
},
Image: image{
objectMeta: objectMeta{
Name: manifestDigest,
},
DockerImageReference: dockerImageReference,
DockerImageManifest: string(manifest),
},
Tag: d.client.tag,
}
body, err := json.Marshal(ism)
if err != nil {
return err
}
// FIXME: validate components per validation.IsValidPathSegmentName?
path := fmt.Sprintf("/oapi/v1/namespaces/%s/imagestreammappings", d.client.namespace)
body, err = d.client.doRequest("POST", path, body)
if err != nil {
return err
}
return d.docker.PutManifest(manifest)
}
func (d *openshiftImageDestination) PutLayer(digest string, stream io.Reader) error {
return d.docker.PutLayer(digest, stream)
}
func (d *openshiftImageDestination) PutSignatures(signatures [][]byte) error {
if len(signatures) != 0 {
return fmt.Errorf("Pushing signatures to an Atomic Registry is not supported")
}
return nil
}
// These structs are subsets of github.com/openshift/origin/pkg/image/api/v1 and its dependencies.
type imageStream struct {
Status imageStreamStatus `json:"status,omitempty"`
}
type imageStreamStatus struct {
DockerImageRepository string `json:"dockerImageRepository"`
Tags []namedTagEventList `json:"tags,omitempty"`
}
type namedTagEventList struct {
Tag string `json:"tag"`
Items []tagEvent `json:"items"`
}
type tagEvent struct {
DockerImageReference string `json:"dockerImageReference"`
Image string `json:"image"`
}
type imageStreamImage struct {
Image image `json:"image"`
}
type image struct {
objectMeta `json:"metadata,omitempty"`
DockerImageReference string `json:"dockerImageReference,omitempty"`
// DockerImageMetadata runtime.RawExtension `json:"dockerImageMetadata,omitempty"`
DockerImageMetadataVersion string `json:"dockerImageMetadataVersion,omitempty"`
DockerImageManifest string `json:"dockerImageManifest,omitempty"`
// DockerImageLayers []ImageLayer `json:"dockerImageLayers"`
}
type imageStreamMapping struct {
typeMeta `json:",inline"`
objectMeta `json:"metadata,omitempty"`
Image image `json:"image"`
Tag string `json:"tag"`
}
type typeMeta struct {
Kind string `json:"kind,omitempty"`
APIVersion string `json:"apiVersion,omitempty"`
}
type objectMeta struct {
Name string `json:"name,omitempty"`
GenerateName string `json:"generateName,omitempty"`
Namespace string `json:"namespace,omitempty"`
SelfLink string `json:"selfLink,omitempty"`
ResourceVersion string `json:"resourceVersion,omitempty"`
Generation int64 `json:"generation,omitempty"`
DeletionGracePeriodSeconds *int64 `json:"deletionGracePeriodSeconds,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}
// A subset of k8s.io/kubernetes/pkg/api/unversioned/Status
type status struct {
Status string `json:"status,omitempty"`
Message string `json:"message,omitempty"`
// Reason StatusReason `json:"reason,omitempty"`
// Details *StatusDetails `json:"details,omitempty"`
Code int32 `json:"code,omitempty"`
}

View File

@@ -1,43 +0,0 @@
// Note: Consider the API unstable until the code supports at least three different image formats or transports.
package signature
import (
"fmt"
"github.com/projectatomic/skopeo/docker/utils"
)
// SignDockerManifest returns a signature for manifest as the specified dockerReference,
// using mech and keyIdentity.
func SignDockerManifest(manifest []byte, dockerReference string, mech SigningMechanism, keyIdentity string) ([]byte, error) {
manifestDigest, err := utils.ManifestDigest(manifest)
if err != nil {
return nil, err
}
sig := privateSignature{
Signature{
DockerManifestDigest: manifestDigest,
DockerReference: dockerReference,
},
}
return sig.sign(mech, keyIdentity)
}
// VerifyDockerManifestSignature checks that unverifiedSignature uses expectedKeyIdentity to sign unverifiedManifest as expectedDockerReference,
// using mech.
func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byte,
expectedDockerReference string, mech SigningMechanism, expectedKeyIdentity string) (*Signature, error) {
expectedManifestDigest, err := utils.ManifestDigest(unverifiedManifest)
if err != nil {
return nil, err
}
sig, err := verifyAndExtractSignature(mech, unverifiedSignature, expectedKeyIdentity, expectedDockerReference)
if err != nil {
return nil, err
}
if sig.DockerManifestDigest != expectedManifestDigest {
return nil, InvalidSignatureError{msg: fmt.Sprintf("Docker manifest digest %s does not match %s", sig.DockerManifestDigest, expectedManifestDigest)}
}
return sig, nil
}

View File

@@ -1,79 +0,0 @@
package signature
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSignDockerManifest(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
manifest, err := ioutil.ReadFile("fixtures/image.manifest.json")
require.NoError(t, err)
// Successful signing
signature, err := SignDockerManifest(manifest, TestImageSignatureReference, mech, TestKeyFingerprint)
require.NoError(t, err)
verified, err := VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint)
assert.NoError(t, err)
assert.Equal(t, TestImageSignatureReference, verified.DockerReference)
assert.Equal(t, TestImageManifestDigest, verified.DockerManifestDigest)
// Error computing Docker manifest
invalidManifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json")
require.NoError(t, err)
_, err = SignDockerManifest(invalidManifest, TestImageSignatureReference, mech, TestKeyFingerprint)
assert.Error(t, err)
// Error creating blob to sign
_, err = SignDockerManifest(manifest, "", mech, TestKeyFingerprint)
assert.Error(t, err)
// Error signing
_, err = SignDockerManifest(manifest, TestImageSignatureReference, mech, "this fingerprint doesn't exist")
assert.Error(t, err)
}
func TestVerifyDockerManifestSignature(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
manifest, err := ioutil.ReadFile("fixtures/image.manifest.json")
require.NoError(t, err)
signature, err := ioutil.ReadFile("fixtures/image.signature")
require.NoError(t, err)
// Successful verification
sig, err := VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint)
require.NoError(t, err)
assert.Equal(t, TestImageSignatureReference, sig.DockerReference)
assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest)
// For extra paranoia, test that we return nil data on error.
// Error computing Docker manifest
invalidManifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json")
require.NoError(t, err)
sig, err = VerifyDockerManifestSignature(signature, invalidManifest, TestImageSignatureReference, mech, TestKeyFingerprint)
assert.Error(t, err)
assert.Nil(t, sig)
// Error verifying signature
corruptSignature, err := ioutil.ReadFile("fixtures/corrupt.signature")
sig, err = VerifyDockerManifestSignature(corruptSignature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint)
assert.Error(t, err)
assert.Nil(t, sig)
// Key fingerprint mismatch
sig, err = VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, "unexpected fingerprint")
assert.Error(t, err)
assert.Nil(t, sig)
// Docker manifest digest mismatch
sig, err = VerifyDockerManifestSignature(signature, []byte("unexpected manifest"), TestImageSignatureReference, mech, TestKeyFingerprint)
assert.Error(t, err)
assert.Nil(t, sig)
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,84 +0,0 @@
{
"default": [
{
"type": "reject"
}
],
"specific": {
"example.com/playground": [
{
"type": "insecureAcceptAnything"
}
],
"example.com/production": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/keys/employee-gpg-keyring"
}
],
"example.com/hardened": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/keys/employee-gpg-keyring",
"signedIdentity": {
"type": "matchRepository"
}
},
{
"type": "signedBy",
"keyType": "signedByGPGKeys",
"keyPath": "/keys/public-key-signing-gpg-keyring",
"signedIdentity": {
"type": "matchExact"
}
},
{
"type": "signedBaseLayer",
"baseLayerIdentity": {
"type": "exactRepository",
"dockerRepository": "registry.access.redhat.com/rhel7/rhel"
}
}
],
"example.com/hardened-x509": [
{
"type": "signedBy",
"keyType": "X509Certificates",
"keyPath": "/keys/employee-cert-file",
"signedIdentity": {
"type": "matchRepository"
}
},
{
"type": "signedBy",
"keyType": "signedByX509CAs",
"keyPath": "/keys/public-key-signing-ca-file"
}
],
"registry.access.redhat.com": [
{
"type": "signedBy",
"keyType": "signedByGPGKeys",
"keyPath": "/keys/RH-key-signing-key-gpg-keyring"
}
],
"bogus/key-data-example": [
{
"type": "signedBy",
"keyType": "signedByGPGKeys",
"keyData": "bm9uc2Vuc2U="
}
],
"bogus/signed-identity-example": [
{
"type": "signedBaseLayer",
"baseLayerIdentity": {
"type": "exactReference",
"dockerReference": "registry.access.redhat.com/rhel7/rhel:latest"
}
}
]
}
}

View File

@@ -1,19 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mI0EVurzqQEEAL3qkFq4K2URtSWVDYnQUNA9HdM9sqS2eAWfqUFMrkD5f+oN+LBL
tPyaE5GNLA0vXY7nHAM2TeM8ijZ/eMP17Raj64JL8GhCymL3wn2jNvb9XaF0R0s6
H0IaRPPu45A3SnxLwm4Orc/9Z7/UxtYjKSg9xOaTiVPzJgaf5Vm4J4ApABEBAAG0
EnNrb3BlbyB0ZXN0aW5nIGtleYi4BBMBAgAiBQJW6vOpAhsDBgsJCAcDAgYVCAIJ
CgsEFgIDAQIeAQIXgAAKCRDbcvIYi7RsyBbOBACgJFiKDlQ1UyvsNmGqJ7D0OpbS
1OppJlradKgZXyfahFswhFI+7ZREvELLHbinq3dBy5cLXRWzQKdJZNHknSN5Tjf2
0ipVBQuqpcBo+dnKiG4zH6fhTri7yeTZksIDfsqlI6FXDOdKLUSnahagEBn4yU+x
jHPvZk5SuuZv56A45biNBFbq86kBBADIC/9CsAlOmRALuYUmkhcqEjuFwn3wKz2d
IBjzgvro7zcVNNCgxQfMEjcUsvEh5cx13G3QQHcwOKy3M6Bv6VMhfZjd+1P1el4P
0fJS8GFmhWRBknMN8jFsgyohQeouQ798RFFv94KszfStNnr/ae8oao5URmoUXSCa
/MdUxn0YKwARAQABiJ8EGAECAAkFAlbq86kCGwwACgkQ23LyGIu0bMjUywQAq0dn
lUpDNSoLTcpNWuVvHQ7c/qmnE4TyiSLiRiAywdEWA6gMiyhUUucuGsEhMFP1WX1k
UNwArZ6UG7BDOUsvngP7jKGNqyUOQrq1s/r8D+0MrJGOWErGLlfttO2WeoijECkI
5qm8cXzAra3Xf/Z3VjxYTKSnNu37LtZkakdTdYE=
=tJAt
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,11 +0,0 @@
{
"schemaVersion": 1,
"name": "mitr/buxybox",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
],
"history": [
],
"signatures": 1
}

View File

@@ -1,10 +0,0 @@
package signature
const (
// TestImageManifestDigest is the Docker manifest digest of "image.manifest.json"
TestImageManifestDigest = "sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"
// TestImageSignatureReference is the Docker image reference signed in "image.signature"
TestImageSignatureReference = "testing/manifest"
// TestKeyFingerprint is the fingerprint of the private key in this directory.
TestKeyFingerprint = "1D8230F6CDB6A06716E414C1DB72F2188BB46CC8"
)

View File

@@ -1,149 +0,0 @@
package signature
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mSI map[string]interface{} // To minimize typing the long name
// A short-hand way to get a JSON object field value or panic. No error handling done, we know
// what we are working with, a panic in a test is good enough, and fitting test cases on a single line
// is a priority.
func x(m mSI, fields ...string) mSI {
for _, field := range fields {
// Not .(mSI) because type assertion of an unnamed type to a named type always fails (the types
// are not "identical"), but the assignment is fine because they are "assignable".
m = m[field].(map[string]interface{})
}
return m
}
func TestValidateExactMapKeys(t *testing.T) {
// Empty map and keys
err := validateExactMapKeys(mSI{})
assert.NoError(t, err)
// Success
err = validateExactMapKeys(mSI{"a": nil, "b": 1}, "b", "a")
assert.NoError(t, err)
// Extra map keys
err = validateExactMapKeys(mSI{"a": nil, "b": 1}, "a")
assert.Error(t, err)
// Extra expected keys
err = validateExactMapKeys(mSI{"a": 1}, "b", "a")
assert.Error(t, err)
// Unexpected key values
err = validateExactMapKeys(mSI{"a": 1}, "b")
assert.Error(t, err)
}
func TestMapField(t *testing.T) {
// Field not found
_, err := mapField(mSI{"a": mSI{}}, "b")
assert.Error(t, err)
// Field has a wrong type
_, err = mapField(mSI{"a": 1}, "a")
assert.Error(t, err)
// Success
// FIXME? We can't use mSI as the type of child, that type apparently can't be converted to the raw map type.
child := map[string]interface{}{"b": mSI{}}
m, err := mapField(mSI{"a": child, "b": nil}, "a")
require.NoError(t, err)
assert.Equal(t, child, m)
}
func TestStringField(t *testing.T) {
// Field not found
_, err := stringField(mSI{"a": "x"}, "b")
assert.Error(t, err)
// Field has a wrong type
_, err = stringField(mSI{"a": 1}, "a")
assert.Error(t, err)
// Success
s, err := stringField(mSI{"a": "x", "b": nil}, "a")
require.NoError(t, err)
assert.Equal(t, "x", s)
}
// implementsUnmarshalJSON is a minimalistic type used to detect that
// paranoidUnmarshalJSONObject uses the json.Unmarshaler interface of resolved
// pointers.
type implementsUnmarshalJSON bool
// Compile-time check that Policy implements json.Unmarshaler.
var _ json.Unmarshaler = (*implementsUnmarshalJSON)(nil)
func (dest *implementsUnmarshalJSON) UnmarshalJSON(data []byte) error {
_ = data // We don't care, not really.
*dest = true // Mark handler as called
return nil
}
func TestParanoidUnmarshalJSONObject(t *testing.T) {
type testStruct struct {
A string
B int
}
ts := testStruct{}
var unmarshalJSONCalled implementsUnmarshalJSON
tsResolver := func(key string) interface{} {
switch key {
case "a":
return &ts.A
case "b":
return &ts.B
case "implementsUnmarshalJSON":
return &unmarshalJSONCalled
default:
return nil
}
}
// Empty object
ts = testStruct{}
err := paranoidUnmarshalJSONObject([]byte(`{}`), tsResolver)
require.NoError(t, err)
assert.Equal(t, testStruct{}, ts)
// Success
ts = testStruct{}
err = paranoidUnmarshalJSONObject([]byte(`{"a":"x", "b":2}`), tsResolver)
require.NoError(t, err)
assert.Equal(t, testStruct{A: "x", B: 2}, ts)
// json.Unamarshaler is used for decoding values
ts = testStruct{}
unmarshalJSONCalled = implementsUnmarshalJSON(false)
err = paranoidUnmarshalJSONObject([]byte(`{"implementsUnmarshalJSON":true}`), tsResolver)
require.NoError(t, err)
assert.Equal(t, unmarshalJSONCalled, implementsUnmarshalJSON(true))
// Various kinds of invalid input
for _, input := range []string{
``, // Empty input
`&`, // Entirely invalid JSON
`1`, // Not an object
`{&}`, // Invalid key JSON
`{1:1}`, // Key not a string
`{"b":1, "b":1}`, // Duplicate key
`{"thisdoesnotexist":1}`, // Key rejected by resolver
`{"a":&}`, // Invalid value JSON
`{"a":1}`, // Type mismatch
`{"a":"value"}{}`, // Extra data after object
} {
ts = testStruct{}
err := paranoidUnmarshalJSONObject([]byte(input), tsResolver)
assert.Error(t, err)
}
}

View File

@@ -1,142 +0,0 @@
package signature
import (
"bytes"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testGPGHomeDirectory = "./fixtures"
)
func TestNewGPGSigningMechanism(t *testing.T) {
// A dumb test just for code coverage. We test more with newGPGSigningMechanismInDirectory().
_, err := NewGPGSigningMechanism()
assert.NoError(t, err)
}
func TestNewGPGSigningMechanismInDirectory(t *testing.T) {
// A dumb test just for code coverage.
_, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
assert.NoError(t, err)
// The various GPG failure cases are not obviously easy to reach.
}
func TestGPGSigningMechanismImportKeysFromBytes(t *testing.T) {
testDir, err := ioutil.TempDir("", "gpg-import-keys")
require.NoError(t, err)
defer os.RemoveAll(testDir)
mech, err := newGPGSigningMechanismInDirectory(testDir)
require.NoError(t, err)
// Try validating a signature when the key is unknown.
signature, err := ioutil.ReadFile("./fixtures/invalid-blob.signature")
require.NoError(t, err)
content, signingFingerprint, err := mech.Verify(signature)
require.Error(t, err)
// Successful import
keyBlob, err := ioutil.ReadFile("./fixtures/public-key.gpg")
require.NoError(t, err)
keyIdentities, err := mech.ImportKeysFromBytes(keyBlob)
require.NoError(t, err)
assert.Equal(t, []string{TestKeyFingerprint}, keyIdentities)
// After import, the signature should validate.
content, signingFingerprint, err = mech.Verify(signature)
require.NoError(t, err)
assert.Equal(t, []byte("This is not JSON\n"), content)
assert.Equal(t, TestKeyFingerprint, signingFingerprint)
// Two keys: just concatenate the valid input twice.
keyIdentities, err = mech.ImportKeysFromBytes(bytes.Join([][]byte{keyBlob, keyBlob}, nil))
require.NoError(t, err)
assert.Equal(t, []string{TestKeyFingerprint, TestKeyFingerprint}, keyIdentities)
// Invalid input: This is accepted anyway by GPG, just returns no keys.
keyIdentities, err = mech.ImportKeysFromBytes([]byte("This is invalid"))
require.NoError(t, err)
assert.Equal(t, []string{}, keyIdentities)
// The various GPG/GPGME failures cases are not obviously easy to reach.
}
func TestGPGSigningMechanismSign(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
// Successful signing
content := []byte("content")
signature, err := mech.Sign(content, TestKeyFingerprint)
require.NoError(t, err)
signedContent, signingFingerprint, err := mech.Verify(signature)
require.NoError(t, err)
assert.EqualValues(t, content, signedContent)
assert.Equal(t, TestKeyFingerprint, signingFingerprint)
// Error signing
_, err = mech.Sign(content, "this fingerprint doesn't exist")
assert.Error(t, err)
// The various GPG/GPGME failures cases are not obviously easy to reach.
}
func assertSigningError(t *testing.T, content []byte, fingerprint string, err error) {
assert.Error(t, err)
assert.Nil(t, content)
assert.Empty(t, fingerprint)
}
func TestGPGSigningMechanismVerify(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
// Successful verification
signature, err := ioutil.ReadFile("./fixtures/invalid-blob.signature")
require.NoError(t, err)
content, signingFingerprint, err := mech.Verify(signature)
require.NoError(t, err)
assert.Equal(t, []byte("This is not JSON\n"), content)
assert.Equal(t, TestKeyFingerprint, signingFingerprint)
// For extra paranoia, test that we return nil data on error.
// Completely invalid signature.
content, signingFingerprint, err = mech.Verify([]byte{})
assertSigningError(t, content, signingFingerprint, err)
content, signingFingerprint, err = mech.Verify([]byte("invalid signature"))
assertSigningError(t, content, signingFingerprint, err)
// Literal packet, not a signature
signature, err = ioutil.ReadFile("./fixtures/unsigned-literal.signature")
require.NoError(t, err)
content, signingFingerprint, err = mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err)
// Encrypted data, not a signature.
signature, err = ioutil.ReadFile("./fixtures/unsigned-encrypted.signature")
require.NoError(t, err)
content, signingFingerprint, err = mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err)
// FIXME? Is there a way to create a multi-signature so that gpgme_op_verify returns multiple signatures?
// Expired signature
signature, err = ioutil.ReadFile("./fixtures/expired.signature")
require.NoError(t, err)
content, signingFingerprint, err = mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err)
// Corrupt signature
signature, err = ioutil.ReadFile("./fixtures/corrupt.signature")
require.NoError(t, err)
content, signingFingerprint, err = mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err)
// The various GPG/GPGME failures cases are not obviously easy to reach.
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,206 +0,0 @@
package signature
import (
"encoding/json"
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInvalidSignatureError(t *testing.T) {
// A stupid test just to keep code coverage
s := "test"
err := InvalidSignatureError{msg: s}
assert.Equal(t, s, err.Error())
}
func TestMarshalJSON(t *testing.T) {
// Empty string values
s := privateSignature{Signature{DockerManifestDigest: "", DockerReference: "_"}}
_, err := s.MarshalJSON()
assert.Error(t, err)
s = privateSignature{Signature{DockerManifestDigest: "_", DockerReference: ""}}
_, err = s.MarshalJSON()
assert.Error(t, err)
// Success
s = privateSignature{Signature{DockerManifestDigest: "digest!@#", DockerReference: "reference#@!"}}
marshaled, err := s.marshalJSONWithVariables(0, "CREATOR")
require.NoError(t, err)
assert.Equal(t, []byte("{\"critical\":{\"identity\":{\"docker-reference\":\"reference#@!\"},\"image\":{\"docker-manifest-digest\":\"digest!@#\"},\"type\":\"atomic container signature\"},\"optional\":{\"creator\":\"CREATOR\",\"timestamp\":0}}"),
marshaled)
// We can't test MarshalJSON directly because the timestamp will keep changing, so just test that
// it doesn't fail. And call it through the JSON package for a good measure.
_, err = json.Marshal(s)
assert.NoError(t, err)
}
// Return the result of modifying validJSON with fn and unmarshaling it into *sig
func tryUnmarshalModifiedSignature(t *testing.T, sig *privateSignature, validJSON []byte, modifyFn func(mSI)) error {
var tmp mSI
err := json.Unmarshal(validJSON, &tmp)
require.NoError(t, err)
modifyFn(tmp)
testJSON, err := json.Marshal(tmp)
require.NoError(t, err)
*sig = privateSignature{}
return json.Unmarshal(testJSON, sig)
}
func TestUnmarshalJSON(t *testing.T) {
var s privateSignature
// Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our
// UnmarshalJSON implementation; so test that first, then test our error handling for completeness.
err := json.Unmarshal([]byte("&"), &s)
assert.Error(t, err)
err = s.UnmarshalJSON([]byte("&"))
assert.Error(t, err)
// Not an object
err = json.Unmarshal([]byte("1"), &s)
assert.Error(t, err)
// Start with a valid JSON.
validSig := privateSignature{
Signature{
DockerManifestDigest: "digest!@#",
DockerReference: "reference#@!",
},
}
validJSON, err := validSig.MarshalJSON()
require.NoError(t, err)
// Success
s = privateSignature{}
err = json.Unmarshal(validJSON, &s)
require.NoError(t, err)
assert.Equal(t, validSig, s)
// Various ways to corrupt the JSON
breakFns := []func(mSI){
// A top-level field is missing
func(v mSI) { delete(v, "critical") },
func(v mSI) { delete(v, "optional") },
// Extra top-level sub-object
func(v mSI) { v["unexpected"] = 1 },
// "critical" not an object
func(v mSI) { v["critical"] = 1 },
// "optional" not an object
func(v mSI) { v["optional"] = 1 },
// A field of "critical" is missing
func(v mSI) { delete(x(v, "critical"), "type") },
func(v mSI) { delete(x(v, "critical"), "image") },
func(v mSI) { delete(x(v, "critical"), "identity") },
// Extra field of "critical"
func(v mSI) { x(v, "critical")["unexpected"] = 1 },
// Invalid "type"
func(v mSI) { x(v, "critical")["type"] = 1 },
func(v mSI) { x(v, "critical")["type"] = "unexpected" },
// Invalid "image" object
func(v mSI) { x(v, "critical")["image"] = 1 },
func(v mSI) { delete(x(v, "critical", "image"), "docker-manifest-digest") },
func(v mSI) { x(v, "critical", "image")["unexpected"] = 1 },
// Invalid "docker-manifest-digest"
func(v mSI) { x(v, "critical", "image")["docker-manifest-digest"] = 1 },
// Invalid "identity" object
func(v mSI) { x(v, "critical")["identity"] = 1 },
func(v mSI) { delete(x(v, "critical", "identity"), "docker-reference") },
func(v mSI) { x(v, "critical", "identity")["unexpected"] = 1 },
// Invalid "docker-reference"
func(v mSI) { x(v, "critical", "identity")["docker-reference"] = 1 },
}
for _, fn := range breakFns {
err = tryUnmarshalModifiedSignature(t, &s, validJSON, fn)
assert.Error(t, err)
}
// Modifications to "optional" are allowed and ignored
allowedModificationFns := []func(mSI){
// Add an optional field
func(v mSI) { x(v, "optional")["unexpected"] = 1 },
// Delete an optional field
func(v mSI) { delete(x(v, "optional"), "creator") },
}
for _, fn := range allowedModificationFns {
err = tryUnmarshalModifiedSignature(t, &s, validJSON, fn)
require.NoError(t, err)
assert.Equal(t, validSig, s)
}
}
func TestSign(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
sig := privateSignature{
Signature{
DockerManifestDigest: "digest!@#",
DockerReference: "reference#@!",
},
}
// Successful signing
signature, err := sig.sign(mech, TestKeyFingerprint)
require.NoError(t, err)
verified, err := verifyAndExtractSignature(mech, signature, TestKeyFingerprint, sig.DockerReference)
require.NoError(t, err)
assert.Equal(t, sig.Signature, *verified)
// Error creating blob to sign
_, err = privateSignature{}.sign(mech, TestKeyFingerprint)
assert.Error(t, err)
// Error signing
_, err = sig.sign(mech, "this fingerprint doesn't exist")
assert.Error(t, err)
}
func TestVerifyAndExtractSignature(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
signature, err := ioutil.ReadFile("./fixtures/image.signature")
require.NoError(t, err)
// Successful verification
sig, err := verifyAndExtractSignature(mech, signature, TestKeyFingerprint, TestImageSignatureReference)
require.NoError(t, err)
assert.Equal(t, TestImageSignatureReference, sig.DockerReference)
assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest)
// For extra paranoia, test that we return a nil signature object on error.
// Completely invalid signature.
sig, err = verifyAndExtractSignature(mech, []byte{}, TestKeyFingerprint, TestImageSignatureReference)
assert.Error(t, err)
assert.Nil(t, sig)
sig, err = verifyAndExtractSignature(mech, []byte("invalid signature"), TestKeyFingerprint, TestImageSignatureReference)
assert.Error(t, err)
assert.Nil(t, sig)
// Valid signature of non-JSON
invalidBlobSignature, err := ioutil.ReadFile("./fixtures/invalid-blob.signature")
require.NoError(t, err)
sig, err = verifyAndExtractSignature(mech, invalidBlobSignature, TestKeyFingerprint, TestImageSignatureReference)
assert.Error(t, err)
assert.Nil(t, sig)
// Valid signature with a wrong key
sig, err = verifyAndExtractSignature(mech, signature, "unexpected fingerprint", TestImageSignatureReference)
assert.Error(t, err)
assert.Nil(t, sig)
// Valid signature with a wrong image reference
sig, err = verifyAndExtractSignature(mech, signature, TestKeyFingerprint, "unexpected docker reference")
assert.Error(t, err)
assert.Nil(t, sig)
}

View File

@@ -1,76 +0,0 @@
package types
import (
"io"
"time"
)
// Registry is a service providing repositories.
type Registry interface {
Repositories() []Repository
Repository(ref string) Repository
Lookup(term string) []Image // docker registry v1 only AFAICT, v2 can be built hacking with Images()
}
// Repository is a set of images.
type Repository interface {
Images() []Image
Image(ref string) Image // ref == image name w/o registry part
}
// ImageSource is a service, possibly remote (= slow), to download components of a single image.
// This is primarily useful for copying images around; for examining their properties, Image (below)
// is usually more useful.
type ImageSource interface {
// IntendedDockerReference returns the full, unambiguous, Docker reference for this image, _as specified by the user_
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
// May be "" if unknown.
IntendedDockerReference() string
// GetManifest returns the image's manifest along with its MIME type. The empty string is returned if the MIME type is unknown. The slice parameter indicates the supported mime types the manifest should be when getting it.
// It may use a remote (= slow) service.
GetManifest([]string) ([]byte, string, error)
// Note: Calling GetLayer() may have ordering dependencies WRT other methods of this type. FIXME: How does this work with (docker save) on stdin?
GetLayer(digest string) (io.ReadCloser, error)
// GetSignatures returns the image's signatures. It may use a remote (= slow) service.
GetSignatures() ([][]byte, error)
}
// ImageDestination is a service, possibly remote (= slow), to store components of a single image.
type ImageDestination interface {
// CanonicalDockerReference returns the full, unambiguous, Docker reference for this image (even if the user referred to the image using some shorthand notation).
CanonicalDockerReference() (string, error)
// FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
PutManifest([]byte) error
// Note: Calling PutLayer() and other methods may have ordering dependencies WRT other methods of this type. FIXME: Figure out and document.
PutLayer(digest string, stream io.Reader) error
PutSignatures(signatures [][]byte) error
}
// Image is the primary API for inspecting properties of images.
type Image interface {
// ref to repository?
// IntendedDockerReference returns the full, unambiguous, Docker reference for this image, _as specified by the user_
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
// May be "" if unknown.
IntendedDockerReference() string
// Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need.
// FIXME? This should also return a MIME type if known, to differentiate between schema versions.
Manifest() ([]byte, error)
// Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
Signatures() ([][]byte, error)
Layers(layers ...string) error // configure download directory? Call it DownloadLayers?
// Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration.
Inspect() (*ImageInspectInfo, error)
DockerTar() ([]byte, error) // ??? also, configure output directory
}
// ImageInspectInfo is a set of metadata describing Docker images, primarily their manifest and configuration.
type ImageInspectInfo struct {
Tag string
Created time.Time
DockerVersion string
Labels map[string]string
Architecture string
Os string
Layers []string
}

View File

@@ -1,6 +0,0 @@
language: go
go: 1.1
script:
- go vet ./...
- go test -v ./...

View File

@@ -1,280 +0,0 @@
[![Build Status](https://travis-ci.org/codegangsta/cli.png?branch=master)](https://travis-ci.org/codegangsta/cli)
# cli.go
cli.go is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way.
You can view the API docs here:
http://godoc.org/github.com/codegangsta/cli
## Overview
Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app.
This is where cli.go comes into play. cli.go makes command line programming fun, organized, and expressive!
## Installation
Make sure you have a working Go environment (go 1.1 is *required*). [See the install instructions](http://golang.org/doc/install.html).
To install cli.go, simply run:
```
$ go get github.com/codegangsta/cli
```
Make sure your PATH includes to the `$GOPATH/bin` directory so your commands can be easily used:
```
export PATH=$PATH:$GOPATH/bin
```
## Getting Started
One of the philosophies behind cli.go is that an API should be playful and full of discovery. So a cli.go app can be as little as one line of code in `main()`.
``` go
package main
import (
"os"
"github.com/codegangsta/cli"
)
func main() {
cli.NewApp().Run(os.Args)
}
```
This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation:
``` go
package main
import (
"os"
"github.com/codegangsta/cli"
)
func main() {
app := cli.NewApp()
app.Name = "boom"
app.Usage = "make an explosive entrance"
app.Action = func(c *cli.Context) {
println("boom! I say!")
}
app.Run(os.Args)
}
```
Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered below.
## Example
Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness!
``` go
/* greet.go */
package main
import (
"os"
"github.com/codegangsta/cli"
)
func main() {
app := cli.NewApp()
app.Name = "greet"
app.Usage = "fight the loneliness!"
app.Action = func(c *cli.Context) {
println("Hello friend!")
}
app.Run(os.Args)
}
```
Install our command to the `$GOPATH/bin` directory:
```
$ go install
```
Finally run our new command:
```
$ greet
Hello friend!
```
cli.go also generates some bitchass help text:
```
$ greet help
NAME:
greet - fight the loneliness!
USAGE:
greet [global options] command [command options] [arguments...]
VERSION:
0.0.0
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS
--version Shows version information
```
### Arguments
You can lookup arguments by calling the `Args` function on cli.Context.
``` go
...
app.Action = func(c *cli.Context) {
println("Hello", c.Args()[0])
}
...
```
### Flags
Setting and querying flags is simple.
``` go
...
app.Flags = []cli.Flag {
cli.StringFlag{
Name: "lang",
Value: "english",
Usage: "language for the greeting",
},
}
app.Action = func(c *cli.Context) {
name := "someone"
if len(c.Args()) > 0 {
name = c.Args()[0]
}
if c.String("lang") == "spanish" {
println("Hola", name)
} else {
println("Hello", name)
}
}
...
```
#### Alternate Names
You can set alternate (or short) names for flags by providing a comma-delimited list for the Name. e.g.
``` go
app.Flags = []cli.Flag {
cli.StringFlag{
Name: "lang, l",
Value: "english",
Usage: "language for the greeting",
},
}
```
#### Values from the Environment
You can also have the default value set from the environment via EnvVar. e.g.
``` go
app.Flags = []cli.Flag {
cli.StringFlag{
Name: "lang, l",
Value: "english",
Usage: "language for the greeting",
EnvVar: "APP_LANG",
},
}
```
That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error.
### Subcommands
Subcommands can be defined for a more git-like command line app.
```go
...
app.Commands = []cli.Command{
{
Name: "add",
ShortName: "a",
Usage: "add a task to the list",
Action: func(c *cli.Context) {
println("added task: ", c.Args().First())
},
},
{
Name: "complete",
ShortName: "c",
Usage: "complete a task on the list",
Action: func(c *cli.Context) {
println("completed task: ", c.Args().First())
},
},
{
Name: "template",
ShortName: "r",
Usage: "options for task templates",
Subcommands: []cli.Command{
{
Name: "add",
Usage: "add a new template",
Action: func(c *cli.Context) {
println("new task template: ", c.Args().First())
},
},
{
Name: "remove",
Usage: "remove an existing template",
Action: func(c *cli.Context) {
println("removed task template: ", c.Args().First())
},
},
},
},
}
...
```
### Bash Completion
You can enable completion commands by setting the EnableBashCompletion
flag on the App object. By default, this setting will only auto-complete to
show an app's subcommands, but you can write your own completion methods for
the App or its subcommands.
```go
...
var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"}
app := cli.NewApp()
app.EnableBashCompletion = true
app.Commands = []cli.Command{
{
Name: "complete",
ShortName: "c",
Usage: "complete a task on the list",
Action: func(c *cli.Context) {
println("completed task: ", c.Args().First())
},
BashComplete: func(c *cli.Context) {
// This will complete if no args are passed
if len(c.Args()) > 0 {
return
}
for _, t := range tasks {
println(t)
}
},
}
}
...
```
#### To Enable
Source the autocomplete/bash_autocomplete file in your .bashrc file while
setting the PROG variable to the name of your program:
`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete`
## About
cli.go is written by none other than the [Code Gangsta](http://codegangsta.io)

View File

@@ -1,248 +0,0 @@
package cli
import (
"fmt"
"io/ioutil"
"os"
"time"
)
// App is the main structure of a cli application. It is recomended that
// and app be created with the cli.NewApp() function
type App struct {
// The name of the program. Defaults to os.Args[0]
Name string
// Description of the program.
Usage string
// Version of the program
Version string
// List of commands to execute
Commands []Command
// List of flags to parse
Flags []Flag
// Boolean to enable bash completion commands
EnableBashCompletion bool
// Boolean to hide built-in help command
HideHelp bool
// An action to execute when the bash-completion flag is set
BashComplete func(context *Context)
// An action to execute before any subcommands are run, but after the context is ready
// If a non-nil error is returned, no subcommands are run
Before func(context *Context) error
// The action to execute when no subcommands are specified
Action func(context *Context)
// Execute this function if the proper command cannot be found
CommandNotFound func(context *Context, command string)
// Compilation date
Compiled time.Time
// Author
Author string
// Author e-mail
Email string
}
// Tries to find out when this binary was compiled.
// Returns the current time if it fails to find it.
func compileTime() time.Time {
info, err := os.Stat(os.Args[0])
if err != nil {
return time.Now()
}
return info.ModTime()
}
// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action.
func NewApp() *App {
return &App{
Name: os.Args[0],
Usage: "A new cli application",
Version: "0.0.0",
BashComplete: DefaultAppComplete,
Action: helpCommand.Action,
Compiled: compileTime(),
Author: "Author",
Email: "unknown@email",
}
}
// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination
func (a *App) Run(arguments []string) error {
// append help to commands
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
a.appendFlag(HelpFlag)
}
//append version/help flags
if a.EnableBashCompletion {
a.appendFlag(BashCompletionFlag)
}
a.appendFlag(VersionFlag)
// parse flags
set := flagSet(a.Name, a.Flags)
set.SetOutput(ioutil.Discard)
err := set.Parse(arguments[1:])
nerr := normalizeFlags(a.Flags, set)
if nerr != nil {
fmt.Println(nerr)
context := NewContext(a, set, set)
ShowAppHelp(context)
fmt.Println("")
return nerr
}
context := NewContext(a, set, set)
if err != nil {
fmt.Printf("Incorrect Usage.\n\n")
ShowAppHelp(context)
fmt.Println("")
return err
}
if checkCompletions(context) {
return nil
}
if checkHelp(context) {
return nil
}
if checkVersion(context) {
return nil
}
if a.Before != nil {
err := a.Before(context)
if err != nil {
return err
}
}
args := context.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
}
}
// Run default Action
a.Action(context)
return nil
}
// Another entry point to the cli app, takes care of passing arguments and error handling
func (a *App) RunAndExitOnError() {
if err := a.Run(os.Args); err != nil {
os.Stderr.WriteString(fmt.Sprintln(err))
os.Exit(1)
}
}
// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags
func (a *App) RunAsSubcommand(ctx *Context) error {
// append help to commands
if len(a.Commands) > 0 {
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
a.appendFlag(HelpFlag)
}
}
// append flags
if a.EnableBashCompletion {
a.appendFlag(BashCompletionFlag)
}
// parse flags
set := flagSet(a.Name, a.Flags)
set.SetOutput(ioutil.Discard)
err := set.Parse(ctx.Args().Tail())
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, ctx.globalSet)
if nerr != nil {
fmt.Println(nerr)
if len(a.Commands) > 0 {
ShowSubcommandHelp(context)
} else {
ShowCommandHelp(ctx, context.Args().First())
}
fmt.Println("")
return nerr
}
if err != nil {
fmt.Printf("Incorrect Usage.\n\n")
ShowSubcommandHelp(context)
return err
}
if checkCompletions(context) {
return nil
}
if len(a.Commands) > 0 {
if checkSubcommandHelp(context) {
return nil
}
} else {
if checkCommandHelp(ctx, context.Args().First()) {
return nil
}
}
if a.Before != nil {
err := a.Before(context)
if err != nil {
return err
}
}
args := context.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
}
}
// Run default Action
if len(a.Commands) > 0 {
a.Action(context)
} else {
a.Action(ctx)
}
return nil
}
// Returns the named command on App. Returns nil if the command does not exist
func (a *App) Command(name string) *Command {
for _, c := range a.Commands {
if c.HasName(name) {
return &c
}
}
return nil
}
func (a *App) hasFlag(flag Flag) bool {
for _, f := range a.Flags {
if flag == f {
return true
}
}
return false
}
func (a *App) appendFlag(flag Flag) {
if !a.hasFlag(flag) {
a.Flags = append(a.Flags, flag)
}
}

View File

@@ -1,141 +0,0 @@
package cli
import (
"fmt"
"io/ioutil"
"strings"
)
// Command is a subcommand for a cli.App.
type Command struct {
// The name of the command
Name string
// short name of the command. Typically one character
ShortName string
// A short description of the usage of this command
Usage string
// A longer explanation of how the command works
Description string
// The function to call when checking for bash command completions
BashComplete func(context *Context)
// An action to execute before any sub-subcommands are run, but after the context is ready
// If a non-nil error is returned, no sub-subcommands are run
Before func(context *Context) error
// The function to call when this command is invoked
Action func(context *Context)
// List of child commands
Subcommands []Command
// List of flags to parse
Flags []Flag
// Treat all flags as normal arguments if true
SkipFlagParsing bool
// Boolean to hide built-in help command
HideHelp bool
}
// Invokes the command given the context, parses ctx.Args() to generate command-specific flags
func (c Command) Run(ctx *Context) error {
if len(c.Subcommands) > 0 || c.Before != nil {
return c.startApp(ctx)
}
if !c.HideHelp {
// append help to flags
c.Flags = append(
c.Flags,
HelpFlag,
)
}
if ctx.App.EnableBashCompletion {
c.Flags = append(c.Flags, BashCompletionFlag)
}
set := flagSet(c.Name, c.Flags)
set.SetOutput(ioutil.Discard)
firstFlagIndex := -1
for index, arg := range ctx.Args() {
if strings.HasPrefix(arg, "-") {
firstFlagIndex = index
break
}
}
var err error
if firstFlagIndex > -1 && !c.SkipFlagParsing {
args := ctx.Args()
regularArgs := args[1:firstFlagIndex]
flagArgs := args[firstFlagIndex:]
err = set.Parse(append(flagArgs, regularArgs...))
} else {
err = set.Parse(ctx.Args().Tail())
}
if err != nil {
fmt.Printf("Incorrect Usage.\n\n")
ShowCommandHelp(ctx, c.Name)
fmt.Println("")
return err
}
nerr := normalizeFlags(c.Flags, set)
if nerr != nil {
fmt.Println(nerr)
fmt.Println("")
ShowCommandHelp(ctx, c.Name)
fmt.Println("")
return nerr
}
context := NewContext(ctx.App, set, ctx.globalSet)
if checkCommandCompletions(context, c.Name) {
return nil
}
if checkCommandHelp(context, c.Name) {
return nil
}
context.Command = c
c.Action(context)
return nil
}
// Returns true if Command.Name or Command.ShortName matches given name
func (c Command) HasName(name string) bool {
return c.Name == name || c.ShortName == name
}
func (c Command) startApp(ctx *Context) error {
app := NewApp()
// set the name and usage
app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name)
if c.Description != "" {
app.Usage = c.Description
} else {
app.Usage = c.Usage
}
// set the flags and commands
app.Commands = c.Subcommands
app.Flags = c.Flags
app.HideHelp = c.HideHelp
// bash completion
app.EnableBashCompletion = ctx.App.EnableBashCompletion
if c.BashComplete != nil {
app.BashComplete = c.BashComplete
}
// set the actions
app.Before = c.Before
if c.Action != nil {
app.Action = c.Action
} else {
app.Action = helpSubcommand.Action
}
return app.RunAsSubcommand(ctx)
}

View File

@@ -1,280 +0,0 @@
package cli
import (
"errors"
"flag"
"strconv"
"strings"
)
// Context is a type that is passed through to
// each Handler action in a cli application. Context
// can be used to retrieve context-specific Args and
// parsed command-line options.
type Context struct {
App *App
Command Command
flagSet *flag.FlagSet
globalSet *flag.FlagSet
setFlags map[string]bool
}
// Creates a new context. For use in when invoking an App or Command action.
func NewContext(app *App, set *flag.FlagSet, globalSet *flag.FlagSet) *Context {
return &Context{App: app, flagSet: set, globalSet: globalSet}
}
// Looks up the value of a local int flag, returns 0 if no int flag exists
func (c *Context) Int(name string) int {
return lookupInt(name, c.flagSet)
}
// Looks up the value of a local float64 flag, returns 0 if no float64 flag exists
func (c *Context) Float64(name string) float64 {
return lookupFloat64(name, c.flagSet)
}
// Looks up the value of a local bool flag, returns false if no bool flag exists
func (c *Context) Bool(name string) bool {
return lookupBool(name, c.flagSet)
}
// Looks up the value of a local boolT flag, returns false if no bool flag exists
func (c *Context) BoolT(name string) bool {
return lookupBoolT(name, c.flagSet)
}
// Looks up the value of a local string flag, returns "" if no string flag exists
func (c *Context) String(name string) string {
return lookupString(name, c.flagSet)
}
// Looks up the value of a local string slice flag, returns nil if no string slice flag exists
func (c *Context) StringSlice(name string) []string {
return lookupStringSlice(name, c.flagSet)
}
// Looks up the value of a local int slice flag, returns nil if no int slice flag exists
func (c *Context) IntSlice(name string) []int {
return lookupIntSlice(name, c.flagSet)
}
// Looks up the value of a local generic flag, returns nil if no generic flag exists
func (c *Context) Generic(name string) interface{} {
return lookupGeneric(name, c.flagSet)
}
// Looks up the value of a global int flag, returns 0 if no int flag exists
func (c *Context) GlobalInt(name string) int {
return lookupInt(name, c.globalSet)
}
// Looks up the value of a global bool flag, returns false if no bool flag exists
func (c *Context) GlobalBool(name string) bool {
return lookupBool(name, c.globalSet)
}
// Looks up the value of a global string flag, returns "" if no string flag exists
func (c *Context) GlobalString(name string) string {
return lookupString(name, c.globalSet)
}
// Looks up the value of a global string slice flag, returns nil if no string slice flag exists
func (c *Context) GlobalStringSlice(name string) []string {
return lookupStringSlice(name, c.globalSet)
}
// Looks up the value of a global int slice flag, returns nil if no int slice flag exists
func (c *Context) GlobalIntSlice(name string) []int {
return lookupIntSlice(name, c.globalSet)
}
// Looks up the value of a global generic flag, returns nil if no generic flag exists
func (c *Context) GlobalGeneric(name string) interface{} {
return lookupGeneric(name, c.globalSet)
}
// Determines if the flag was actually set exists
func (c *Context) IsSet(name string) bool {
if c.setFlags == nil {
c.setFlags = make(map[string]bool)
c.flagSet.Visit(func(f *flag.Flag) {
c.setFlags[f.Name] = true
})
}
return c.setFlags[name] == true
}
type Args []string
// Returns the command line arguments associated with the context.
func (c *Context) Args() Args {
args := Args(c.flagSet.Args())
return args
}
// Returns the nth argument, or else a blank string
func (a Args) Get(n int) string {
if len(a) > n {
return a[n]
}
return ""
}
// Returns the first argument, or else a blank string
func (a Args) First() string {
return a.Get(0)
}
// Return the rest of the arguments (not the first one)
// or else an empty string slice
func (a Args) Tail() []string {
if len(a) >= 2 {
return []string(a)[1:]
}
return []string{}
}
// Checks if there are any arguments present
func (a Args) Present() bool {
return len(a) != 0
}
// Swaps arguments at the given indexes
func (a Args) Swap(from, to int) error {
if from >= len(a) || to >= len(a) {
return errors.New("index out of range")
}
a[from], a[to] = a[to], a[from]
return nil
}
func lookupInt(name string, set *flag.FlagSet) int {
f := set.Lookup(name)
if f != nil {
val, err := strconv.Atoi(f.Value.String())
if err != nil {
return 0
}
return val
}
return 0
}
func lookupFloat64(name string, set *flag.FlagSet) float64 {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseFloat(f.Value.String(), 64)
if err != nil {
return 0
}
return val
}
return 0
}
func lookupString(name string, set *flag.FlagSet) string {
f := set.Lookup(name)
if f != nil {
return f.Value.String()
}
return ""
}
func lookupStringSlice(name string, set *flag.FlagSet) []string {
f := set.Lookup(name)
if f != nil {
return (f.Value.(*StringSlice)).Value()
}
return nil
}
func lookupIntSlice(name string, set *flag.FlagSet) []int {
f := set.Lookup(name)
if f != nil {
return (f.Value.(*IntSlice)).Value()
}
return nil
}
func lookupGeneric(name string, set *flag.FlagSet) interface{} {
f := set.Lookup(name)
if f != nil {
return f.Value
}
return nil
}
func lookupBool(name string, set *flag.FlagSet) bool {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseBool(f.Value.String())
if err != nil {
return false
}
return val
}
return false
}
func lookupBoolT(name string, set *flag.FlagSet) bool {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseBool(f.Value.String())
if err != nil {
return true
}
return val
}
return false
}
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
switch ff.Value.(type) {
case *StringSlice:
default:
set.Set(name, ff.Value.String())
}
}
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
visited := make(map[string]bool)
set.Visit(func(f *flag.Flag) {
visited[f.Name] = true
})
for _, f := range flags {
parts := strings.Split(f.getName(), ",")
if len(parts) == 1 {
continue
}
var ff *flag.Flag
for _, name := range parts {
name = strings.Trim(name, " ")
if visited[name] {
if ff != nil {
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
}
ff = set.Lookup(name)
}
}
if ff == nil {
continue
}
for _, name := range parts {
name = strings.Trim(name, " ")
if !visited[name] {
copyFlag(name, ff, set)
}
}
}
return nil
}

View File

@@ -1,379 +0,0 @@
package cli
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
)
// This flag enables bash-completion for all commands and subcommands
var BashCompletionFlag = BoolFlag{
Name: "generate-bash-completion",
}
// This flag prints the version for the application
var VersionFlag = BoolFlag{
Name: "version, v",
Usage: "print the version",
}
// This flag prints the help for all commands and subcommands
var HelpFlag = BoolFlag{
Name: "help, h",
Usage: "show help",
}
// Flag is a common interface related to parsing flags in cli.
// For more advanced flag parsing techniques, it is recomended that
// this interface be implemented.
type Flag interface {
fmt.Stringer
// Apply Flag settings to the given flag set
Apply(*flag.FlagSet)
getName() string
}
func flagSet(name string, flags []Flag) *flag.FlagSet {
set := flag.NewFlagSet(name, flag.ContinueOnError)
for _, f := range flags {
f.Apply(set)
}
return set
}
func eachName(longName string, fn func(string)) {
parts := strings.Split(longName, ",")
for _, name := range parts {
name = strings.Trim(name, " ")
fn(name)
}
}
// Generic is a generic parseable type identified by a specific flag
type Generic interface {
Set(value string) error
String() string
}
// GenericFlag is the flag type for types implementing Generic
type GenericFlag struct {
Name string
Value Generic
Usage string
EnvVar string
}
func (f GenericFlag) String() string {
return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s %v\t`%v` %s", prefixFor(f.Name), f.Name, f.Value, "-"+f.Name+" option -"+f.Name+" option", f.Usage))
}
func (f GenericFlag) Apply(set *flag.FlagSet) {
val := f.Value
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
val.Set(envVal)
}
}
eachName(f.Name, func(name string) {
set.Var(f.Value, name, f.Usage)
})
}
func (f GenericFlag) getName() string {
return f.Name
}
type StringSlice []string
func (f *StringSlice) Set(value string) error {
*f = append(*f, value)
return nil
}
func (f *StringSlice) String() string {
return fmt.Sprintf("%s", *f)
}
func (f *StringSlice) Value() []string {
return *f
}
type StringSliceFlag struct {
Name string
Value *StringSlice
Usage string
EnvVar string
}
func (f StringSliceFlag) String() string {
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
pref := prefixFor(firstName)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage))
}
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
newVal := &StringSlice{}
for _, s := range strings.Split(envVal, ",") {
newVal.Set(s)
}
f.Value = newVal
}
}
eachName(f.Name, func(name string) {
set.Var(f.Value, name, f.Usage)
})
}
func (f StringSliceFlag) getName() string {
return f.Name
}
type IntSlice []int
func (f *IntSlice) Set(value string) error {
tmp, err := strconv.Atoi(value)
if err != nil {
return err
} else {
*f = append(*f, tmp)
}
return nil
}
func (f *IntSlice) String() string {
return fmt.Sprintf("%d", *f)
}
func (f *IntSlice) Value() []int {
return *f
}
type IntSliceFlag struct {
Name string
Value *IntSlice
Usage string
EnvVar string
}
func (f IntSliceFlag) String() string {
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
pref := prefixFor(firstName)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage))
}
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
newVal := &IntSlice{}
for _, s := range strings.Split(envVal, ",") {
err := newVal.Set(s)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
}
}
f.Value = newVal
}
}
eachName(f.Name, func(name string) {
set.Var(f.Value, name, f.Usage)
})
}
func (f IntSliceFlag) getName() string {
return f.Name
}
type BoolFlag struct {
Name string
Usage string
EnvVar string
}
func (f BoolFlag) String() string {
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage))
}
func (f BoolFlag) Apply(set *flag.FlagSet) {
val := false
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
envValBool, err := strconv.ParseBool(envVal)
if err == nil {
val = envValBool
}
}
}
eachName(f.Name, func(name string) {
set.Bool(name, val, f.Usage)
})
}
func (f BoolFlag) getName() string {
return f.Name
}
type BoolTFlag struct {
Name string
Usage string
EnvVar string
}
func (f BoolTFlag) String() string {
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage))
}
func (f BoolTFlag) Apply(set *flag.FlagSet) {
val := true
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
envValBool, err := strconv.ParseBool(envVal)
if err == nil {
val = envValBool
}
}
}
eachName(f.Name, func(name string) {
set.Bool(name, val, f.Usage)
})
}
func (f BoolTFlag) getName() string {
return f.Name
}
type StringFlag struct {
Name string
Value string
Usage string
EnvVar string
}
func (f StringFlag) String() string {
var fmtString string
fmtString = "%s %v\t%v"
if len(f.Value) > 0 {
fmtString = "%s '%v'\t%v"
} else {
fmtString = "%s %v\t%v"
}
return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage))
}
func (f StringFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
f.Value = envVal
}
}
eachName(f.Name, func(name string) {
set.String(name, f.Value, f.Usage)
})
}
func (f StringFlag) getName() string {
return f.Name
}
type IntFlag struct {
Name string
Value int
Usage string
EnvVar string
}
func (f IntFlag) String() string {
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage))
}
func (f IntFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
envValInt, err := strconv.ParseUint(envVal, 10, 64)
if err == nil {
f.Value = int(envValInt)
}
}
}
eachName(f.Name, func(name string) {
set.Int(name, f.Value, f.Usage)
})
}
func (f IntFlag) getName() string {
return f.Name
}
type Float64Flag struct {
Name string
Value float64
Usage string
EnvVar string
}
func (f Float64Flag) String() string {
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage))
}
func (f Float64Flag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
envValFloat, err := strconv.ParseFloat(envVal, 10)
if err == nil {
f.Value = float64(envValFloat)
}
}
}
eachName(f.Name, func(name string) {
set.Float64(name, f.Value, f.Usage)
})
}
func (f Float64Flag) getName() string {
return f.Name
}
func prefixFor(name string) (prefix string) {
if len(name) == 1 {
prefix = "-"
} else {
prefix = "--"
}
return
}
func prefixedNames(fullName string) (prefixed string) {
parts := strings.Split(fullName, ",")
for i, name := range parts {
name = strings.Trim(name, " ")
prefixed += prefixFor(name) + name
if i < len(parts)-1 {
prefixed += ", "
}
}
return
}
func withEnvHint(envVar, str string) string {
envText := ""
if envVar != "" {
envText = fmt.Sprintf(" [$%s]", envVar)
}
return str + envText
}

View File

@@ -1,213 +0,0 @@
package cli
import (
"fmt"
"os"
"text/tabwriter"
"text/template"
)
// The text template for the Default help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.Name}} {{ if .Flags }}[global options] {{ end }}command{{ if .Flags }} [command options]{{ end }} [arguments...]
VERSION:
{{.Version}}
COMMANDS:
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
{{end}}{{ if .Flags }}
GLOBAL OPTIONS:
{{range .Flags}}{{.}}
{{end}}{{ end }}
`
// The text template for the command help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var CommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
command {{.Name}}{{ if .Flags }} [command options]{{ end }} [arguments...]
DESCRIPTION:
{{.Description}}{{ if .Flags }}
OPTIONS:
{{range .Flags}}{{.}}
{{end}}{{ end }}
`
// The text template for the subcommand help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var SubcommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.Name}} command{{ if .Flags }} [command options]{{ end }} [arguments...]
COMMANDS:
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
{{end}}{{ if .Flags }}
OPTIONS:
{{range .Flags}}{{.}}
{{end}}{{ end }}
`
var helpCommand = Command{
Name: "help",
ShortName: "h",
Usage: "Shows a list of commands or help for one command",
Action: func(c *Context) {
args := c.Args()
if args.Present() {
ShowCommandHelp(c, args.First())
} else {
ShowAppHelp(c)
}
},
}
var helpSubcommand = Command{
Name: "help",
ShortName: "h",
Usage: "Shows a list of commands or help for one command",
Action: func(c *Context) {
args := c.Args()
if args.Present() {
ShowCommandHelp(c, args.First())
} else {
ShowSubcommandHelp(c)
}
},
}
// Prints help for the App
var HelpPrinter = printHelp
func ShowAppHelp(c *Context) {
HelpPrinter(AppHelpTemplate, c.App)
}
// Prints the list of subcommands as the default app completion method
func DefaultAppComplete(c *Context) {
for _, command := range c.App.Commands {
fmt.Println(command.Name)
if command.ShortName != "" {
fmt.Println(command.ShortName)
}
}
}
// Prints help for the given command
func ShowCommandHelp(c *Context, command string) {
for _, c := range c.App.Commands {
if c.HasName(command) {
HelpPrinter(CommandHelpTemplate, c)
return
}
}
if c.App.CommandNotFound != nil {
c.App.CommandNotFound(c, command)
} else {
fmt.Printf("No help topic for '%v'\n", command)
}
}
// Prints help for the given subcommand
func ShowSubcommandHelp(c *Context) {
HelpPrinter(SubcommandHelpTemplate, c.App)
}
// Prints the version number of the App
func ShowVersion(c *Context) {
fmt.Printf("%v version %v\n", c.App.Name, c.App.Version)
}
// Prints the lists of commands within a given context
func ShowCompletions(c *Context) {
a := c.App
if a != nil && a.BashComplete != nil {
a.BashComplete(c)
}
}
// Prints the custom completions for a given command
func ShowCommandCompletions(ctx *Context, command string) {
c := ctx.App.Command(command)
if c != nil && c.BashComplete != nil {
c.BashComplete(ctx)
}
}
func printHelp(templ string, data interface{}) {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
t := template.Must(template.New("help").Parse(templ))
err := t.Execute(w, data)
if err != nil {
panic(err)
}
w.Flush()
}
func checkVersion(c *Context) bool {
if c.GlobalBool("version") {
ShowVersion(c)
return true
}
return false
}
func checkHelp(c *Context) bool {
if c.GlobalBool("h") || c.GlobalBool("help") {
ShowAppHelp(c)
return true
}
return false
}
func checkCommandHelp(c *Context, name string) bool {
if c.Bool("h") || c.Bool("help") {
ShowCommandHelp(c, name)
return true
}
return false
}
func checkSubcommandHelp(c *Context) bool {
if c.GlobalBool("h") || c.GlobalBool("help") {
ShowSubcommandHelp(c)
return true
}
return false
}
func checkCompletions(c *Context) bool {
if c.GlobalBool(BashCompletionFlag.Name) && c.App.EnableBashCompletion {
ShowCompletions(c)
return true
}
return false
}
func checkCommandCompletions(c *Context, name string) bool {
if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion {
ShowCommandCompletions(c, name)
return true
}
return false
}

189
vendor/github.com/containers/image/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,189 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

353
vendor/github.com/containers/image/copy/copy.go generated vendored Normal file
View File

@@ -0,0 +1,353 @@
package copy
import (
"bytes"
"compress/gzip"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
"io/ioutil"
"reflect"
"strings"
pb "gopkg.in/cheggaaa/pb.v1"
"github.com/Sirupsen/logrus"
"github.com/containers/image/image"
"github.com/containers/image/signature"
"github.com/containers/image/transports"
"github.com/containers/image/types"
)
// supportedDigests lists the supported blob digest types.
var supportedDigests = map[string]func() hash.Hash{
"sha256": sha256.New,
}
type digestingReader struct {
source io.Reader
digest hash.Hash
expectedDigest []byte
validationFailed bool
}
// newDigestingReader returns an io.Reader implementation with contents of source, which will eventually return a non-EOF error
// and set validationFailed to true if the source stream does not match expectedDigestString.
func newDigestingReader(source io.Reader, expectedDigestString string) (*digestingReader, error) {
fields := strings.SplitN(expectedDigestString, ":", 2)
if len(fields) != 2 {
return nil, fmt.Errorf("Invalid digest specification %s", expectedDigestString)
}
fn, ok := supportedDigests[fields[0]]
if !ok {
return nil, fmt.Errorf("Invalid digest specification %s: unknown digest type %s", expectedDigestString, fields[0])
}
digest := fn()
expectedDigest, err := hex.DecodeString(fields[1])
if err != nil {
return nil, fmt.Errorf("Invalid digest value %s: %v", expectedDigestString, err)
}
if len(expectedDigest) != digest.Size() {
return nil, fmt.Errorf("Invalid digest specification %s: length %d does not match %d", expectedDigestString, len(expectedDigest), digest.Size())
}
return &digestingReader{
source: source,
digest: digest,
expectedDigest: expectedDigest,
validationFailed: false,
}, nil
}
func (d *digestingReader) Read(p []byte) (int, error) {
n, err := d.source.Read(p)
if n > 0 {
if n2, err := d.digest.Write(p[:n]); n2 != n || err != nil {
// Coverage: This should not happen, the hash.Hash interface requires
// d.digest.Write to never return an error, and the io.Writer interface
// requires n2 == len(input) if no error is returned.
return 0, fmt.Errorf("Error updating digest during verification: %d vs. %d, %v", n2, n, err)
}
}
if err == io.EOF {
actualDigest := d.digest.Sum(nil)
if subtle.ConstantTimeCompare(actualDigest, d.expectedDigest) != 1 {
d.validationFailed = true
return 0, fmt.Errorf("Digest did not match, expected %s, got %s", hex.EncodeToString(d.expectedDigest), hex.EncodeToString(actualDigest))
}
}
return n, err
}
// Options allows supplying non-default configuration modifying the behavior of CopyImage.
type Options struct {
RemoveSignatures bool // Remove any pre-existing signatures. SignBy will still add a new signature.
SignBy string // If non-empty, asks for a signature to be added during the copy, and specifies a key ID, as accepted by signature.NewGPGSigningMechanism().SignDockerManifest(),
ReportWriter io.Writer
}
// Image copies image from srcRef to destRef, using policyContext to validate source image admissibility.
func Image(ctx *types.SystemContext, policyContext *signature.PolicyContext, destRef, srcRef types.ImageReference, options *Options) error {
reportWriter := options.ReportWriter
if reportWriter == nil {
reportWriter = ioutil.Discard
}
writeReport := func(f string, a ...interface{}) {
fmt.Fprintf(reportWriter, f, a...)
}
dest, err := destRef.NewImageDestination(ctx)
if err != nil {
return fmt.Errorf("Error initializing destination %s: %v", transports.ImageName(destRef), err)
}
defer dest.Close()
rawSource, err := srcRef.NewImageSource(ctx, dest.SupportedManifestMIMETypes())
if err != nil {
return fmt.Errorf("Error initializing source %s: %v", transports.ImageName(srcRef), err)
}
src := image.FromSource(rawSource)
defer src.Close()
// Please keep this policy check BEFORE reading any other information about the image.
if allowed, err := policyContext.IsRunningImageAllowed(src); !allowed || err != nil { // Be paranoid and fail if either return value indicates so.
return fmt.Errorf("Source image rejected: %v", err)
}
writeReport("Getting image source manifest\n")
manifest, _, err := src.Manifest()
if err != nil {
return fmt.Errorf("Error reading manifest: %v", err)
}
var sigs [][]byte
if options != nil && options.RemoveSignatures {
sigs = [][]byte{}
} else {
writeReport("Getting image source signatures\n")
s, err := src.Signatures()
if err != nil {
return fmt.Errorf("Error reading signatures: %v", err)
}
sigs = s
}
if len(sigs) != 0 {
writeReport("Checking if image destination supports signatures\n")
if err := dest.SupportsSignatures(); err != nil {
return fmt.Errorf("Can not copy signatures: %v", err)
}
}
canModifyManifest := len(sigs) == 0
writeReport("Getting image source configuration\n")
srcConfigInfo, err := src.ConfigInfo()
if err != nil {
return fmt.Errorf("Error parsing manifest: %v", err)
}
if srcConfigInfo.Digest != "" {
writeReport("Uploading blob %s\n", srcConfigInfo.Digest)
destConfigInfo, err := copyBlob(dest, rawSource, srcConfigInfo, false, options.ReportWriter)
if err != nil {
return err
}
if destConfigInfo.Digest != srcConfigInfo.Digest {
return fmt.Errorf("Internal error: copying uncompressed config blob %s changed digest to %s", srcConfigInfo.Digest, destConfigInfo.Digest)
}
}
srcLayerInfos, err := src.LayerInfos()
if err != nil {
return fmt.Errorf("Error parsing manifest: %v", err)
}
destLayerInfos := []types.BlobInfo{}
copiedLayers := map[string]types.BlobInfo{}
for _, srcLayer := range srcLayerInfos {
destLayer, ok := copiedLayers[srcLayer.Digest]
if !ok {
writeReport("Uploading blob %s\n", srcLayer.Digest)
destLayer, err = copyBlob(dest, rawSource, srcLayer, canModifyManifest, options.ReportWriter)
if err != nil {
return err
}
copiedLayers[srcLayer.Digest] = destLayer
}
destLayerInfos = append(destLayerInfos, destLayer)
}
manifestUpdates := types.ManifestUpdateOptions{}
if layerDigestsDiffer(srcLayerInfos, destLayerInfos) {
manifestUpdates.LayerInfos = destLayerInfos
}
if !reflect.DeepEqual(manifestUpdates, types.ManifestUpdateOptions{}) {
if !canModifyManifest {
return fmt.Errorf("Internal error: copy needs an updated manifest but that was known to be forbidden")
}
manifest, err = src.UpdatedManifest(manifestUpdates)
if err != nil {
return fmt.Errorf("Error creating an updated manifest: %v", err)
}
}
if options != nil && options.SignBy != "" {
mech, err := signature.NewGPGSigningMechanism()
if err != nil {
return fmt.Errorf("Error initializing GPG: %v", err)
}
dockerReference := dest.Reference().DockerReference()
if dockerReference == nil {
return fmt.Errorf("Cannot determine canonical Docker reference for destination %s", transports.ImageName(dest.Reference()))
}
writeReport("Signing manifest\n")
newSig, err := signature.SignDockerManifest(manifest, dockerReference.String(), mech, options.SignBy)
if err != nil {
return fmt.Errorf("Error creating signature: %v", err)
}
sigs = append(sigs, newSig)
}
writeReport("Uploading manifest to image destination\n")
if err := dest.PutManifest(manifest); err != nil {
return fmt.Errorf("Error writing manifest: %v", err)
}
writeReport("Storing signatures\n")
if err := dest.PutSignatures(sigs); err != nil {
return fmt.Errorf("Error writing signatures: %v", err)
}
if err := dest.Commit(); err != nil {
return fmt.Errorf("Error committing the finished image: %v", err)
}
return nil
}
// layerDigestsDiffer return true iff the digests in a and b differ (ignoring sizes and possible other fields)
func layerDigestsDiffer(a, b []types.BlobInfo) bool {
if len(a) != len(b) {
return true
}
for i := range a {
if a[i].Digest != b[i].Digest {
return true
}
}
return false
}
// copyBlob copies a blob with srcInfo (with known Digest and possibly known Size) in src to dest, perhaps compressing it if canCompress,
// and returns a complete blobInfo of the copied blob.
func copyBlob(dest types.ImageDestination, src types.ImageSource, srcInfo types.BlobInfo, canCompress bool, reportWriter io.Writer) (types.BlobInfo, error) {
srcStream, srcBlobSize, err := src.GetBlob(srcInfo.Digest) // We currently completely ignore srcInfo.Size throughout.
if err != nil {
return types.BlobInfo{}, fmt.Errorf("Error reading blob %s: %v", srcInfo.Digest, err)
}
defer srcStream.Close()
// Be paranoid; in case PutBlob somehow managed to ignore an error from digestingReader,
// use a separate validation failure indicator.
// Note that we don't use a stronger "validationSucceeded" indicator, because
// dest.PutBlob may detect that the layer already exists, in which case we don't
// read stream to the end, and validation does not happen.
digestingReader, err := newDigestingReader(srcStream, srcInfo.Digest)
if err != nil {
return types.BlobInfo{}, fmt.Errorf("Error preparing to verify blob %s: %v", srcInfo.Digest, err)
}
var destStream io.Reader = digestingReader
isCompressed, destStream, err := isStreamCompressed(destStream) // We could skip this in some cases, but let's keep the code path uniform
if err != nil {
return types.BlobInfo{}, fmt.Errorf("Error reading blob %s: %v", srcInfo.Digest, err)
}
bar := pb.New(int(srcInfo.Size)).SetUnits(pb.U_BYTES)
bar.Output = reportWriter
bar.SetMaxWidth(80)
bar.ShowTimeLeft = false
bar.ShowPercent = false
bar.Start()
destStream = bar.NewProxyReader(destStream)
defer fmt.Fprint(reportWriter, "\n")
var inputInfo types.BlobInfo
if !canCompress || isCompressed || !dest.ShouldCompressLayers() {
logrus.Debugf("Using original blob without modification")
inputInfo.Digest = srcInfo.Digest
inputInfo.Size = srcBlobSize
} else {
logrus.Debugf("Compressing blob on the fly")
pipeReader, pipeWriter := io.Pipe()
defer pipeReader.Close()
// If this fails while writing data, it will do pipeWriter.CloseWithError(); if it fails otherwise,
// e.g. because we have exited and due to pipeReader.Close() above further writing to the pipe has failed,
// we dont care.
go compressGoroutine(pipeWriter, destStream) // Closes pipeWriter
destStream = pipeReader
inputInfo.Digest = ""
inputInfo.Size = -1
}
uploadedInfo, err := dest.PutBlob(destStream, inputInfo)
if err != nil {
return types.BlobInfo{}, fmt.Errorf("Error writing blob: %v", err)
}
if digestingReader.validationFailed { // Coverage: This should never happen.
return types.BlobInfo{}, fmt.Errorf("Internal error uploading blob %s, digest verification failed but was ignored", srcInfo.Digest)
}
if inputInfo.Digest != "" && uploadedInfo.Digest != inputInfo.Digest {
return types.BlobInfo{}, fmt.Errorf("Internal error uploading blob %s, blob with digest %s uploaded with digest %s", srcInfo.Digest, inputInfo.Digest, uploadedInfo.Digest)
}
return uploadedInfo, nil
}
// compressionPrefixes is an internal implementation detail of isStreamCompressed
var compressionPrefixes = map[string][]byte{
"gzip": {0x1F, 0x8B, 0x08}, // gzip (RFC 1952)
"bzip2": {0x42, 0x5A, 0x68}, // bzip2 (decompress.c:BZ2_decompress)
"xz": {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, // xz (/usr/share/doc/xz/xz-file-format.txt)
}
// isStreamCompressed returns true if input is recognized as a compressed format.
// Because it consumes the start of input, other consumers must use the returned io.Reader instead to also read from the beginning.
func isStreamCompressed(input io.Reader) (bool, io.Reader, error) {
buffer := [8]byte{}
n, err := io.ReadAtLeast(input, buffer[:], len(buffer))
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
// This is a “real” error. We could just ignore it this time, process the data we have, and hope that the source will report the same error again.
// Instead, fail immediately with the original error cause instead of a possibly secondary/misleading error returned later.
return false, nil, err
}
isCompressed := false
for algo, prefix := range compressionPrefixes {
if bytes.HasPrefix(buffer[:n], prefix) {
logrus.Debugf("Detected compression format %s", algo)
isCompressed = true
break
}
}
if !isCompressed {
logrus.Debugf("No compression detected")
}
return isCompressed, io.MultiReader(bytes.NewReader(buffer[:n]), input), nil
}
// compressGoroutine reads all input from src and writes its compressed equivalent to dest.
func compressGoroutine(dest *io.PipeWriter, src io.Reader) {
err := errors.New("Internal error: unexpected panic in compressGoroutine")
defer func() { // Note that this is not the same as {defer dest.CloseWithError(err)}; we need err to be evaluated lazily.
dest.CloseWithError(err) // CloseWithError(nil) is equivalent to Close()
}()
zipper := gzip.NewWriter(dest)
defer zipper.Close()
_, err = io.Copy(zipper, src) // Sets err to nil, i.e. causes dest.Close()
}

View File

@@ -0,0 +1,111 @@
package directory
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/containers/image/types"
)
type dirImageDestination struct {
ref dirReference
}
// newImageDestination returns an ImageDestination for writing to an existing directory.
func newImageDestination(ref dirReference) types.ImageDestination {
return &dirImageDestination{ref}
}
// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent,
// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects.
func (d *dirImageDestination) Reference() types.ImageReference {
return d.ref
}
// Close removes resources associated with an initialized ImageDestination, if any.
func (d *dirImageDestination) Close() {
}
func (d *dirImageDestination) SupportedManifestMIMETypes() []string {
return nil
}
// SupportsSignatures returns an error (to be displayed to the user) if the destination certainly can't store signatures.
// Note: It is still possible for PutSignatures to fail if SupportsSignatures returns nil.
func (d *dirImageDestination) SupportsSignatures() error {
return nil
}
// ShouldCompressLayers returns true iff it is desirable to compress layer blobs written to this destination.
func (d *dirImageDestination) ShouldCompressLayers() bool {
return false
}
// PutBlob writes contents of stream and returns data representing the result (with all data filled in).
// 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.
// 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.
func (d *dirImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobInfo) (types.BlobInfo, error) {
blobFile, err := ioutil.TempFile(d.ref.path, "dir-put-blob")
if err != nil {
return types.BlobInfo{}, err
}
succeeded := false
defer func() {
blobFile.Close()
if !succeeded {
os.Remove(blobFile.Name())
}
}()
h := sha256.New()
tee := io.TeeReader(stream, h)
size, err := io.Copy(blobFile, tee)
if err != nil {
return types.BlobInfo{}, err
}
computedDigest := hex.EncodeToString(h.Sum(nil))
if inputInfo.Size != -1 && size != inputInfo.Size {
return types.BlobInfo{}, fmt.Errorf("Size mismatch when copying %s, expected %d, got %d", computedDigest, inputInfo.Size, size)
}
if err := blobFile.Sync(); err != nil {
return types.BlobInfo{}, err
}
if err := blobFile.Chmod(0644); err != nil {
return types.BlobInfo{}, err
}
blobPath := d.ref.layerPath(computedDigest)
if err := os.Rename(blobFile.Name(), blobPath); err != nil {
return types.BlobInfo{}, err
}
succeeded = true
return types.BlobInfo{Digest: "sha256:" + computedDigest, Size: size}, nil
}
func (d *dirImageDestination) PutManifest(manifest []byte) error {
return ioutil.WriteFile(d.ref.manifestPath(), manifest, 0644)
}
func (d *dirImageDestination) PutSignatures(signatures [][]byte) error {
for i, sig := range signatures {
if err := ioutil.WriteFile(d.ref.signaturePath(i), sig, 0644); err != nil {
return err
}
}
return nil
}
// Commit marks the process of storing the image as successful and asks for the image to be persisted.
// WARNING: This does not have any transactional semantics:
// - Uploaded data MAY be visible to others before Commit() is called
// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed)
func (d *dirImageDestination) Commit() error {
return nil
}

View File

@@ -0,0 +1,66 @@
package directory
import (
"io"
"io/ioutil"
"os"
"github.com/containers/image/types"
)
type dirImageSource struct {
ref dirReference
}
// newImageSource returns an ImageSource reading from an existing directory.
// The caller must call .Close() on the returned ImageSource.
func newImageSource(ref dirReference) types.ImageSource {
return &dirImageSource{ref}
}
// Reference returns the reference used to set up this source, _as specified by the user_
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
func (s *dirImageSource) Reference() types.ImageReference {
return s.ref
}
// Close removes resources associated with an initialized ImageSource, if any.
func (s *dirImageSource) Close() {
}
// it's up to the caller to determine the MIME type of the returned manifest's bytes
func (s *dirImageSource) GetManifest() ([]byte, string, error) {
m, err := ioutil.ReadFile(s.ref.manifestPath())
if err != nil {
return nil, "", err
}
return m, "", err
}
// GetBlob returns a stream for the specified blob, and the blobs size (or -1 if unknown).
func (s *dirImageSource) GetBlob(digest string) (io.ReadCloser, int64, error) {
r, err := os.Open(s.ref.layerPath(digest))
if err != nil {
return nil, 0, nil
}
fi, err := r.Stat()
if err != nil {
return nil, 0, nil
}
return r, fi.Size(), nil
}
func (s *dirImageSource) GetSignatures() ([][]byte, error) {
signatures := [][]byte{}
for i := 0; ; i++ {
signature, err := ioutil.ReadFile(s.ref.signaturePath(i))
if err != nil {
if os.IsNotExist(err) {
break
}
return nil, err
}
signatures = append(signatures, signature)
}
return signatures, nil
}

View File

@@ -0,0 +1,170 @@
package directory
import (
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/containers/image/directory/explicitfilepath"
"github.com/containers/image/image"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
// Transport is an ImageTransport for directory paths.
var Transport = dirTransport{}
type dirTransport struct{}
func (t dirTransport) Name() string {
return "dir"
}
// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference.
func (t dirTransport) ParseReference(reference string) (types.ImageReference, error) {
return NewReference(reference)
}
// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
func (t dirTransport) ValidatePolicyConfigurationScope(scope string) error {
if !strings.HasPrefix(scope, "/") {
return fmt.Errorf("Invalid scope %s: Must be an absolute path", scope)
}
// Refuse also "/", otherwise "/" and "" would have the same semantics,
// and "" could be unexpectedly shadowed by the "/" entry.
if scope == "/" {
return errors.New(`Invalid scope "/": Use the generic default scope ""`)
}
cleaned := filepath.Clean(scope)
if cleaned != scope {
return fmt.Errorf(`Invalid scope %s: Uses non-canonical format, perhaps try %s`, scope, cleaned)
}
return nil
}
// dirReference is an ImageReference for directory paths.
type dirReference struct {
// Note that the interpretation of paths below depends on the underlying filesystem state, which may change under us at any time!
// Either of the paths may point to a different, or no, inode over time. resolvedPath may contain symbolic links, and so on.
// Generally we follow the intent of the user, and use the "path" member for filesystem operations (e.g. the user can use a relative path to avoid
// being exposed to symlinks and renames in the parent directories to the working directory).
// (But in general, we make no attempt to be completely safe against concurrent hostile filesystem modifications.)
path string // As specified by the user. May be relative, contain symlinks, etc.
resolvedPath string // Absolute path with no symlinks, at least at the time of its creation. Primarily used for policy namespaces.
}
// There is no directory.ParseReference because it is rather pointless.
// Callers who need a transport-independent interface will go through
// dirTransport.ParseReference; callers who intentionally deal with directories
// can use directory.NewReference.
// NewReference returns a directory reference for a specified path.
//
// We do not expose an API supplying the resolvedPath; we could, but recomputing it
// is generally cheap enough that we prefer being confident about the properties of resolvedPath.
func NewReference(path string) (types.ImageReference, error) {
resolved, err := explicitfilepath.ResolvePathToFullyExplicit(path)
if err != nil {
return nil, err
}
return dirReference{path: path, resolvedPath: resolved}, nil
}
func (ref dirReference) Transport() types.ImageTransport {
return Transport
}
// StringWithinTransport returns a string representation of the reference, which MUST be such that
// reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference.
// NOTE: The returned string is not promised to be equal to the original input to ParseReference;
// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa.
// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix.
func (ref dirReference) StringWithinTransport() string {
return ref.path
}
// DockerReference returns a Docker reference associated with this reference
// (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent,
// not e.g. after redirect or alias processing), or nil if unknown/not applicable.
func (ref dirReference) DockerReference() reference.Named {
return nil
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref dirReference) PolicyConfigurationIdentity() string {
return ref.resolvedPath
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref dirReference) PolicyConfigurationNamespaces() []string {
res := []string{}
path := ref.resolvedPath
for {
lastSlash := strings.LastIndex(path, "/")
if lastSlash == -1 || lastSlash == 0 {
break
}
path = path[:lastSlash]
res = append(res, path)
}
// Note that we do not include "/"; it is redundant with the default "" global default,
// and rejected by dirTransport.ValidatePolicyConfigurationScope above.
return res
}
// NewImage returns a types.Image for this reference.
// The caller must call .Close() on the returned Image.
func (ref dirReference) NewImage(ctx *types.SystemContext) (types.Image, error) {
src := newImageSource(ref)
return image.FromSource(src), nil
}
// NewImageSource returns a types.ImageSource for this reference,
// asking the backend to use a manifest from requestedManifestMIMETypes if possible.
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
// The caller must call .Close() on the returned ImageSource.
func (ref dirReference) NewImageSource(ctx *types.SystemContext, requestedManifestMIMETypes []string) (types.ImageSource, error) {
return newImageSource(ref), nil
}
// 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
}
// DeleteImage deletes the named image from the registry, if supported.
func (ref dirReference) DeleteImage(ctx *types.SystemContext) error {
return fmt.Errorf("Deleting images not implemented for dir: images")
}
// manifestPath returns a path for the manifest within a directory using our conventions.
func (ref dirReference) manifestPath() string {
return filepath.Join(ref.path, "manifest.json")
}
// layerPath returns a path for a layer tarball within a directory using our conventions.
func (ref dirReference) layerPath(digest string) string {
// FIXME: Should we keep the digest identification?
return filepath.Join(ref.path, strings.TrimPrefix(digest, "sha256:")+".tar")
}
// signaturePath returns a path for a signature within a directory using our conventions.
func (ref dirReference) signaturePath(index int) string {
return filepath.Join(ref.path, fmt.Sprintf("signature-%d", index+1))
}

View File

@@ -0,0 +1,55 @@
package explicitfilepath
import (
"fmt"
"os"
"path/filepath"
)
// ResolvePathToFullyExplicit returns the input path converted to an absolute, no-symlinks, cleaned up path.
// To do so, all elements of the input path must exist; as a special case, the final component may be
// a non-existent name (but not a symlink pointing to a non-existent name)
// This is intended as a a helper for implementations of types.ImageReference.PolicyConfigurationIdentity etc.
func ResolvePathToFullyExplicit(path string) (string, error) {
switch _, err := os.Lstat(path); {
case err == nil:
return resolveExistingPathToFullyExplicit(path)
case os.IsNotExist(err):
parent, file := filepath.Split(path)
resolvedParent, err := resolveExistingPathToFullyExplicit(parent)
if err != nil {
return "", err
}
if file == "." || file == ".." {
// Coverage: This can happen, but very rarely: if we have successfully resolved the parent, both "." and ".." in it should have been resolved as well.
// This can still happen if there is a filesystem race condition, causing the Lstat() above to fail but the later resolution to succeed.
// We do not care to promise anything if such filesystem race conditions can happen, but we definitely don't want to return "."/".." components
// in the resulting path, and especially not at the end.
return "", fmt.Errorf("Unexpectedly missing special filename component in %s", path)
}
resolvedPath := filepath.Join(resolvedParent, file)
// As a sanity check, ensure that there are no "." or ".." components.
cleanedResolvedPath := filepath.Clean(resolvedPath)
if cleanedResolvedPath != resolvedPath {
// Coverage: This should never happen.
return "", fmt.Errorf("Internal inconsistency: Path %s resolved to %s still cleaned up to %s", path, resolvedPath, cleanedResolvedPath)
}
return resolvedPath, nil
default: // err != nil, unrecognized
return "", err
}
}
// resolveExistingPathToFullyExplicit is the same as ResolvePathToFullyExplicit,
// but without the special case for missing final component.
func resolveExistingPathToFullyExplicit(path string) (string, error) {
resolved, err := filepath.Abs(path)
if err != nil {
return "", err // Coverage: This can fail only if os.Getwd() fails.
}
resolved, err = filepath.EvalSymlinks(resolved)
if err != nil {
return "", err
}
return filepath.Clean(resolved), nil
}

View File

@@ -13,6 +13,7 @@ import (
"strings"
"github.com/Sirupsen/logrus"
"github.com/containers/image/types"
"github.com/docker/docker/pkg/homedir"
)
@@ -29,55 +30,70 @@ const (
tagsURL = "%s/tags/list"
manifestURL = "%s/manifests/%s"
blobsURL = "%s/blobs/%s"
blobUploadURL = "%s/blobs/uploads/?digest=%s"
blobUploadURL = "%s/blobs/uploads/"
)
// dockerClient is configuration for dealing with a single Docker registry.
type dockerClient struct {
ctx *types.SystemContext
registry string
username string
password string
wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty
scheme string // Cache of a value returned by a successful ping() if not empty
transport *http.Transport
client *http.Client
signatureBase signatureStorageBase
}
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
func newDockerClient(refHostname, certPath string, tlsVerify bool) (*dockerClient, error) {
var registry string
if refHostname == dockerHostname {
// “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) (*dockerClient, error) {
registry := ref.ref.Hostname()
if registry == dockerHostname {
registry = dockerRegistry
} else {
registry = refHostname
}
username, password, err := getAuth(refHostname)
username, password, err := getAuth(ref.ref.Hostname())
if err != nil {
return nil, err
}
var tr *http.Transport
if certPath != "" || !tlsVerify {
if ctx != nil && (ctx.DockerCertPath != "" || ctx.DockerInsecureSkipTLSVerify) {
tlsc := &tls.Config{}
if certPath != "" {
cert, err := tls.LoadX509KeyPair(filepath.Join(certPath, "cert.pem"), filepath.Join(certPath, "key.pem"))
if ctx.DockerCertPath != "" {
cert, err := tls.LoadX509KeyPair(filepath.Join(ctx.DockerCertPath, "cert.pem"), filepath.Join(ctx.DockerCertPath, "key.pem"))
if err != nil {
return nil, fmt.Errorf("Error loading x509 key pair: %s", err)
}
tlsc.Certificates = append(tlsc.Certificates, cert)
}
tlsc.InsecureSkipVerify = !tlsVerify
tlsc.InsecureSkipVerify = ctx.DockerInsecureSkipTLSVerify
tr = &http.Transport{
TLSClientConfig: tlsc,
}
}
client := &http.Client{}
if tr != nil {
client.Transport = tr
}
sigBase, err := configuredSignatureStorageBase(ctx, ref, write)
if err != nil {
return nil, err
}
return &dockerClient{
registry: registry,
username: username,
password: password,
transport: tr,
ctx: ctx,
registry: registry,
username: username,
password: password,
client: client,
signatureBase: sigBase,
}, nil
}
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
// url is NOT an absolute URL, but a path relative to the /v2/ top-level API path. The host name and schema is taken from the client or autodetected.
func (c *dockerClient) makeRequest(method, url string, headers map[string][]string, stream io.Reader) (*http.Response, error) {
if c.scheme == "" {
pr, err := c.ping()
@@ -89,10 +105,20 @@ func (c *dockerClient) makeRequest(method, url string, headers map[string][]stri
}
url = fmt.Sprintf(baseURL, c.scheme, c.registry) + url
return c.makeRequestToResolvedURL(method, url, headers, stream, -1)
}
// makeRequestToResolvedURL creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
// streamLen, if not -1, specifies the length of the data expected on stream.
// makeRequest should generally be preferred.
func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[string][]string, stream io.Reader, streamLen int64) (*http.Response, error) {
req, err := http.NewRequest(method, url, stream)
if err != nil {
return nil, err
}
if streamLen != -1 { // Do not blindly overwrite if streamLen == -1, http.NewRequest above can figure out the length of bytes.Reader and similar objects without us having to compute it.
req.ContentLength = streamLen
}
req.Header.Set("Docker-Distribution-API-Version", "registry/2.0")
for n, h := range headers {
for _, hh := range h {
@@ -104,12 +130,8 @@ func (c *dockerClient) makeRequest(method, url string, headers map[string][]stri
return nil, err
}
}
client := &http.Client{}
if c.transport != nil {
client.Transport = c.transport
}
logrus.Debugf("%s %s", method, url)
res, err := client.Do(req)
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
@@ -126,45 +148,38 @@ func (c *dockerClient) setupRequestAuth(req *http.Request) error {
req.SetBasicAuth(c.username, c.password)
return nil
case "Bearer":
client := &http.Client{}
if c.transport != nil {
client.Transport = c.transport
}
res, err := client.Do(req)
// FIXME? This gets a new token for every API request;
// we may be easily able to reuse a previous token, e.g.
// for OpenShift the token only identifies the user and does not vary
// across operations. Should we just try the request first, and
// only get a new token on failure?
// OTOH what to do with the single-use body stream in that case?
// Try performing the request, expecting it to fail.
testReq := *req
// Do not use the body stream, or we couldn't reuse it for the "real" call later.
testReq.Body = nil
testReq.ContentLength = 0
res, err := c.client.Do(&testReq)
if err != nil {
return err
}
hdr := res.Header.Get("WWW-Authenticate")
if hdr == "" || res.StatusCode != http.StatusUnauthorized {
chs := parseAuthHeader(res.Header)
if res.StatusCode != http.StatusUnauthorized || chs == nil || len(chs) == 0 {
// no need for bearer? wtf?
return nil
}
tokens = strings.Split(hdr, " ")
tokens = strings.Split(tokens[1], ",")
var realm, service, scope string
for _, token := range tokens {
if strings.HasPrefix(token, "realm") {
realm = strings.Trim(token[len("realm="):], "\"")
}
if strings.HasPrefix(token, "service") {
service = strings.Trim(token[len("service="):], "\"")
}
if strings.HasPrefix(token, "scope") {
scope = strings.Trim(token[len("scope="):], "\"")
}
// Arbitrarily use the first challenge, there is no reason to expect more than one.
challenge := chs[0]
if challenge.Scheme != "bearer" { // Another artifact of trying to handle WWW-Authenticate before it actually happens.
return fmt.Errorf("Unimplemented: WWW-Authenticate Bearer replaced by %#v", challenge.Scheme)
}
if realm == "" {
realm, ok := challenge.Parameters["realm"]
if !ok {
return fmt.Errorf("missing realm in bearer auth challenge")
}
if service == "" {
return fmt.Errorf("missing service in bearer auth challenge")
}
// The scope can be empty if we're not getting a token for a specific repo
//if scope == "" && repo != "" {
if scope == "" {
return fmt.Errorf("missing scope in bearer auth challenge")
}
service, _ := challenge.Parameters["service"] // Will be "" if not present
scope, _ := challenge.Parameters["scope"] // Will be "" if not present
token, err := c.getBearerToken(realm, service, scope)
if err != nil {
return err
@@ -182,7 +197,9 @@ func (c *dockerClient) getBearerToken(realm, service, scope string) (string, err
return "", err
}
getParams := authReq.URL.Query()
getParams.Add("service", service)
if service != "" {
getParams.Add("service", service)
}
if scope != "" {
getParams.Add("scope", scope)
}
@@ -286,13 +303,9 @@ type pingResponse struct {
}
func (c *dockerClient) ping() (*pingResponse, error) {
client := &http.Client{}
if c.transport != nil {
client.Transport = c.transport
}
ping := func(scheme string) (*pingResponse, error) {
url := fmt.Sprintf(baseURL, scheme, c.registry)
resp, err := client.Get(url)
resp, err := c.client.Get(url)
logrus.Debugf("Ping %s err %#v", url, err)
if err != nil {
return nil, err
@@ -318,14 +331,9 @@ func (c *dockerClient) ping() (*pingResponse, error) {
}
return pr, nil
}
scheme := "https"
pr, err := ping(scheme)
if err != nil {
scheme = "http"
pr, err = ping(scheme)
if err == nil {
return pr, nil
}
pr, err := ping("https")
if err != nil && c.ctx.DockerInsecureSkipTLSVerify {
pr, err = ping("http")
}
return pr, err
}

View File

@@ -5,51 +5,37 @@ import (
"fmt"
"net/http"
"github.com/projectatomic/skopeo/types"
"github.com/containers/image/image"
"github.com/containers/image/types"
)
// Image is a Docker-specific implementation of types.Image with a few extra methods
// which are specific to Docker.
type Image struct {
genericImage
types.Image
src *dockerImageSource
}
// NewDockerImage returns a new Image interface type after setting up
// newImage returns a new Image interface type after setting up
// a client to the registry hosting the given image.
func NewDockerImage(img, certPath string, tlsVerify bool) (types.Image, error) {
s, err := newDockerImageSource(img, certPath, tlsVerify)
// The caller must call .Close() on the returned Image.
func newImage(ctx *types.SystemContext, ref dockerReference) (types.Image, error) {
s, err := newImageSource(ctx, ref, nil)
if err != nil {
return nil, err
}
return &Image{genericImage{src: s}}, nil
}
// By construction a, docker.Image.genericImage.src must be a dockerImageSource.
// dockerSource returns it.
func (i *Image) dockerSource() (*dockerImageSource, error) {
if src, ok := i.genericImage.src.(*dockerImageSource); ok {
return src, nil
}
return nil, fmt.Errorf("Unexpected internal inconsistency, docker.Image not based on dockerImageSource")
return &Image{Image: image.FromSource(s), src: s}, nil
}
// SourceRefFullName returns a fully expanded name for the repository this image is in.
func (i *Image) SourceRefFullName() (string, error) {
src, err := i.dockerSource()
if err != nil {
return "", err
}
return src.ref.FullName(), nil
func (i *Image) SourceRefFullName() string {
return i.src.ref.ref.FullName()
}
// GetRepositoryTags list all tags available in the repository. Note that this has no connection with the tag(s) used for this specific image, if any.
func (i *Image) GetRepositoryTags() ([]string, error) {
src, err := i.dockerSource()
if err != nil {
return nil, err
}
url := fmt.Sprintf(tagsURL, src.ref.RemoteName())
res, err := src.c.makeRequest("GET", url, nil, nil)
url := fmt.Sprintf(tagsURL, i.src.ref.ref.RemoteName())
res, err := i.src.c.makeRequest("GET", url, nil, nil)
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,297 @@
package docker
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"github.com/Sirupsen/logrus"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
)
type dockerImageDestination struct {
ref dockerReference
c *dockerClient
// State
manifestDigest string // or "" if not yet known.
}
// 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)
if err != nil {
return nil, err
}
return &dockerImageDestination{
ref: ref,
c: c,
}, nil
}
// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent,
// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects.
func (d *dockerImageDestination) Reference() types.ImageReference {
return d.ref
}
// Close removes resources associated with an initialized ImageDestination, if any.
func (d *dockerImageDestination) Close() {
}
func (d *dockerImageDestination) SupportedManifestMIMETypes() []string {
return []string{
// TODO(runcom): we'll add OCI as part of another PR here
manifest.DockerV2Schema2MediaType,
manifest.DockerV2Schema1SignedMediaType,
manifest.DockerV2Schema1MediaType,
}
}
// SupportsSignatures returns an error (to be displayed to the user) if the destination certainly can't store signatures.
// Note: It is still possible for PutSignatures to fail if SupportsSignatures returns nil.
func (d *dockerImageDestination) SupportsSignatures() error {
return fmt.Errorf("Pushing signatures to a Docker Registry is not supported")
}
// ShouldCompressLayers returns true iff it is desirable to compress layer blobs written to this destination.
func (d *dockerImageDestination) ShouldCompressLayers() bool {
return true
}
// sizeCounter is an io.Writer which only counts the total size of its input.
type sizeCounter struct{ size int64 }
func (c *sizeCounter) Write(p []byte) (n int, err error) {
c.size += int64(len(p))
return len(p), nil
}
// PutBlob writes contents of stream and returns data representing the result (with all data filled in).
// 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.
// 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.
func (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobInfo) (types.BlobInfo, error) {
if inputInfo.Digest != "" {
checkURL := fmt.Sprintf(blobsURL, d.ref.ref.RemoteName(), inputInfo.Digest)
logrus.Debugf("Checking %s", checkURL)
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil)
if err != nil {
return types.BlobInfo{}, err
}
defer res.Body.Close()
switch res.StatusCode {
case http.StatusOK:
logrus.Debugf("... already exists, not uploading")
blobLength, err := strconv.ParseInt(res.Header.Get("Content-Length"), 10, 64)
if err != nil {
return types.BlobInfo{}, err
}
return types.BlobInfo{Digest: inputInfo.Digest, Size: blobLength}, nil
case http.StatusUnauthorized:
logrus.Debugf("... not authorized")
return types.BlobInfo{}, fmt.Errorf("not authorized to read from destination repository %s", d.ref.ref.RemoteName())
case http.StatusNotFound:
// noop
default:
return types.BlobInfo{}, fmt.Errorf("failed to read from destination repository %s: %v", d.ref.ref.RemoteName(), http.StatusText(res.StatusCode))
}
logrus.Debugf("... failed, status %d", res.StatusCode)
}
// FIXME? Chunked upload, progress reporting, etc.
uploadURL := fmt.Sprintf(blobUploadURL, d.ref.ref.RemoteName())
logrus.Debugf("Uploading %s", uploadURL)
res, err := d.c.makeRequest("POST", uploadURL, nil, nil)
if err != nil {
return types.BlobInfo{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusAccepted {
logrus.Debugf("Error initiating layer upload, response %#v", *res)
return types.BlobInfo{}, fmt.Errorf("Error initiating layer upload to %s, status %d", uploadURL, res.StatusCode)
}
uploadLocation, err := res.Location()
if err != nil {
return types.BlobInfo{}, fmt.Errorf("Error determining upload URL: %s", err.Error())
}
h := sha256.New()
sizeCounter := &sizeCounter{}
tee := io.TeeReader(stream, io.MultiWriter(h, sizeCounter))
res, err = d.c.makeRequestToResolvedURL("PATCH", uploadLocation.String(), map[string][]string{"Content-Type": {"application/octet-stream"}}, tee, inputInfo.Size)
if err != nil {
logrus.Debugf("Error uploading layer chunked, response %#v", *res)
return types.BlobInfo{}, err
}
defer res.Body.Close()
hash := h.Sum(nil)
computedDigest := "sha256:" + hex.EncodeToString(hash[:])
uploadLocation, err = res.Location()
if err != nil {
return types.BlobInfo{}, fmt.Errorf("Error determining upload URL: %s", err.Error())
}
// FIXME: DELETE uploadLocation on failure
locationQuery := uploadLocation.Query()
// TODO: check inputInfo.Digest == computedDigest https://github.com/containers/image/pull/70#discussion_r77646717
locationQuery.Set("digest", computedDigest)
uploadLocation.RawQuery = locationQuery.Encode()
res, err = d.c.makeRequestToResolvedURL("PUT", uploadLocation.String(), map[string][]string{"Content-Type": {"application/octet-stream"}}, nil, -1)
if err != nil {
return types.BlobInfo{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
logrus.Debugf("Error uploading layer, response %#v", *res)
return types.BlobInfo{}, fmt.Errorf("Error uploading layer to %s, status %d", uploadLocation, res.StatusCode)
}
logrus.Debugf("Upload of layer %s complete", computedDigest)
return types.BlobInfo{Digest: computedDigest, Size: sizeCounter.size}, nil
}
func (d *dockerImageDestination) PutManifest(m []byte) error {
digest, err := manifest.Digest(m)
if err != nil {
return err
}
d.manifestDigest = digest
reference, err := d.ref.tagOrDigest()
if err != nil {
return err
}
url := fmt.Sprintf(manifestURL, d.ref.ref.RemoteName(), reference)
headers := map[string][]string{}
mimeType := manifest.GuessMIMEType(m)
if mimeType != "" {
headers["Content-Type"] = []string{mimeType}
}
res, err := d.c.makeRequest("PUT", url, headers, bytes.NewReader(m))
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
body, err := ioutil.ReadAll(res.Body)
if err == nil {
logrus.Debugf("Error body %s", string(body))
}
logrus.Debugf("Error uploading manifest, status %d, %#v", res.StatusCode, res)
return fmt.Errorf("Error uploading manifest to %s, status %d", url, res.StatusCode)
}
return nil
}
func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error {
// FIXME? This overwrites files one at a time, definitely not atomic.
// A failure when updating signatures with a reordered copy could lose some of them.
// Skip dealing with the manifest digest if not necessary.
if len(signatures) == 0 {
return nil
}
if d.c.signatureBase == nil {
return fmt.Errorf("Pushing signatures to a Docker Registry is not supported, and there is no applicable signature storage configured")
}
// FIXME: This assumption that signatures are stored after the manifest rather breaks the model.
if d.manifestDigest == "" {
return fmt.Errorf("Unknown manifest digest, can't add signatures")
}
for i, signature := range signatures {
url := signatureStorageURL(d.c.signatureBase, d.manifestDigest, i)
if url == nil {
return fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
}
err := d.putOneSignature(url, signature)
if err != nil {
return err
}
}
// Remove any other signatures, if present.
// We stop at the first missing signature; if a previous deleting loop aborted
// prematurely, this may not clean up all of them, but one missing signature
// is enough for dockerImageSource to stop looking for other signatures, so that
// is sufficient.
for i := len(signatures); ; i++ {
url := signatureStorageURL(d.c.signatureBase, d.manifestDigest, i)
if url == nil {
return fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
}
missing, err := d.c.deleteOneSignature(url)
if err != nil {
return err
}
if missing {
break
}
}
return nil
}
// putOneSignature stores one signature to url.
func (d *dockerImageDestination) putOneSignature(url *url.URL, signature []byte) error {
switch url.Scheme {
case "file":
logrus.Debugf("Writing to %s", url.Path)
err := os.MkdirAll(filepath.Dir(url.Path), 0755)
if err != nil {
return err
}
err = ioutil.WriteFile(url.Path, signature, 0644)
if err != nil {
return err
}
return nil
case "http", "https":
return fmt.Errorf("Writing directly to a %s sigstore %s is not supported. Configure a sigstore-staging: location.", url.Scheme, url.String())
default:
return fmt.Errorf("Unsupported scheme when writing signature to %s", url.String())
}
}
// deleteOneSignature deletes a signature from url, if it exists.
// If it successfully determines that the signature does not exist, returns (true, nil)
func (c *dockerClient) deleteOneSignature(url *url.URL) (missing bool, err error) {
switch url.Scheme {
case "file":
logrus.Debugf("Deleting %s", url.Path)
err := os.Remove(url.Path)
if err != nil && os.IsNotExist(err) {
return true, nil
}
return false, err
case "http", "https":
return false, fmt.Errorf("Writing directly to a %s sigstore %s is not supported. Configure a sigstore-staging: location.", url.Scheme, url.String())
default:
return false, fmt.Errorf("Unsupported scheme when deleting signature from %s", url.String())
}
}
// Commit marks the process of storing the image as successful and asks for the image to be persisted.
// WARNING: This does not have any transactional semantics:
// - Uploaded data MAY be visible to others before Commit() is called
// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed)
func (d *dockerImageDestination) Commit() error {
return nil
}

View File

@@ -0,0 +1,288 @@
package docker
import (
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
"net/url"
"os"
"strconv"
"github.com/Sirupsen/logrus"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
)
type errFetchManifest struct {
statusCode int
body []byte
}
func (e errFetchManifest) Error() string {
return fmt.Sprintf("error fetching manifest: status code: %d, body: %s", e.statusCode, string(e.body))
}
type dockerImageSource struct {
ref dockerReference
requestedManifestMIMETypes []string
c *dockerClient
// State
cachedManifest []byte // nil if not loaded yet
cachedManifestMIMEType string // Only valid if cachedManifest != nil
}
// newImageSource creates a new ImageSource for the specified image reference,
// asking the backend to use a manifest from requestedManifestMIMETypes if possible.
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
// The caller must call .Close() on the returned ImageSource.
func newImageSource(ctx *types.SystemContext, ref dockerReference, requestedManifestMIMETypes []string) (*dockerImageSource, error) {
c, err := newDockerClient(ctx, ref, false)
if err != nil {
return nil, err
}
if requestedManifestMIMETypes == nil {
requestedManifestMIMETypes = manifest.DefaultRequestedManifestMIMETypes
}
return &dockerImageSource{
ref: ref,
requestedManifestMIMETypes: requestedManifestMIMETypes,
c: c,
}, nil
}
// Reference returns the reference used to set up this source, _as specified by the user_
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
func (s *dockerImageSource) Reference() types.ImageReference {
return s.ref
}
// Close removes resources associated with an initialized ImageSource, if any.
func (s *dockerImageSource) Close() {
}
// simplifyContentType drops parameters from a HTTP media type (see https://tools.ietf.org/html/rfc7231#section-3.1.1.1)
// Alternatively, an empty string is returned unchanged, and invalid values are "simplified" to an empty string.
func simplifyContentType(contentType string) string {
if contentType == "" {
return contentType
}
mimeType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return ""
}
return mimeType
}
func (s *dockerImageSource) GetManifest() ([]byte, string, error) {
err := s.ensureManifestIsLoaded()
if err != nil {
return nil, "", err
}
return s.cachedManifest, s.cachedManifestMIMEType, nil
}
// ensureManifestIsLoaded sets s.cachedManifest and s.cachedManifestMIMEType
//
// ImageSource implementations are not required or expected to do any caching,
// but because our signatures are “attached” to the manifest digest,
// we need to ensure that the digest of the manifest returned by GetManifest
// and used by GetSignatures are consistent, otherwise we would get spurious
// signature verification failures when pulling while a tag is being updated.
func (s *dockerImageSource) ensureManifestIsLoaded() error {
if s.cachedManifest != nil {
return nil
}
reference, err := s.ref.tagOrDigest()
if err != nil {
return err
}
url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), reference)
// TODO(runcom) set manifest version header! schema1 for now - then schema2 etc etc and v1
// TODO(runcom) NO, switch on the resulter manifest like Docker is doing
headers := make(map[string][]string)
headers["Accept"] = s.requestedManifestMIMETypes
res, err := s.c.makeRequest("GET", url, headers, nil)
if err != nil {
return err
}
defer res.Body.Close()
manblob, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
if res.StatusCode != http.StatusOK {
return errFetchManifest{res.StatusCode, manblob}
}
// We might validate manblob against the Docker-Content-Digest header here to protect against transport errors.
s.cachedManifest = manblob
s.cachedManifestMIMEType = simplifyContentType(res.Header.Get("Content-Type"))
return nil
}
// GetBlob returns a stream for the specified blob, and the blobs size (or -1 if unknown).
func (s *dockerImageSource) GetBlob(digest string) (io.ReadCloser, int64, error) {
url := fmt.Sprintf(blobsURL, s.ref.ref.RemoteName(), digest)
logrus.Debugf("Downloading %s", url)
res, err := s.c.makeRequest("GET", url, nil, nil)
if err != nil {
return nil, 0, err
}
if res.StatusCode != http.StatusOK {
// print url also
return nil, 0, fmt.Errorf("Invalid status code returned when fetching blob %d", res.StatusCode)
}
size, err := strconv.ParseInt(res.Header.Get("Content-Length"), 10, 64)
if err != nil {
size = -1
}
return res.Body, size, nil
}
func (s *dockerImageSource) GetSignatures() ([][]byte, error) {
if s.c.signatureBase == nil { // Skip dealing with the manifest digest if not necessary.
return [][]byte{}, nil
}
if err := s.ensureManifestIsLoaded(); err != nil {
return nil, err
}
manifestDigest, err := manifest.Digest(s.cachedManifest)
if err != nil {
return nil, err
}
signatures := [][]byte{}
for i := 0; ; i++ {
url := signatureStorageURL(s.c.signatureBase, manifestDigest, i)
if url == nil {
return nil, fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
}
signature, missing, err := s.getOneSignature(url)
if err != nil {
return nil, err
}
if missing {
break
}
signatures = append(signatures, signature)
}
return signatures, nil
}
// getOneSignature downloads one signature from url.
// If it successfully determines that the signature does not exist, returns with missing set to true and error set to nil.
func (s *dockerImageSource) getOneSignature(url *url.URL) (signature []byte, missing bool, err error) {
switch url.Scheme {
case "file":
logrus.Debugf("Reading %s", url.Path)
sig, err := ioutil.ReadFile(url.Path)
if err != nil {
if os.IsNotExist(err) {
return nil, true, nil
}
return nil, false, err
}
return sig, false, nil
case "http", "https":
logrus.Debugf("GET %s", url)
res, err := s.c.client.Get(url.String())
if err != nil {
return nil, false, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
return nil, true, nil
} else if res.StatusCode != http.StatusOK {
return nil, false, fmt.Errorf("Error reading signature from %s: status %d", url.String(), res.StatusCode)
}
sig, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, false, err
}
return sig, false, nil
default:
return nil, false, fmt.Errorf("Unsupported scheme when reading signature from %s", url.String())
}
}
// deleteImage deletes the named image from the registry, if supported.
func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
c, err := newDockerClient(ctx, ref, true)
if err != nil {
return err
}
// When retrieving the digest from a registry >= 2.3 use the following header:
// "Accept": "application/vnd.docker.distribution.manifest.v2+json"
headers := make(map[string][]string)
headers["Accept"] = []string{manifest.DockerV2Schema2MediaType}
reference, err := ref.tagOrDigest()
if err != nil {
return err
}
getURL := fmt.Sprintf(manifestURL, ref.ref.RemoteName(), reference)
get, err := c.makeRequest("GET", getURL, headers, nil)
if err != nil {
return err
}
defer get.Body.Close()
manifestBody, err := ioutil.ReadAll(get.Body)
if err != nil {
return err
}
switch get.StatusCode {
case http.StatusOK:
case http.StatusNotFound:
return fmt.Errorf("Unable to delete %v. Image may not exist or is not stored with a v2 Schema in a v2 registry.", ref.ref)
default:
return fmt.Errorf("Failed to delete %v: %s (%v)", ref.ref, manifestBody, get.Status)
}
digest := get.Header.Get("Docker-Content-Digest")
deleteURL := fmt.Sprintf(manifestURL, ref.ref.RemoteName(), digest)
// When retrieving the digest from a registry >= 2.3 use the following header:
// "Accept": "application/vnd.docker.distribution.manifest.v2+json"
delete, err := c.makeRequest("DELETE", deleteURL, headers, nil)
if err != nil {
return err
}
defer delete.Body.Close()
body, err := ioutil.ReadAll(delete.Body)
if err != nil {
return err
}
if delete.StatusCode != http.StatusAccepted {
return fmt.Errorf("Failed to delete %v: %s (%v)", deleteURL, string(body), delete.Status)
}
if c.signatureBase != nil {
manifestDigest, err := manifest.Digest(manifestBody)
if err != nil {
return err
}
for i := 0; ; i++ {
url := signatureStorageURL(c.signatureBase, manifestDigest, i)
if url == nil {
return fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
}
missing, err := c.deleteOneSignature(url)
if err != nil {
return err
}
if missing {
break
}
}
}
return nil
}

View File

@@ -0,0 +1,153 @@
package docker
import (
"fmt"
"strings"
"github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
// Transport is an ImageTransport for Docker registry-hosted images.
var Transport = dockerTransport{}
type dockerTransport struct{}
func (t dockerTransport) Name() string {
return "docker"
}
// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference.
func (t dockerTransport) ParseReference(reference string) (types.ImageReference, error) {
return ParseReference(reference)
}
// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
func (t dockerTransport) ValidatePolicyConfigurationScope(scope string) error {
// FIXME? We could be verifying the various character set and length restrictions
// from docker/distribution/reference.regexp.go, but other than that there
// are few semantically invalid strings.
return nil
}
// dockerReference is an ImageReference for Docker images.
type dockerReference struct {
ref reference.Named // By construction we know that !reference.IsNameOnly(ref)
}
// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an Docker ImageReference.
func ParseReference(refString string) (types.ImageReference, error) {
if !strings.HasPrefix(refString, "//") {
return nil, fmt.Errorf("docker: image reference %s does not start with //", refString)
}
ref, err := reference.ParseNamed(strings.TrimPrefix(refString, "//"))
if err != nil {
return nil, err
}
ref = reference.WithDefaultTag(ref)
return NewReference(ref)
}
// NewReference returns a Docker reference for a named reference. The reference must satisfy !reference.IsNameOnly().
func NewReference(ref reference.Named) (types.ImageReference, error) {
if reference.IsNameOnly(ref) {
return nil, fmt.Errorf("Docker reference %s has neither a tag nor a digest", ref.String())
}
// A github.com/distribution/reference value can have a tag and a digest at the same time!
// docker/reference does not handle that, so fail.
// (Even if it were supported, the semantics of policy namespaces are unclear - should we drop
// the tag or the digest first?)
_, isTagged := ref.(reference.NamedTagged)
_, isDigested := ref.(reference.Canonical)
if isTagged && isDigested {
return nil, fmt.Errorf("Docker references with both a tag and digest are currently not supported")
}
return dockerReference{
ref: ref,
}, nil
}
func (ref dockerReference) Transport() types.ImageTransport {
return Transport
}
// StringWithinTransport returns a string representation of the reference, which MUST be such that
// reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference.
// NOTE: The returned string is not promised to be equal to the original input to ParseReference;
// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa.
// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix.
func (ref dockerReference) StringWithinTransport() string {
return "//" + ref.ref.String()
}
// DockerReference returns a Docker reference associated with this reference
// (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent,
// not e.g. after redirect or alias processing), or nil if unknown/not applicable.
func (ref dockerReference) DockerReference() reference.Named {
return ref.ref
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref dockerReference) PolicyConfigurationIdentity() string {
res, err := policyconfiguration.DockerReferenceIdentity(ref.ref)
if res == "" || err != nil { // Coverage: Should never happen, NewReference above should refuse values which could cause a failure.
panic(fmt.Sprintf("Internal inconsistency: policyconfiguration.DockerReferenceIdentity returned %#v, %v", res, err))
}
return res
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref dockerReference) PolicyConfigurationNamespaces() []string {
return policyconfiguration.DockerReferenceNamespaces(ref.ref)
}
// NewImage returns a types.Image for this reference.
// The caller must call .Close() on the returned Image.
func (ref dockerReference) NewImage(ctx *types.SystemContext) (types.Image, error) {
return newImage(ctx, ref)
}
// NewImageSource returns a types.ImageSource for this reference,
// asking the backend to use a manifest from requestedManifestMIMETypes if possible.
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
// The caller must call .Close() on the returned ImageSource.
func (ref dockerReference) NewImageSource(ctx *types.SystemContext, requestedManifestMIMETypes []string) (types.ImageSource, error) {
return newImageSource(ctx, ref, requestedManifestMIMETypes)
}
// NewImageDestination returns a types.ImageDestination for this reference.
// The caller must call .Close() on the returned ImageDestination.
func (ref dockerReference) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
return newImageDestination(ctx, ref)
}
// DeleteImage deletes the named image from the registry, if supported.
func (ref dockerReference) DeleteImage(ctx *types.SystemContext) error {
return deleteImage(ctx, ref)
}
// tagOrDigest returns a tag or digest from the reference.
func (ref dockerReference) tagOrDigest() (string, error) {
if ref, ok := ref.ref.(reference.Canonical); ok {
return ref.Digest().String(), nil
}
if ref, ok := ref.ref.(reference.NamedTagged); ok {
return ref.Tag(), nil
}
// This should not happen, NewReference above refuses reference.IsNameOnly values.
return "", fmt.Errorf("Internal inconsistency: Reference %s unexpectedly has neither a digest nor a tag", ref.ref.String())
}

198
vendor/github.com/containers/image/docker/lookaside.go generated vendored Normal file
View File

@@ -0,0 +1,198 @@
package docker
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"github.com/ghodss/yaml"
"github.com/Sirupsen/logrus"
"github.com/containers/image/types"
)
// systemRegistriesDirPath is the path to registries.d, used for locating lookaside Docker signature storage.
// You can override this at build time with
// -ldflags '-X github.com/containers/image/docker.systemRegistriesDirPath=$your_path'
var systemRegistriesDirPath = builtinRegistriesDirPath
// builtinRegistriesDirPath is the path to registries.d.
// DO NOT change this, instead see systemRegistriesDirPath above.
const builtinRegistriesDirPath = "/etc/containers/registries.d"
// registryConfiguration is one of the files in registriesDirPath configuring lookaside locations, or the result of merging them all.
// NOTE: Keep this in sync with docs/registries.d.md!
type registryConfiguration struct {
DefaultDocker *registryNamespace `json:"default-docker"`
// The key is a namespace, using fully-expanded Docker reference format or parent namespaces (per dockerReference.PolicyConfiguration*),
Docker map[string]registryNamespace `json:"docker"`
}
// registryNamespace defines lookaside locations for a single namespace.
type registryNamespace struct {
SigStore string `json:"sigstore"` // For reading, and if SigStoreStaging is not present, for writing.
SigStoreStaging string `json:"sigstore-staging"` // For writing only.
}
// signatureStorageBase is an "opaque" type representing a lookaside Docker signature storage.
// Users outside of this file should use configuredSignatureStorageBase and signatureStorageURL below.
type signatureStorageBase *url.URL // The only documented value is nil, meaning storage is not supported.
// configuredSignatureStorageBase reads configuration to find an appropriate signature storage URL for ref, for write access if “write”.
func configuredSignatureStorageBase(ctx *types.SystemContext, ref dockerReference, write bool) (signatureStorageBase, error) {
// FIXME? Loading and parsing the config could be cached across calls.
dirPath := registriesDirPath(ctx)
logrus.Debugf(`Using registries.d directory %s for sigstore configuration`, dirPath)
config, err := loadAndMergeConfig(dirPath)
if err != nil {
return nil, err
}
topLevel := config.signatureTopLevel(ref, write)
if topLevel == "" {
return nil, nil
}
url, err := url.Parse(topLevel)
if err != nil {
return nil, fmt.Errorf("Invalid signature storage URL %s: %v", topLevel, err)
}
// FIXME? Restrict to explicitly supported schemes?
repo := ref.ref.FullName() // Note that this is without a tag or digest.
if path.Clean(repo) != repo { // Coverage: This should not be reachable because /./ and /../ components are not valid in docker references
return nil, fmt.Errorf("Unexpected path elements in Docker reference %s for signature storage", ref.ref.String())
}
url.Path = url.Path + "/" + repo
return url, nil
}
// registriesDirPath returns a path to registries.d
func registriesDirPath(ctx *types.SystemContext) string {
if ctx != nil {
if ctx.RegistriesDirPath != "" {
return ctx.RegistriesDirPath
}
if ctx.RootForImplicitAbsolutePaths != "" {
return filepath.Join(ctx.RootForImplicitAbsolutePaths, systemRegistriesDirPath)
}
}
return systemRegistriesDirPath
}
// loadAndMergeConfig loads configuration files in dirPath
func loadAndMergeConfig(dirPath string) (*registryConfiguration, error) {
mergedConfig := registryConfiguration{Docker: map[string]registryNamespace{}}
dockerDefaultMergedFrom := ""
nsMergedFrom := map[string]string{}
dir, err := os.Open(dirPath)
if err != nil {
if os.IsNotExist(err) {
return &mergedConfig, nil
}
return nil, err
}
configNames, err := dir.Readdirnames(0)
if err != nil {
return nil, err
}
for _, configName := range configNames {
if !strings.HasSuffix(configName, ".yaml") {
continue
}
configPath := filepath.Join(dirPath, configName)
configBytes, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, err
}
var config registryConfiguration
err = yaml.Unmarshal(configBytes, &config)
if err != nil {
return nil, fmt.Errorf("Error parsing %s: %v", configPath, err)
}
if config.DefaultDocker != nil {
if mergedConfig.DefaultDocker != nil {
return nil, fmt.Errorf(`Error parsing signature storage configuration: "default-docker" defined both in "%s" and "%s"`,
dockerDefaultMergedFrom, configPath)
}
mergedConfig.DefaultDocker = config.DefaultDocker
dockerDefaultMergedFrom = configPath
}
for nsName, nsConfig := range config.Docker { // includes config.Docker == nil
if _, ok := mergedConfig.Docker[nsName]; ok {
return nil, fmt.Errorf(`Error parsing signature storage configuration: "docker" namespace "%s" defined both in "%s" and "%s"`,
nsName, nsMergedFrom[nsName], configPath)
}
mergedConfig.Docker[nsName] = nsConfig
nsMergedFrom[nsName] = configPath
}
}
return &mergedConfig, nil
}
// config.signatureTopLevel returns an URL string configured in config for ref, for write access if “write”.
// (the top level of the storage, namespaced by repo.FullName etc.), or "" if no signature storage should be used.
func (config *registryConfiguration) signatureTopLevel(ref dockerReference, write bool) string {
if config.Docker != nil {
// Look for a full match.
identity := ref.PolicyConfigurationIdentity()
if ns, ok := config.Docker[identity]; ok {
logrus.Debugf(` Using "docker" namespace %s`, identity)
if url := ns.signatureTopLevel(write); url != "" {
return url
}
}
// Look for a match of the possible parent namespaces.
for _, name := range ref.PolicyConfigurationNamespaces() {
if ns, ok := config.Docker[name]; ok {
logrus.Debugf(` Using "docker" namespace %s`, name)
if url := ns.signatureTopLevel(write); url != "" {
return url
}
}
}
}
// Look for a default location
if config.DefaultDocker != nil {
logrus.Debugf(` Using "default-docker" configuration`)
if url := config.DefaultDocker.signatureTopLevel(write); url != "" {
return url
}
}
logrus.Debugf(" No signature storage configuration found for %s", ref.PolicyConfigurationIdentity())
return ""
}
// ns.signatureTopLevel returns an URL string configured in ns for ref, for write access if “write”.
// or "" if nothing has been configured.
func (ns registryNamespace) signatureTopLevel(write bool) string {
if write && ns.SigStoreStaging != "" {
logrus.Debugf(` Using %s`, ns.SigStoreStaging)
return ns.SigStoreStaging
}
if ns.SigStore != "" {
logrus.Debugf(` Using %s`, ns.SigStore)
return ns.SigStore
}
return ""
}
// signatureStorageURL returns an URL usable for acessing signature index in base with known manifestDigest, or nil if not applicable.
// Returns nil iff base == nil.
func signatureStorageURL(base signatureStorageBase, manifestDigest string, index int) *url.URL {
if base == nil {
return nil
}
url := *base
url.Path = fmt.Sprintf("%s@%s/signature-%d", url.Path, manifestDigest, index+1)
return &url
}

View File

@@ -0,0 +1,57 @@
package policyconfiguration
import (
"errors"
"fmt"
"strings"
"github.com/docker/docker/reference"
)
// DockerReferenceIdentity returns a string representation of the reference, suitable for policy lookup,
// as a backend for ImageReference.PolicyConfigurationIdentity.
// The reference must satisfy !reference.IsNameOnly().
func DockerReferenceIdentity(ref reference.Named) (string, error) {
res := ref.FullName()
tagged, isTagged := ref.(reference.NamedTagged)
digested, isDigested := ref.(reference.Canonical)
switch {
case isTagged && isDigested: // This should not happen, docker/reference.ParseNamed drops the tag.
return "", fmt.Errorf("Unexpected Docker reference %s with both a name and a digest", ref.String())
case !isTagged && !isDigested: // This should not happen, the caller is expected to ensure !reference.IsNameOnly()
return "", fmt.Errorf("Internal inconsistency: Docker reference %s with neither a tag nor a digest", ref.String())
case isTagged:
res = res + ":" + tagged.Tag()
case isDigested:
res = res + "@" + digested.Digest().String()
default: // Coverage: The above was supposed to be exhaustive.
return "", errors.New("Internal inconsistency, unexpected default branch")
}
return res, nil
}
// DockerReferenceNamespaces returns a list of other policy configuration namespaces to search,
// as a backend for ImageReference.PolicyConfigurationIdentity.
// The reference must satisfy !reference.IsNameOnly().
func DockerReferenceNamespaces(ref reference.Named) []string {
// Look for a match of the repository, and then of the possible parent
// namespaces. Note that this only happens on the expanded host names
// and repository names, i.e. "busybox" is looked up as "docker.io/library/busybox",
// then in its parent "docker.io/library"; in none of "busybox",
// un-namespaced "library" nor in "" supposedly implicitly representing "library/".
//
// ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last
// iteration matches the host name (for any namespace).
res := []string{}
name := ref.FullName()
for {
res = append(res, name)
lastSlash := strings.LastIndex(name, "/")
if lastSlash == -1 {
break
}
name = name[:lastSlash]
}
return res
}

View File

@@ -0,0 +1,159 @@
package docker
// Based on github.com/docker/distribution/registry/client/auth/authchallenge.go, primarily stripping unnecessary dependencies.
import (
"net/http"
"strings"
)
// challenge carries information from a WWW-Authenticate response header.
// See RFC 7235.
type challenge struct {
// Scheme is the auth-scheme according to RFC 7235
Scheme string
// Parameters are the auth-params according to RFC 7235
Parameters map[string]string
}
// Octet types from RFC 7230.
type octetType byte
var octetTypes [256]octetType
const (
isToken octetType = 1 << iota
isSpace
)
func init() {
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
// CRLF = CR LF
// LWS = [CRLF] 1*( SP | HT )
// TEXT = <any OCTET except CTLs, but including LWS>
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
var t octetType
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
t |= isSpace
}
if isChar && !isCtl && !isSeparator {
t |= isToken
}
octetTypes[c] = t
}
}
func parseAuthHeader(header http.Header) []challenge {
challenges := []challenge{}
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
v, p := parseValueAndParams(h)
if v != "" {
challenges = append(challenges, challenge{Scheme: v, Parameters: p})
}
}
return challenges
}
// NOTE: This is not a fully compliant parser per RFC 7235:
// Most notably it does not support more than one challenge within a single header
// Some of the whitespace parsing also seems noncompliant.
// But it is clearly better than what we used to have…
func parseValueAndParams(header string) (value string, params map[string]string) {
params = make(map[string]string)
value, s := expectToken(header)
if value == "" {
return
}
value = strings.ToLower(value)
s = "," + skipSpace(s)
for strings.HasPrefix(s, ",") {
var pkey string
pkey, s = expectToken(skipSpace(s[1:]))
if pkey == "" {
return
}
if !strings.HasPrefix(s, "=") {
return
}
var pvalue string
pvalue, s = expectTokenOrQuoted(s[1:])
if pvalue == "" {
return
}
pkey = strings.ToLower(pkey)
params[pkey] = pvalue
s = skipSpace(s)
}
return
}
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpace == 0 {
break
}
}
return s[i:]
}
func expectToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isToken == 0 {
break
}
}
return s[:i], s[i:]
}
func expectTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return expectToken(s)
}
s = s[1:]
for i := 0; i < len(s); i++ {
switch s[i] {
case '"':
return s[:i], s[i+1:]
case '\\':
p := make([]byte, len(s)-1)
j := copy(p, s[:i])
escape := true
for i = i + 1; i < len(s); i++ {
b := s[i]
switch {
case escape:
escape = false
p[j] = b
j++
case b == '\\':
escape = true
case b == '"':
return string(p[:j]), s[i+1:]
default:
p[j] = b
j++
}
}
return "", ""
}
}
return "", ""
}

View File

@@ -0,0 +1,163 @@
package image
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
)
var (
validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
)
type fsLayersSchema1 struct {
BlobSum string `json:"blobSum"`
}
type manifestSchema1 struct {
Name string `json:"name"`
Tag string `json:"tag"`
Architecture string `json:"architecture"`
FSLayers []fsLayersSchema1 `json:"fsLayers"`
History []struct {
V1Compatibility string `json:"v1Compatibility"`
} `json:"history"`
SchemaVersion int `json:"schemaVersion"`
}
func manifestSchema1FromManifest(manifest []byte) (genericManifest, error) {
mschema1 := &manifestSchema1{}
if err := json.Unmarshal(manifest, mschema1); err != nil {
return nil, err
}
if err := fixManifestLayers(mschema1); err != nil {
return nil, err
}
// TODO(runcom): verify manifest schema 1, 2 etc
//if len(m.FSLayers) != len(m.History) {
//return nil, fmt.Errorf("length of history not equal to number of layers for %q", ref.String())
//}
//if len(m.FSLayers) == 0 {
//return nil, fmt.Errorf("no FSLayers in manifest for %q", ref.String())
//}
return mschema1, nil
}
func (m *manifestSchema1) ConfigInfo() types.BlobInfo {
return types.BlobInfo{}
}
func (m *manifestSchema1) LayerInfos() []types.BlobInfo {
layers := make([]types.BlobInfo, len(m.FSLayers))
for i, layer := range m.FSLayers { // NOTE: This includes empty layers (where m.History.V1Compatibility->ThrowAway)
layers[(len(m.FSLayers)-1)-i] = types.BlobInfo{Digest: layer.BlobSum, Size: -1}
}
return layers
}
func (m *manifestSchema1) Config() ([]byte, error) {
return []byte(m.History[0].V1Compatibility), nil
}
func (m *manifestSchema1) ImageInspectInfo() (*types.ImageInspectInfo, error) {
v1 := &v1Image{}
config, err := m.Config()
if err != nil {
return nil, err
}
if err := json.Unmarshal(config, v1); err != nil {
return nil, err
}
return &types.ImageInspectInfo{
Tag: m.Tag,
DockerVersion: v1.DockerVersion,
Created: v1.Created,
Labels: v1.Config.Labels,
Architecture: v1.Architecture,
Os: v1.OS,
}, nil
}
func (m *manifestSchema1) UpdatedManifest(options types.ManifestUpdateOptions) ([]byte, error) {
copy := *m
if options.LayerInfos != nil {
// Our LayerInfos includes empty layers (where m.History.V1Compatibility->ThrowAway), so expect them to be included here as well.
if len(copy.FSLayers) != len(options.LayerInfos) {
return nil, fmt.Errorf("Error preparing updated manifest: layer count changed from %d to %d", len(copy.FSLayers), len(options.LayerInfos))
}
for i, info := range options.LayerInfos {
// (docker push) sets up m.History.V1Compatibility->{Id,Parent} based on values of info.Digest,
// but (docker pull) ignores them in favor of computing DiffIDs from uncompressed data, except verifying the child->parent links and uniqueness.
// So, we don't bother recomputing the IDs in m.History.V1Compatibility.
copy.FSLayers[(len(options.LayerInfos)-1)-i].BlobSum = info.Digest
}
}
// docker/distribution requires a signature even if the incoming data uses the nominally unsigned DockerV2Schema1MediaType.
unsigned, err := json.Marshal(copy)
if err != nil {
return nil, err
}
return manifest.AddDummyV2S1Signature(unsigned)
}
// fixManifestLayers, after validating the supplied manifest
// (to use correctly-formatted IDs, and to not have non-consecutive ID collisions in manifest.History),
// modifies manifest to only have one entry for each layer ID in manifest.History (deleting the older duplicates,
// both from manifest.History and manifest.FSLayers).
// Note that even after this succeeds, manifest.FSLayers may contain duplicate entries
// (for Dockerfile operations which change the configuration but not the filesystem).
func fixManifestLayers(manifest *manifestSchema1) error {
type imageV1 struct {
ID string
Parent string
}
// Per the specification, we can assume that len(manifest.FSLayers) == len(manifest.History)
imgs := make([]*imageV1, len(manifest.FSLayers))
for i := range manifest.FSLayers {
img := &imageV1{}
if err := json.Unmarshal([]byte(manifest.History[i].V1Compatibility), img); err != nil {
return err
}
imgs[i] = img
if err := validateV1ID(img.ID); err != nil {
return err
}
}
if imgs[len(imgs)-1].Parent != "" {
return errors.New("Invalid parent ID in the base layer of the image.")
}
// check general duplicates to error instead of a deadlock
idmap := make(map[string]struct{})
var lastID string
for _, img := range imgs {
// skip IDs that appear after each other, we handle those later
if _, exists := idmap[img.ID]; img.ID != lastID && exists {
return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID)
}
lastID = img.ID
idmap[lastID] = struct{}{}
}
// backwards loop so that we keep the remaining indexes after removing items
for i := len(imgs) - 2; i >= 0; i-- {
if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue
manifest.FSLayers = append(manifest.FSLayers[:i], manifest.FSLayers[i+1:]...)
manifest.History = append(manifest.History[:i], manifest.History[i+1:]...)
} else if imgs[i].Parent != imgs[i+1].ID {
return fmt.Errorf("Invalid parent ID. Expected %v, got %v.", imgs[i+1].ID, imgs[i].Parent)
}
}
return nil
}
func validateV1ID(id string) error {
if ok := validHex.MatchString(id); !ok {
return fmt.Errorf("image ID %q is invalid", id)
}
return nil
}

View File

@@ -0,0 +1,85 @@
package image
import (
"encoding/json"
"fmt"
"io/ioutil"
"github.com/containers/image/types"
)
type descriptor struct {
MediaType string `json:"mediaType"`
Size int64 `json:"size"`
Digest string `json:"digest"`
}
type manifestSchema2 struct {
src types.ImageSource
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
ConfigDescriptor descriptor `json:"config"`
LayersDescriptors []descriptor `json:"layers"`
}
func manifestSchema2FromManifest(src types.ImageSource, manifest []byte) (genericManifest, error) {
v2s2 := manifestSchema2{src: src}
if err := json.Unmarshal(manifest, &v2s2); err != nil {
return nil, err
}
return &v2s2, nil
}
func (m *manifestSchema2) ConfigInfo() types.BlobInfo {
return types.BlobInfo{Digest: m.ConfigDescriptor.Digest, Size: m.ConfigDescriptor.Size}
}
func (m *manifestSchema2) LayerInfos() []types.BlobInfo {
blobs := []types.BlobInfo{}
for _, layer := range m.LayersDescriptors {
blobs = append(blobs, types.BlobInfo{Digest: layer.Digest, Size: layer.Size})
}
return blobs
}
func (m *manifestSchema2) Config() ([]byte, error) {
rawConfig, _, err := m.src.GetBlob(m.ConfigDescriptor.Digest)
if err != nil {
return nil, err
}
config, err := ioutil.ReadAll(rawConfig)
rawConfig.Close()
return config, err
}
func (m *manifestSchema2) ImageInspectInfo() (*types.ImageInspectInfo, error) {
config, err := m.Config()
if err != nil {
return nil, err
}
v1 := &v1Image{}
if err := json.Unmarshal(config, v1); err != nil {
return nil, err
}
return &types.ImageInspectInfo{
DockerVersion: v1.DockerVersion,
Created: v1.Created,
Labels: v1.Config.Labels,
Architecture: v1.Architecture,
Os: v1.OS,
}, nil
}
func (m *manifestSchema2) UpdatedManifest(options types.ManifestUpdateOptions) ([]byte, error) {
copy := *m
if options.LayerInfos != nil {
if len(copy.LayersDescriptors) != len(options.LayerInfos) {
return nil, fmt.Errorf("Error preparing updated manifest: layer count changed from %d to %d", len(copy.LayersDescriptors), len(options.LayerInfos))
}
for i, info := range options.LayerInfos {
copy.LayersDescriptors[i].Digest = info.Digest
copy.LayersDescriptors[i].Size = info.Size
}
}
return json.Marshal(copy)
}

186
vendor/github.com/containers/image/image/image.go generated vendored Normal file
View File

@@ -0,0 +1,186 @@
// Package image consolidates knowledge about various container image formats
// (as opposed to image storage mechanisms, which are handled by types.ImageSource)
// and exposes all of them using an unified interface.
package image
import (
"errors"
"fmt"
"time"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
)
// genericImage is a general set of utilities for working with container images,
// whatever is their underlying location (i.e. dockerImageSource-independent).
// Note the existence of skopeo/docker.Image: some instances of a `types.Image`
// may not be a `genericImage` directly. However, most users of `types.Image`
// do not care, and those who care about `skopeo/docker.Image` know they do.
type genericImage struct {
src types.ImageSource
// private cache for Manifest(); nil if not yet known.
cachedManifest []byte
// private cache for the manifest media type w/o having to guess it
// this may be the empty string in case the MIME Type wasn't guessed correctly
// this field is valid only if cachedManifest is not nil
cachedManifestMIMEType string
// private cache for Signatures(); nil if not yet known.
cachedSignatures [][]byte
}
// FromSource returns a types.Image implementation for source.
// The caller must call .Close() on the returned Image.
//
// FromSource “takes ownership” of the input ImageSource and will call src.Close()
// when the image is closed. (This does not prevent callers from using both the
// Image and ImageSource objects simultaneously, but it means that they only need to
// the Image.)
func FromSource(src types.ImageSource) types.Image {
return &genericImage{src: src}
}
// Reference returns the reference used to set up this source, _as specified by the user_
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
func (i *genericImage) Reference() types.ImageReference {
return i.src.Reference()
}
// Close removes resources associated with an initialized Image, if any.
func (i *genericImage) Close() {
i.src.Close()
}
// Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need.
// NOTE: It is essential for signature verification that Manifest returns the manifest from which ConfigInfo and LayerInfos is computed.
func (i *genericImage) Manifest() ([]byte, string, error) {
if i.cachedManifest == nil {
m, mt, err := i.src.GetManifest()
if err != nil {
return nil, "", err
}
i.cachedManifest = m
if mt == "" || mt == "text/plain" {
// Crane registries can return "text/plain".
// This makes no real sense, but it happens
// because requests for manifests are
// redirected to a content distribution
// network which is configured that way.
mt = manifest.GuessMIMEType(i.cachedManifest)
}
i.cachedManifestMIMEType = mt
}
return i.cachedManifest, i.cachedManifestMIMEType, nil
}
// Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
func (i *genericImage) Signatures() ([][]byte, error) {
if i.cachedSignatures == nil {
sigs, err := i.src.GetSignatures()
if err != nil {
return nil, err
}
i.cachedSignatures = sigs
}
return i.cachedSignatures, nil
}
type config struct {
Labels map[string]string
}
type v1Image struct {
// Config is the configuration of the container received from the client
Config *config `json:"config,omitempty"`
// DockerVersion specifies version on which image is built
DockerVersion string `json:"docker_version,omitempty"`
// Created timestamp when image was created
Created time.Time `json:"created"`
// Architecture is the hardware that the image is build and runs on
Architecture string `json:"architecture,omitempty"`
// OS is the operating system used to build and run the image
OS string `json:"os,omitempty"`
}
// will support v1 one day...
type genericManifest interface {
Config() ([]byte, error)
ConfigInfo() types.BlobInfo
LayerInfos() []types.BlobInfo
ImageInspectInfo() (*types.ImageInspectInfo, error) // The caller will need to fill in Layers
UpdatedManifest(types.ManifestUpdateOptions) ([]byte, error)
}
// getParsedManifest parses the manifest into a data structure, cleans it up, and returns it.
// NOTE: The manifest may have been modified in the process; DO NOT reserialize and store the return value
// if you want to preserve the original manifest; use the blob returned by Manifest() directly.
// NOTE: It is essential for signature verification that the object is computed from the same manifest which is returned by Manifest().
func (i *genericImage) getParsedManifest() (genericManifest, error) {
manblob, mt, err := i.Manifest()
if err != nil {
return nil, err
}
switch mt {
// "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md .
// This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might
// need to happen within the ImageSource.
case manifest.DockerV2Schema1MediaType, manifest.DockerV2Schema1SignedMediaType, "application/json":
return manifestSchema1FromManifest(manblob)
case manifest.DockerV2Schema2MediaType:
return manifestSchema2FromManifest(i.src, manblob)
case "":
return nil, errors.New("could not guess manifest media type")
default:
return nil, fmt.Errorf("unsupported manifest media type %s", mt)
}
}
func (i *genericImage) Inspect() (*types.ImageInspectInfo, error) {
// TODO(runcom): unused version param for now, default to docker v2-1
m, err := i.getParsedManifest()
if err != nil {
return nil, err
}
info, err := m.ImageInspectInfo()
if err != nil {
return nil, err
}
layers := m.LayerInfos()
info.Layers = make([]string, len(layers))
for i, layer := range layers {
info.Layers[i] = layer.Digest
}
return info, nil
}
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
// NOTE: It is essential for signature verification that ConfigInfo is computed from the same manifest which is returned by Manifest().
func (i *genericImage) ConfigInfo() (types.BlobInfo, error) {
m, err := i.getParsedManifest()
if err != nil {
return types.BlobInfo{}, err
}
return m.ConfigInfo(), nil
}
// 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.
// NOTE: It is essential for signature verification that LayerInfos is computed from the same manifest which is returned by Manifest().
// WARNING: The list may contain duplicates, and they are semantically relevant.
func (i *genericImage) LayerInfos() ([]types.BlobInfo, error) {
m, err := i.getParsedManifest()
if err != nil {
return nil, err
}
return m.LayerInfos(), nil
}
// UpdatedManifest returns the image's manifest modified according to updateOptions.
// This does not change the state of the Image object.
func (i *genericImage) UpdatedManifest(options types.ManifestUpdateOptions) ([]byte, error) {
m, err := i.getParsedManifest()
if err != nil {
return nil, err
}
return m.UpdatedManifest(options)
}

Some files were not shown because too many files have changed in this diff Show More