| ... | ... |
@@ -147,14 +147,21 @@ RUN set -x \ |
| 147 | 147 |
) \ |
| 148 | 148 |
&& rm -rf "$SECCOMP_PATH" |
| 149 | 149 |
|
| 150 |
-# Install registry |
|
| 151 |
-ENV REGISTRY_COMMIT ec87e9b6971d831f0eff752ddb54fb64693e51cd |
|
| 150 |
+# Install two versions of the registry. The first is an older version that |
|
| 151 |
+# only supports schema1 manifests. The second is a newer version that supports |
|
| 152 |
+# both. This allows integration-cli tests to cover push/pull with both schema1 |
|
| 153 |
+# and schema2 manifests. |
|
| 154 |
+ENV REGISTRY_COMMIT_SCHEMA1 ec87e9b6971d831f0eff752ddb54fb64693e51cd |
|
| 155 |
+ENV REGISTRY_COMMIT a7ae88da459b98b481a245e5b1750134724ac67d |
|
| 152 | 156 |
RUN set -x \ |
| 153 | 157 |
&& export GOPATH="$(mktemp -d)" \ |
| 154 | 158 |
&& git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \ |
| 155 | 159 |
&& (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT") \ |
| 156 | 160 |
&& GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ |
| 157 | 161 |
go build -o /usr/local/bin/registry-v2 github.com/docker/distribution/cmd/registry \ |
| 162 |
+ && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT_SCHEMA1") \ |
|
| 163 |
+ && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ |
|
| 164 |
+ go build -o /usr/local/bin/registry-v2-schema1 github.com/docker/distribution/cmd/registry \ |
|
| 158 | 165 |
&& rm -rf "$GOPATH" |
| 159 | 166 |
|
| 160 | 167 |
# Install notary server |
| ... | ... |
@@ -12,8 +12,11 @@ import ( |
| 12 | 12 |
"github.com/Sirupsen/logrus" |
| 13 | 13 |
"github.com/docker/distribution" |
| 14 | 14 |
"github.com/docker/distribution/digest" |
| 15 |
+ "github.com/docker/distribution/manifest/manifestlist" |
|
| 15 | 16 |
"github.com/docker/distribution/manifest/schema1" |
| 17 |
+ "github.com/docker/distribution/manifest/schema2" |
|
| 16 | 18 |
"github.com/docker/distribution/registry/api/errcode" |
| 19 |
+ "github.com/docker/distribution/registry/client" |
|
| 17 | 20 |
"github.com/docker/docker/distribution/metadata" |
| 18 | 21 |
"github.com/docker/docker/distribution/xfer" |
| 19 | 22 |
"github.com/docker/docker/image" |
| ... | ... |
@@ -27,6 +30,8 @@ import ( |
| 27 | 27 |
"golang.org/x/net/context" |
| 28 | 28 |
) |
| 29 | 29 |
|
| 30 |
+var errRootFSMismatch = errors.New("layers from manifest don't match image configuration")
|
|
| 31 |
+ |
|
| 30 | 32 |
type v2Puller struct {
|
| 31 | 33 |
blobSumService *metadata.BlobSumService |
| 32 | 34 |
endpoint registry.APIEndpoint |
| ... | ... |
@@ -61,18 +66,12 @@ func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
|
| 61 | 61 |
func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (err error) {
|
| 62 | 62 |
var layersDownloaded bool |
| 63 | 63 |
if !reference.IsNameOnly(ref) {
|
| 64 |
- var err error |
|
| 65 | 64 |
layersDownloaded, err = p.pullV2Tag(ctx, ref) |
| 66 | 65 |
if err != nil {
|
| 67 | 66 |
return err |
| 68 | 67 |
} |
| 69 | 68 |
} else {
|
| 70 |
- manSvc, err := p.repo.Manifests(ctx) |
|
| 71 |
- if err != nil {
|
|
| 72 |
- return err |
|
| 73 |
- } |
|
| 74 |
- |
|
| 75 |
- tags, err := manSvc.Tags() |
|
| 69 |
+ tags, err := p.repo.Tags(ctx).All(ctx) |
|
| 76 | 70 |
if err != nil {
|
| 77 | 71 |
// If this repository doesn't exist on V2, we should |
| 78 | 72 |
// permit a fallback to V1. |
| ... | ... |
@@ -84,8 +83,6 @@ func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (e |
| 84 | 84 |
// error later on. |
| 85 | 85 |
p.confirmedV2 = true |
| 86 | 86 |
|
| 87 |
- // This probably becomes a lot nicer after the manifest |
|
| 88 |
- // refactor... |
|
| 89 | 87 |
for _, tag := range tags {
|
| 90 | 88 |
tagRef, err := reference.WithTag(ref, tag) |
| 91 | 89 |
if err != nil {
|
| ... | ... |
@@ -203,58 +200,111 @@ func (ld *v2LayerDescriptor) Registered(diffID layer.DiffID) {
|
| 203 | 203 |
} |
| 204 | 204 |
|
| 205 | 205 |
func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdated bool, err error) {
|
| 206 |
- tagOrDigest := "" |
|
| 206 |
+ manSvc, err := p.repo.Manifests(ctx) |
|
| 207 |
+ if err != nil {
|
|
| 208 |
+ return false, err |
|
| 209 |
+ } |
|
| 210 |
+ |
|
| 211 |
+ var ( |
|
| 212 |
+ manifest distribution.Manifest |
|
| 213 |
+ tagOrDigest string // Used for logging/progress only |
|
| 214 |
+ ) |
|
| 207 | 215 |
if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
|
| 216 |
+ // NOTE: not using TagService.Get, since it uses HEAD requests |
|
| 217 |
+ // against the manifests endpoint, which are not supported by |
|
| 218 |
+ // all registry versions. |
|
| 219 |
+ manifest, err = manSvc.Get(ctx, "", client.WithTag(tagged.Tag())) |
|
| 220 |
+ if err != nil {
|
|
| 221 |
+ return false, allowV1Fallback(err) |
|
| 222 |
+ } |
|
| 208 | 223 |
tagOrDigest = tagged.Tag() |
| 209 |
- } else if digested, isCanonical := ref.(reference.Canonical); isCanonical {
|
|
| 224 |
+ } else if digested, isDigested := ref.(reference.Canonical); isDigested {
|
|
| 225 |
+ manifest, err = manSvc.Get(ctx, digested.Digest()) |
|
| 226 |
+ if err != nil {
|
|
| 227 |
+ return false, err |
|
| 228 |
+ } |
|
| 210 | 229 |
tagOrDigest = digested.Digest().String() |
| 211 | 230 |
} else {
|
| 212 | 231 |
return false, fmt.Errorf("internal error: reference has neither a tag nor a digest: %s", ref.String())
|
| 213 | 232 |
} |
| 214 | 233 |
|
| 215 |
- logrus.Debugf("Pulling ref from V2 registry: %s:%s", ref.FullName(), tagOrDigest)
|
|
| 234 |
+ if manifest == nil {
|
|
| 235 |
+ return false, fmt.Errorf("image manifest does not exist for tag or digest %q", tagOrDigest)
|
|
| 236 |
+ } |
|
| 216 | 237 |
|
| 217 |
- manSvc, err := p.repo.Manifests(ctx) |
|
| 218 |
- if err != nil {
|
|
| 219 |
- return false, err |
|
| 238 |
+ // If manSvc.Get succeeded, we can be confident that the registry on |
|
| 239 |
+ // the other side speaks the v2 protocol. |
|
| 240 |
+ p.confirmedV2 = true |
|
| 241 |
+ |
|
| 242 |
+ logrus.Debugf("Pulling ref from V2 registry: %s", ref.String())
|
|
| 243 |
+ progress.Message(p.config.ProgressOutput, tagOrDigest, "Pulling from "+p.repo.Name()) |
|
| 244 |
+ |
|
| 245 |
+ var ( |
|
| 246 |
+ imageID image.ID |
|
| 247 |
+ manifestDigest digest.Digest |
|
| 248 |
+ ) |
|
| 249 |
+ |
|
| 250 |
+ switch v := manifest.(type) {
|
|
| 251 |
+ case *schema1.SignedManifest: |
|
| 252 |
+ imageID, manifestDigest, err = p.pullSchema1(ctx, ref, v) |
|
| 253 |
+ if err != nil {
|
|
| 254 |
+ return false, err |
|
| 255 |
+ } |
|
| 256 |
+ case *schema2.DeserializedManifest: |
|
| 257 |
+ imageID, manifestDigest, err = p.pullSchema2(ctx, ref, v) |
|
| 258 |
+ if err != nil {
|
|
| 259 |
+ return false, err |
|
| 260 |
+ } |
|
| 261 |
+ case *manifestlist.DeserializedManifestList: |
|
| 262 |
+ imageID, manifestDigest, err = p.pullManifestList(ctx, ref, v) |
|
| 263 |
+ if err != nil {
|
|
| 264 |
+ return false, err |
|
| 265 |
+ } |
|
| 266 |
+ default: |
|
| 267 |
+ return false, errors.New("unsupported manifest format")
|
|
| 220 | 268 |
} |
| 221 | 269 |
|
| 222 |
- unverifiedManifest, err := manSvc.GetByTag(tagOrDigest) |
|
| 223 |
- if err != nil {
|
|
| 224 |
- // If this manifest did not exist, we should allow a possible |
|
| 225 |
- // fallback to the v1 protocol, because dual-version setups may |
|
| 226 |
- // not host all manifests with the v2 protocol. We may also get |
|
| 227 |
- // a "not authorized" error if the manifest doesn't exist. |
|
| 228 |
- return false, allowV1Fallback(err) |
|
| 270 |
+ progress.Message(p.config.ProgressOutput, "", "Digest: "+manifestDigest.String()) |
|
| 271 |
+ |
|
| 272 |
+ oldTagImageID, err := p.config.ReferenceStore.Get(ref) |
|
| 273 |
+ if err == nil {
|
|
| 274 |
+ if oldTagImageID == imageID {
|
|
| 275 |
+ return false, nil |
|
| 276 |
+ } |
|
| 277 |
+ } else if err != reference.ErrDoesNotExist {
|
|
| 278 |
+ return false, err |
|
| 229 | 279 |
} |
| 230 |
- if unverifiedManifest == nil {
|
|
| 231 |
- return false, fmt.Errorf("image manifest does not exist for tag or digest %q", tagOrDigest)
|
|
| 280 |
+ |
|
| 281 |
+ if canonical, ok := ref.(reference.Canonical); ok {
|
|
| 282 |
+ if err = p.config.ReferenceStore.AddDigest(canonical, imageID, true); err != nil {
|
|
| 283 |
+ return false, err |
|
| 284 |
+ } |
|
| 285 |
+ } else if err = p.config.ReferenceStore.AddTag(ref, imageID, true); err != nil {
|
|
| 286 |
+ return false, err |
|
| 232 | 287 |
} |
| 233 | 288 |
|
| 234 |
- // If GetByTag succeeded, we can be confident that the registry on |
|
| 235 |
- // the other side speaks the v2 protocol. |
|
| 236 |
- p.confirmedV2 = true |
|
| 289 |
+ return true, nil |
|
| 290 |
+} |
|
| 237 | 291 |
|
| 292 |
+func (p *v2Puller) pullSchema1(ctx context.Context, ref reference.Named, unverifiedManifest *schema1.SignedManifest) (imageID image.ID, manifestDigest digest.Digest, err error) {
|
|
| 238 | 293 |
var verifiedManifest *schema1.Manifest |
| 239 |
- verifiedManifest, err = verifyManifest(unverifiedManifest, ref) |
|
| 294 |
+ verifiedManifest, err = verifySchema1Manifest(unverifiedManifest, ref) |
|
| 240 | 295 |
if err != nil {
|
| 241 |
- return false, err |
|
| 296 |
+ return "", "", err |
|
| 242 | 297 |
} |
| 243 | 298 |
|
| 244 | 299 |
rootFS := image.NewRootFS() |
| 245 | 300 |
|
| 246 | 301 |
if err := detectBaseLayer(p.config.ImageStore, verifiedManifest, rootFS); err != nil {
|
| 247 |
- return false, err |
|
| 302 |
+ return "", "", err |
|
| 248 | 303 |
} |
| 249 | 304 |
|
| 250 | 305 |
// remove duplicate layers and check parent chain validity |
| 251 | 306 |
err = fixManifestLayers(verifiedManifest) |
| 252 | 307 |
if err != nil {
|
| 253 |
- return false, err |
|
| 308 |
+ return "", "", err |
|
| 254 | 309 |
} |
| 255 | 310 |
|
| 256 |
- progress.Message(p.config.ProgressOutput, tagOrDigest, "Pulling from "+p.repo.Name()) |
|
| 257 |
- |
|
| 258 | 311 |
var descriptors []xfer.DownloadDescriptor |
| 259 | 312 |
|
| 260 | 313 |
// Image history converted to the new format |
| ... | ... |
@@ -269,12 +319,12 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdat |
| 269 | 269 |
ThrowAway bool `json:"throwaway,omitempty"` |
| 270 | 270 |
} |
| 271 | 271 |
if err := json.Unmarshal([]byte(verifiedManifest.History[i].V1Compatibility), &throwAway); err != nil {
|
| 272 |
- return false, err |
|
| 272 |
+ return "", "", err |
|
| 273 | 273 |
} |
| 274 | 274 |
|
| 275 | 275 |
h, err := v1.HistoryFromConfig([]byte(verifiedManifest.History[i].V1Compatibility), throwAway.ThrowAway) |
| 276 | 276 |
if err != nil {
|
| 277 |
- return false, err |
|
| 277 |
+ return "", "", err |
|
| 278 | 278 |
} |
| 279 | 279 |
history = append(history, h) |
| 280 | 280 |
|
| ... | ... |
@@ -293,43 +343,257 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdat |
| 293 | 293 |
|
| 294 | 294 |
resultRootFS, release, err := p.config.DownloadManager.Download(ctx, *rootFS, descriptors, p.config.ProgressOutput) |
| 295 | 295 |
if err != nil {
|
| 296 |
- return false, err |
|
| 296 |
+ return "", "", err |
|
| 297 | 297 |
} |
| 298 | 298 |
defer release() |
| 299 | 299 |
|
| 300 | 300 |
config, err := v1.MakeConfigFromV1Config([]byte(verifiedManifest.History[0].V1Compatibility), &resultRootFS, history) |
| 301 | 301 |
if err != nil {
|
| 302 |
- return false, err |
|
| 302 |
+ return "", "", err |
|
| 303 | 303 |
} |
| 304 | 304 |
|
| 305 |
- imageID, err := p.config.ImageStore.Create(config) |
|
| 305 |
+ imageID, err = p.config.ImageStore.Create(config) |
|
| 306 | 306 |
if err != nil {
|
| 307 |
- return false, err |
|
| 307 |
+ return "", "", err |
|
| 308 | 308 |
} |
| 309 | 309 |
|
| 310 |
- manifestDigest, _, err := digestFromManifest(unverifiedManifest, p.repoInfo) |
|
| 310 |
+ manifestDigest = digest.FromBytes(unverifiedManifest.Canonical) |
|
| 311 |
+ |
|
| 312 |
+ return imageID, manifestDigest, nil |
|
| 313 |
+} |
|
| 314 |
+ |
|
| 315 |
+func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *schema2.DeserializedManifest) (imageID image.ID, manifestDigest digest.Digest, err error) {
|
|
| 316 |
+ manifestDigest, err = schema2ManifestDigest(ref, mfst) |
|
| 311 | 317 |
if err != nil {
|
| 312 |
- return false, err |
|
| 318 |
+ return "", "", err |
|
| 313 | 319 |
} |
| 314 | 320 |
|
| 315 |
- if manifestDigest != "" {
|
|
| 316 |
- progress.Message(p.config.ProgressOutput, "", "Digest: "+manifestDigest.String()) |
|
| 321 |
+ target := mfst.Target() |
|
| 322 |
+ imageID = image.ID(target.Digest) |
|
| 323 |
+ if _, err := p.config.ImageStore.Get(imageID); err == nil {
|
|
| 324 |
+ // If the image already exists locally, no need to pull |
|
| 325 |
+ // anything. |
|
| 326 |
+ return imageID, manifestDigest, nil |
|
| 317 | 327 |
} |
| 318 | 328 |
|
| 319 |
- oldTagImageID, err := p.config.ReferenceStore.Get(ref) |
|
| 320 |
- if err == nil && oldTagImageID == imageID {
|
|
| 321 |
- return false, nil |
|
| 329 |
+ configChan := make(chan []byte, 1) |
|
| 330 |
+ errChan := make(chan error, 1) |
|
| 331 |
+ var cancel func() |
|
| 332 |
+ ctx, cancel = context.WithCancel(ctx) |
|
| 333 |
+ |
|
| 334 |
+ // Pull the image config |
|
| 335 |
+ go func() {
|
|
| 336 |
+ configJSON, err := p.pullSchema2ImageConfig(ctx, target.Digest) |
|
| 337 |
+ if err != nil {
|
|
| 338 |
+ errChan <- err |
|
| 339 |
+ cancel() |
|
| 340 |
+ return |
|
| 341 |
+ } |
|
| 342 |
+ configChan <- configJSON |
|
| 343 |
+ }() |
|
| 344 |
+ |
|
| 345 |
+ var descriptors []xfer.DownloadDescriptor |
|
| 346 |
+ |
|
| 347 |
+ // Note that the order of this loop is in the direction of bottom-most |
|
| 348 |
+ // to top-most, so that the downloads slice gets ordered correctly. |
|
| 349 |
+ for _, d := range mfst.References() {
|
|
| 350 |
+ layerDescriptor := &v2LayerDescriptor{
|
|
| 351 |
+ digest: d.Digest, |
|
| 352 |
+ repo: p.repo, |
|
| 353 |
+ blobSumService: p.blobSumService, |
|
| 354 |
+ } |
|
| 355 |
+ |
|
| 356 |
+ descriptors = append(descriptors, layerDescriptor) |
|
| 322 | 357 |
} |
| 323 | 358 |
|
| 324 |
- if canonical, ok := ref.(reference.Canonical); ok {
|
|
| 325 |
- if err = p.config.ReferenceStore.AddDigest(canonical, imageID, true); err != nil {
|
|
| 326 |
- return false, err |
|
| 359 |
+ var ( |
|
| 360 |
+ configJSON []byte // raw serialized image config |
|
| 361 |
+ unmarshalledConfig image.Image // deserialized image config |
|
| 362 |
+ downloadRootFS image.RootFS // rootFS to use for registering layers. |
|
| 363 |
+ ) |
|
| 364 |
+ if runtime.GOOS == "windows" {
|
|
| 365 |
+ configJSON, unmarshalledConfig, err = receiveConfig(configChan, errChan) |
|
| 366 |
+ if err != nil {
|
|
| 367 |
+ return "", "", err |
|
| 327 | 368 |
} |
| 328 |
- } else if err = p.config.ReferenceStore.AddTag(ref, imageID, true); err != nil {
|
|
| 329 |
- return false, err |
|
| 369 |
+ if unmarshalledConfig.RootFS == nil {
|
|
| 370 |
+ return "", "", errors.New("image config has no rootfs section")
|
|
| 371 |
+ } |
|
| 372 |
+ downloadRootFS = *unmarshalledConfig.RootFS |
|
| 373 |
+ downloadRootFS.DiffIDs = []layer.DiffID{}
|
|
| 374 |
+ } else {
|
|
| 375 |
+ downloadRootFS = *image.NewRootFS() |
|
| 330 | 376 |
} |
| 331 | 377 |
|
| 332 |
- return true, nil |
|
| 378 |
+ rootFS, release, err := p.config.DownloadManager.Download(ctx, downloadRootFS, descriptors, p.config.ProgressOutput) |
|
| 379 |
+ if err != nil {
|
|
| 380 |
+ if configJSON != nil {
|
|
| 381 |
+ // Already received the config |
|
| 382 |
+ return "", "", err |
|
| 383 |
+ } |
|
| 384 |
+ select {
|
|
| 385 |
+ case err = <-errChan: |
|
| 386 |
+ return "", "", err |
|
| 387 |
+ default: |
|
| 388 |
+ cancel() |
|
| 389 |
+ select {
|
|
| 390 |
+ case <-configChan: |
|
| 391 |
+ case <-errChan: |
|
| 392 |
+ } |
|
| 393 |
+ return "", "", err |
|
| 394 |
+ } |
|
| 395 |
+ } |
|
| 396 |
+ defer release() |
|
| 397 |
+ |
|
| 398 |
+ if configJSON == nil {
|
|
| 399 |
+ configJSON, unmarshalledConfig, err = receiveConfig(configChan, errChan) |
|
| 400 |
+ if err != nil {
|
|
| 401 |
+ return "", "", err |
|
| 402 |
+ } |
|
| 403 |
+ } |
|
| 404 |
+ |
|
| 405 |
+ // The DiffIDs returned in rootFS MUST match those in the config. |
|
| 406 |
+ // Otherwise the image config could be referencing layers that aren't |
|
| 407 |
+ // included in the manifest. |
|
| 408 |
+ if len(rootFS.DiffIDs) != len(unmarshalledConfig.RootFS.DiffIDs) {
|
|
| 409 |
+ return "", "", errRootFSMismatch |
|
| 410 |
+ } |
|
| 411 |
+ |
|
| 412 |
+ for i := range rootFS.DiffIDs {
|
|
| 413 |
+ if rootFS.DiffIDs[i] != unmarshalledConfig.RootFS.DiffIDs[i] {
|
|
| 414 |
+ return "", "", errRootFSMismatch |
|
| 415 |
+ } |
|
| 416 |
+ } |
|
| 417 |
+ |
|
| 418 |
+ imageID, err = p.config.ImageStore.Create(configJSON) |
|
| 419 |
+ if err != nil {
|
|
| 420 |
+ return "", "", err |
|
| 421 |
+ } |
|
| 422 |
+ |
|
| 423 |
+ return imageID, manifestDigest, nil |
|
| 424 |
+} |
|
| 425 |
+ |
|
| 426 |
+func receiveConfig(configChan <-chan []byte, errChan <-chan error) ([]byte, image.Image, error) {
|
|
| 427 |
+ select {
|
|
| 428 |
+ case configJSON := <-configChan: |
|
| 429 |
+ var unmarshalledConfig image.Image |
|
| 430 |
+ if err := json.Unmarshal(configJSON, &unmarshalledConfig); err != nil {
|
|
| 431 |
+ return nil, image.Image{}, err
|
|
| 432 |
+ } |
|
| 433 |
+ return configJSON, unmarshalledConfig, nil |
|
| 434 |
+ case err := <-errChan: |
|
| 435 |
+ return nil, image.Image{}, err
|
|
| 436 |
+ // Don't need a case for ctx.Done in the select because cancellation |
|
| 437 |
+ // will trigger an error in p.pullSchema2ImageConfig. |
|
| 438 |
+ } |
|
| 439 |
+} |
|
| 440 |
+ |
|
| 441 |
+// pullManifestList handles "manifest lists" which point to various |
|
| 442 |
+// platform-specifc manifests. |
|
| 443 |
+func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mfstList *manifestlist.DeserializedManifestList) (imageID image.ID, manifestListDigest digest.Digest, err error) {
|
|
| 444 |
+ manifestListDigest, err = schema2ManifestDigest(ref, mfstList) |
|
| 445 |
+ if err != nil {
|
|
| 446 |
+ return "", "", err |
|
| 447 |
+ } |
|
| 448 |
+ |
|
| 449 |
+ var manifestDigest digest.Digest |
|
| 450 |
+ for _, manifestDescriptor := range mfstList.Manifests {
|
|
| 451 |
+ // TODO(aaronl): The manifest list spec supports optional |
|
| 452 |
+ // "features" and "variant" fields. These are not yet used. |
|
| 453 |
+ // Once they are, their values should be interpreted here. |
|
| 454 |
+ if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == runtime.GOOS {
|
|
| 455 |
+ manifestDigest = manifestDescriptor.Digest |
|
| 456 |
+ break |
|
| 457 |
+ } |
|
| 458 |
+ } |
|
| 459 |
+ |
|
| 460 |
+ if manifestDigest == "" {
|
|
| 461 |
+ return "", "", errors.New("no supported platform found in manifest list")
|
|
| 462 |
+ } |
|
| 463 |
+ |
|
| 464 |
+ manSvc, err := p.repo.Manifests(ctx) |
|
| 465 |
+ if err != nil {
|
|
| 466 |
+ return "", "", err |
|
| 467 |
+ } |
|
| 468 |
+ |
|
| 469 |
+ manifest, err := manSvc.Get(ctx, manifestDigest) |
|
| 470 |
+ if err != nil {
|
|
| 471 |
+ return "", "", err |
|
| 472 |
+ } |
|
| 473 |
+ |
|
| 474 |
+ manifestRef, err := reference.WithDigest(ref, manifestDigest) |
|
| 475 |
+ if err != nil {
|
|
| 476 |
+ return "", "", err |
|
| 477 |
+ } |
|
| 478 |
+ |
|
| 479 |
+ switch v := manifest.(type) {
|
|
| 480 |
+ case *schema1.SignedManifest: |
|
| 481 |
+ imageID, _, err = p.pullSchema1(ctx, manifestRef, v) |
|
| 482 |
+ if err != nil {
|
|
| 483 |
+ return "", "", err |
|
| 484 |
+ } |
|
| 485 |
+ case *schema2.DeserializedManifest: |
|
| 486 |
+ imageID, _, err = p.pullSchema2(ctx, manifestRef, v) |
|
| 487 |
+ if err != nil {
|
|
| 488 |
+ return "", "", err |
|
| 489 |
+ } |
|
| 490 |
+ default: |
|
| 491 |
+ return "", "", errors.New("unsupported manifest format")
|
|
| 492 |
+ } |
|
| 493 |
+ |
|
| 494 |
+ return imageID, manifestListDigest, err |
|
| 495 |
+} |
|
| 496 |
+ |
|
| 497 |
+func (p *v2Puller) pullSchema2ImageConfig(ctx context.Context, dgst digest.Digest) (configJSON []byte, err error) {
|
|
| 498 |
+ blobs := p.repo.Blobs(ctx) |
|
| 499 |
+ configJSON, err = blobs.Get(ctx, dgst) |
|
| 500 |
+ if err != nil {
|
|
| 501 |
+ return nil, err |
|
| 502 |
+ } |
|
| 503 |
+ |
|
| 504 |
+ // Verify image config digest |
|
| 505 |
+ verifier, err := digest.NewDigestVerifier(dgst) |
|
| 506 |
+ if err != nil {
|
|
| 507 |
+ return nil, err |
|
| 508 |
+ } |
|
| 509 |
+ if _, err := verifier.Write(configJSON); err != nil {
|
|
| 510 |
+ return nil, err |
|
| 511 |
+ } |
|
| 512 |
+ if !verifier.Verified() {
|
|
| 513 |
+ err := fmt.Errorf("image config verification failed for digest %s", dgst)
|
|
| 514 |
+ logrus.Error(err) |
|
| 515 |
+ return nil, err |
|
| 516 |
+ } |
|
| 517 |
+ |
|
| 518 |
+ return configJSON, nil |
|
| 519 |
+} |
|
| 520 |
+ |
|
| 521 |
+// schema2ManifestDigest computes the manifest digest, and, if pulling by |
|
| 522 |
+// digest, ensures that it matches the requested digest. |
|
| 523 |
+func schema2ManifestDigest(ref reference.Named, mfst distribution.Manifest) (digest.Digest, error) {
|
|
| 524 |
+ _, canonical, err := mfst.Payload() |
|
| 525 |
+ if err != nil {
|
|
| 526 |
+ return "", err |
|
| 527 |
+ } |
|
| 528 |
+ |
|
| 529 |
+ // If pull by digest, then verify the manifest digest. |
|
| 530 |
+ if digested, isDigested := ref.(reference.Canonical); isDigested {
|
|
| 531 |
+ verifier, err := digest.NewDigestVerifier(digested.Digest()) |
|
| 532 |
+ if err != nil {
|
|
| 533 |
+ return "", err |
|
| 534 |
+ } |
|
| 535 |
+ if _, err := verifier.Write(canonical); err != nil {
|
|
| 536 |
+ return "", err |
|
| 537 |
+ } |
|
| 538 |
+ if !verifier.Verified() {
|
|
| 539 |
+ err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest())
|
|
| 540 |
+ logrus.Error(err) |
|
| 541 |
+ return "", err |
|
| 542 |
+ } |
|
| 543 |
+ return digested.Digest(), nil |
|
| 544 |
+ } |
|
| 545 |
+ |
|
| 546 |
+ return digest.FromBytes(canonical), nil |
|
| 333 | 547 |
} |
| 334 | 548 |
|
| 335 | 549 |
// allowV1Fallback checks if the error is a possible reason to fallback to v1 |
| ... | ... |
@@ -353,7 +617,7 @@ func allowV1Fallback(err error) error {
|
| 353 | 353 |
return err |
| 354 | 354 |
} |
| 355 | 355 |
|
| 356 |
-func verifyManifest(signedManifest *schema1.SignedManifest, ref reference.Named) (m *schema1.Manifest, err error) {
|
|
| 356 |
+func verifySchema1Manifest(signedManifest *schema1.SignedManifest, ref reference.Named) (m *schema1.Manifest, err error) {
|
|
| 357 | 357 |
// If pull by digest, then verify the manifest digest. NOTE: It is |
| 358 | 358 |
// important to do this first, before any other content validation. If the |
| 359 | 359 |
// digest cannot be verified, don't even bother with those other things. |
| ... | ... |
@@ -362,13 +626,7 @@ func verifyManifest(signedManifest *schema1.SignedManifest, ref reference.Named) |
| 362 | 362 |
if err != nil {
|
| 363 | 363 |
return nil, err |
| 364 | 364 |
} |
| 365 |
- payload, err := signedManifest.Payload() |
|
| 366 |
- if err != nil {
|
|
| 367 |
- // If this failed, the signatures section was corrupted |
|
| 368 |
- // or missing. Treat the entire manifest as the payload. |
|
| 369 |
- payload = signedManifest.Raw |
|
| 370 |
- } |
|
| 371 |
- if _, err := verifier.Write(payload); err != nil {
|
|
| 365 |
+ if _, err := verifier.Write(signedManifest.Canonical); err != nil {
|
|
| 372 | 366 |
return nil, err |
| 373 | 367 |
} |
| 374 | 368 |
if !verifier.Verified() {
|
| ... | ... |
@@ -376,15 +634,8 @@ func verifyManifest(signedManifest *schema1.SignedManifest, ref reference.Named) |
| 376 | 376 |
logrus.Error(err) |
| 377 | 377 |
return nil, err |
| 378 | 378 |
} |
| 379 |
- |
|
| 380 |
- var verifiedManifest schema1.Manifest |
|
| 381 |
- if err = json.Unmarshal(payload, &verifiedManifest); err != nil {
|
|
| 382 |
- return nil, err |
|
| 383 |
- } |
|
| 384 |
- m = &verifiedManifest |
|
| 385 |
- } else {
|
|
| 386 |
- m = &signedManifest.Manifest |
|
| 387 | 379 |
} |
| 380 |
+ m = &signedManifest.Manifest |
|
| 388 | 381 |
|
| 389 | 382 |
if m.SchemaVersion != 1 {
|
| 390 | 383 |
return nil, fmt.Errorf("unsupported schema version %d for %q", m.SchemaVersion, ref.String())
|
| ... | ... |
@@ -123,7 +123,7 @@ func TestValidateManifest(t *testing.T) {
|
| 123 | 123 |
t.Fatal("error unmarshaling manifest:", err)
|
| 124 | 124 |
} |
| 125 | 125 |
|
| 126 |
- verifiedManifest, err := verifyManifest(&goodSignedManifest, expectedDigest) |
|
| 126 |
+ verifiedManifest, err := verifySchema1Manifest(&goodSignedManifest, expectedDigest) |
|
| 127 | 127 |
if err != nil {
|
| 128 | 128 |
t.Fatal("validateManifest failed:", err)
|
| 129 | 129 |
} |
| ... | ... |
@@ -145,7 +145,7 @@ func TestValidateManifest(t *testing.T) {
|
| 145 | 145 |
t.Fatal("error unmarshaling manifest:", err)
|
| 146 | 146 |
} |
| 147 | 147 |
|
| 148 |
- verifiedManifest, err = verifyManifest(&extraDataSignedManifest, expectedDigest) |
|
| 148 |
+ verifiedManifest, err = verifySchema1Manifest(&extraDataSignedManifest, expectedDigest) |
|
| 149 | 149 |
if err != nil {
|
| 150 | 150 |
t.Fatal("validateManifest failed:", err)
|
| 151 | 151 |
} |
| ... | ... |
@@ -167,7 +167,7 @@ func TestValidateManifest(t *testing.T) {
|
| 167 | 167 |
t.Fatal("error unmarshaling manifest:", err)
|
| 168 | 168 |
} |
| 169 | 169 |
|
| 170 |
- verifiedManifest, err = verifyManifest(&badSignedManifest, expectedDigest) |
|
| 170 |
+ verifiedManifest, err = verifySchema1Manifest(&badSignedManifest, expectedDigest) |
|
| 171 | 171 |
if err == nil || !strings.HasPrefix(err.Error(), "image verification failed for digest") {
|
| 172 | 172 |
t.Fatal("expected validateManifest to fail with digest error")
|
| 173 | 173 |
} |
| ... | ... |
@@ -7,7 +7,6 @@ import ( |
| 7 | 7 |
"io" |
| 8 | 8 |
|
| 9 | 9 |
"github.com/Sirupsen/logrus" |
| 10 |
- "github.com/docker/distribution/digest" |
|
| 11 | 10 |
"github.com/docker/docker/distribution/metadata" |
| 12 | 11 |
"github.com/docker/docker/distribution/xfer" |
| 13 | 12 |
"github.com/docker/docker/image" |
| ... | ... |
@@ -77,7 +76,6 @@ func NewPusher(ref reference.Named, endpoint registry.APIEndpoint, repoInfo *reg |
| 77 | 77 |
endpoint: endpoint, |
| 78 | 78 |
repoInfo: repoInfo, |
| 79 | 79 |
config: imagePushConfig, |
| 80 |
- layersPushed: pushMap{layersPushed: make(map[digest.Digest]bool)},
|
|
| 81 | 80 |
}, nil |
| 82 | 81 |
case registry.APIVersion1: |
| 83 | 82 |
return &v1Pusher{
|
| ... | ... |
@@ -1,22 +1,20 @@ |
| 1 | 1 |
package distribution |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "encoding/json" |
|
| 5 | 4 |
"errors" |
| 6 | 5 |
"fmt" |
| 7 | 6 |
"io" |
| 8 | 7 |
"sync" |
| 9 |
- "time" |
|
| 10 | 8 |
|
| 11 | 9 |
"github.com/Sirupsen/logrus" |
| 12 | 10 |
"github.com/docker/distribution" |
| 13 | 11 |
"github.com/docker/distribution/digest" |
| 14 |
- "github.com/docker/distribution/manifest" |
|
| 15 | 12 |
"github.com/docker/distribution/manifest/schema1" |
| 13 |
+ "github.com/docker/distribution/manifest/schema2" |
|
| 14 |
+ "github.com/docker/distribution/registry/client" |
|
| 16 | 15 |
"github.com/docker/docker/distribution/metadata" |
| 17 | 16 |
"github.com/docker/docker/distribution/xfer" |
| 18 | 17 |
"github.com/docker/docker/image" |
| 19 |
- "github.com/docker/docker/image/v1" |
|
| 20 | 18 |
"github.com/docker/docker/layer" |
| 21 | 19 |
"github.com/docker/docker/pkg/ioutils" |
| 22 | 20 |
"github.com/docker/docker/pkg/progress" |
| ... | ... |
@@ -43,75 +41,75 @@ type v2Pusher struct {
|
| 43 | 43 |
config *ImagePushConfig |
| 44 | 44 |
repo distribution.Repository |
| 45 | 45 |
|
| 46 |
- // confirmedV2 is set to true if we confirm we're talking to a v2 |
|
| 47 |
- // registry. This is used to limit fallbacks to the v1 protocol. |
|
| 48 |
- confirmedV2 bool |
|
| 49 |
- |
|
| 50 |
- // layersPushed is the set of layers known to exist on the remote side. |
|
| 51 |
- // This avoids redundant queries when pushing multiple tags that |
|
| 52 |
- // involve the same layers. |
|
| 53 |
- layersPushed pushMap |
|
| 46 |
+ // pushState is state built by the Download functions. |
|
| 47 |
+ pushState pushState |
|
| 54 | 48 |
} |
| 55 | 49 |
|
| 56 |
-type pushMap struct {
|
|
| 50 |
+type pushState struct {
|
|
| 57 | 51 |
sync.Mutex |
| 58 |
- layersPushed map[digest.Digest]bool |
|
| 52 |
+ // remoteLayers is the set of layers known to exist on the remote side. |
|
| 53 |
+ // This avoids redundant queries when pushing multiple tags that |
|
| 54 |
+ // involve the same layers. It is also used to fill in digest and size |
|
| 55 |
+ // information when building the manifest. |
|
| 56 |
+ remoteLayers map[layer.DiffID]distribution.Descriptor |
|
| 57 |
+ // confirmedV2 is set to true if we confirm we're talking to a v2 |
|
| 58 |
+ // registry. This is used to limit fallbacks to the v1 protocol. |
|
| 59 |
+ confirmedV2 bool |
|
| 59 | 60 |
} |
| 60 | 61 |
|
| 61 | 62 |
func (p *v2Pusher) Push(ctx context.Context) (err error) {
|
| 62 |
- p.repo, p.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "push", "pull") |
|
| 63 |
+ p.pushState.remoteLayers = make(map[layer.DiffID]distribution.Descriptor) |
|
| 64 |
+ |
|
| 65 |
+ p.repo, p.pushState.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "push", "pull") |
|
| 63 | 66 |
if err != nil {
|
| 64 | 67 |
logrus.Debugf("Error getting v2 registry: %v", err)
|
| 65 |
- return fallbackError{err: err, confirmedV2: p.confirmedV2}
|
|
| 68 |
+ return fallbackError{err: err, confirmedV2: p.pushState.confirmedV2}
|
|
| 66 | 69 |
} |
| 67 | 70 |
|
| 68 | 71 |
if err = p.pushV2Repository(ctx); err != nil {
|
| 69 | 72 |
if registry.ContinueOnError(err) {
|
| 70 |
- return fallbackError{err: err, confirmedV2: p.confirmedV2}
|
|
| 73 |
+ return fallbackError{err: err, confirmedV2: p.pushState.confirmedV2}
|
|
| 71 | 74 |
} |
| 72 | 75 |
} |
| 73 | 76 |
return err |
| 74 | 77 |
} |
| 75 | 78 |
|
| 76 | 79 |
func (p *v2Pusher) pushV2Repository(ctx context.Context) (err error) {
|
| 77 |
- var associations []reference.Association |
|
| 78 |
- if _, isTagged := p.ref.(reference.NamedTagged); isTagged {
|
|
| 80 |
+ if namedTagged, isNamedTagged := p.ref.(reference.NamedTagged); isNamedTagged {
|
|
| 79 | 81 |
imageID, err := p.config.ReferenceStore.Get(p.ref) |
| 80 | 82 |
if err != nil {
|
| 81 | 83 |
return fmt.Errorf("tag does not exist: %s", p.ref.String())
|
| 82 | 84 |
} |
| 83 | 85 |
|
| 84 |
- associations = []reference.Association{
|
|
| 85 |
- {
|
|
| 86 |
- Ref: p.ref, |
|
| 87 |
- ImageID: imageID, |
|
| 88 |
- }, |
|
| 89 |
- } |
|
| 90 |
- } else {
|
|
| 91 |
- // Pull all tags |
|
| 92 |
- associations = p.config.ReferenceStore.ReferencesByName(p.ref) |
|
| 93 |
- } |
|
| 94 |
- if err != nil {
|
|
| 95 |
- return fmt.Errorf("error getting tags for %s: %s", p.repoInfo.Name(), err)
|
|
| 86 |
+ return p.pushV2Tag(ctx, namedTagged, imageID) |
|
| 96 | 87 |
} |
| 97 |
- if len(associations) == 0 {
|
|
| 98 |
- return fmt.Errorf("no tags to push for %s", p.repoInfo.Name())
|
|
| 88 |
+ |
|
| 89 |
+ if !reference.IsNameOnly(p.ref) {
|
|
| 90 |
+ return errors.New("cannot push a digest reference")
|
|
| 99 | 91 |
} |
| 100 | 92 |
|
| 101 |
- for _, association := range associations {
|
|
| 102 |
- if err := p.pushV2Tag(ctx, association); err != nil {
|
|
| 103 |
- return err |
|
| 93 |
+ // Pull all tags |
|
| 94 |
+ pushed := 0 |
|
| 95 |
+ for _, association := range p.config.ReferenceStore.ReferencesByName(p.ref) {
|
|
| 96 |
+ if namedTagged, isNamedTagged := association.Ref.(reference.NamedTagged); isNamedTagged {
|
|
| 97 |
+ pushed++ |
|
| 98 |
+ if err := p.pushV2Tag(ctx, namedTagged, association.ImageID); err != nil {
|
|
| 99 |
+ return err |
|
| 100 |
+ } |
|
| 104 | 101 |
} |
| 105 | 102 |
} |
| 106 | 103 |
|
| 104 |
+ if pushed == 0 {
|
|
| 105 |
+ return fmt.Errorf("no tags to push for %s", p.repoInfo.Name())
|
|
| 106 |
+ } |
|
| 107 |
+ |
|
| 107 | 108 |
return nil |
| 108 | 109 |
} |
| 109 | 110 |
|
| 110 |
-func (p *v2Pusher) pushV2Tag(ctx context.Context, association reference.Association) error {
|
|
| 111 |
- ref := association.Ref |
|
| 111 |
+func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, imageID image.ID) error {
|
|
| 112 | 112 |
logrus.Debugf("Pushing repository: %s", ref.String())
|
| 113 | 113 |
|
| 114 |
- img, err := p.config.ImageStore.Get(association.ImageID) |
|
| 114 |
+ img, err := p.config.ImageStore.Get(imageID) |
|
| 115 | 115 |
if err != nil {
|
| 116 | 116 |
return fmt.Errorf("could not find image from tag %s: %v", ref.String(), err)
|
| 117 | 117 |
} |
| ... | ... |
@@ -134,18 +132,7 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, association reference.Associat |
| 134 | 134 |
descriptorTemplate := v2PushDescriptor{
|
| 135 | 135 |
blobSumService: p.blobSumService, |
| 136 | 136 |
repo: p.repo, |
| 137 |
- layersPushed: &p.layersPushed, |
|
| 138 |
- confirmedV2: &p.confirmedV2, |
|
| 139 |
- } |
|
| 140 |
- |
|
| 141 |
- // Push empty layer if necessary |
|
| 142 |
- for _, h := range img.History {
|
|
| 143 |
- if h.EmptyLayer {
|
|
| 144 |
- descriptor := descriptorTemplate |
|
| 145 |
- descriptor.layer = layer.EmptyLayer |
|
| 146 |
- descriptors = []xfer.UploadDescriptor{&descriptor}
|
|
| 147 |
- break |
|
| 148 |
- } |
|
| 137 |
+ pushState: &p.pushState, |
|
| 149 | 138 |
} |
| 150 | 139 |
|
| 151 | 140 |
// Loop bounds condition is to avoid pushing the base layer on Windows. |
| ... | ... |
@@ -157,52 +144,75 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, association reference.Associat |
| 157 | 157 |
l = l.Parent() |
| 158 | 158 |
} |
| 159 | 159 |
|
| 160 |
- fsLayers, err := p.config.UploadManager.Upload(ctx, descriptors, p.config.ProgressOutput) |
|
| 161 |
- if err != nil {
|
|
| 160 |
+ if err := p.config.UploadManager.Upload(ctx, descriptors, p.config.ProgressOutput); err != nil {
|
|
| 162 | 161 |
return err |
| 163 | 162 |
} |
| 164 | 163 |
|
| 165 |
- var tag string |
|
| 166 |
- if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
|
|
| 167 |
- tag = tagged.Tag() |
|
| 168 |
- } |
|
| 169 |
- m, err := CreateV2Manifest(p.repo.Name(), tag, img, fsLayers) |
|
| 164 |
+ // Try schema2 first |
|
| 165 |
+ builder := schema2.NewManifestBuilder(p.repo.Blobs(ctx), img.RawJSON()) |
|
| 166 |
+ manifest, err := manifestFromBuilder(ctx, builder, descriptors) |
|
| 170 | 167 |
if err != nil {
|
| 171 | 168 |
return err |
| 172 | 169 |
} |
| 173 | 170 |
|
| 174 |
- logrus.Infof("Signed manifest for %s using daemon's key: %s", ref.String(), p.config.TrustKey.KeyID())
|
|
| 175 |
- signed, err := schema1.Sign(m, p.config.TrustKey) |
|
| 171 |
+ manSvc, err := p.repo.Manifests(ctx) |
|
| 176 | 172 |
if err != nil {
|
| 177 | 173 |
return err |
| 178 | 174 |
} |
| 179 | 175 |
|
| 180 |
- manifestDigest, manifestSize, err := digestFromManifest(signed, ref) |
|
| 181 |
- if err != nil {
|
|
| 182 |
- return err |
|
| 176 |
+ putOptions := []distribution.ManifestServiceOption{client.WithTag(ref.Tag())}
|
|
| 177 |
+ if _, err = manSvc.Put(ctx, manifest, putOptions...); err != nil {
|
|
| 178 |
+ logrus.Warnf("failed to upload schema2 manifest: %v - falling back to schema1", err)
|
|
| 179 |
+ |
|
| 180 |
+ builder = schema1.NewConfigManifestBuilder(p.repo.Blobs(ctx), p.config.TrustKey, p.repo.Name(), ref.Tag(), img.RawJSON()) |
|
| 181 |
+ manifest, err = manifestFromBuilder(ctx, builder, descriptors) |
|
| 182 |
+ if err != nil {
|
|
| 183 |
+ return err |
|
| 184 |
+ } |
|
| 185 |
+ |
|
| 186 |
+ if _, err = manSvc.Put(ctx, manifest, putOptions...); err != nil {
|
|
| 187 |
+ return err |
|
| 188 |
+ } |
|
| 183 | 189 |
} |
| 184 |
- if manifestDigest != "" {
|
|
| 185 |
- if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
|
|
| 186 |
- progress.Messagef(p.config.ProgressOutput, "", "%s: digest: %s size: %d", tagged.Tag(), manifestDigest, manifestSize) |
|
| 187 |
- // Signal digest to the trust client so it can sign the |
|
| 188 |
- // push, if appropriate. |
|
| 189 |
- progress.Aux(p.config.ProgressOutput, PushResult{Tag: tagged.Tag(), Digest: manifestDigest, Size: manifestSize})
|
|
| 190 |
+ |
|
| 191 |
+ var canonicalManifest []byte |
|
| 192 |
+ |
|
| 193 |
+ switch v := manifest.(type) {
|
|
| 194 |
+ case *schema1.SignedManifest: |
|
| 195 |
+ canonicalManifest = v.Canonical |
|
| 196 |
+ case *schema2.DeserializedManifest: |
|
| 197 |
+ _, canonicalManifest, err = v.Payload() |
|
| 198 |
+ if err != nil {
|
|
| 199 |
+ return err |
|
| 190 | 200 |
} |
| 191 | 201 |
} |
| 192 | 202 |
|
| 193 |
- manSvc, err := p.repo.Manifests(ctx) |
|
| 194 |
- if err != nil {
|
|
| 195 |
- return err |
|
| 203 |
+ manifestDigest := digest.FromBytes(canonicalManifest) |
|
| 204 |
+ progress.Messagef(p.config.ProgressOutput, "", "%s: digest: %s size: %d", ref.Tag(), manifestDigest, len(canonicalManifest)) |
|
| 205 |
+ // Signal digest to the trust client so it can sign the |
|
| 206 |
+ // push, if appropriate. |
|
| 207 |
+ progress.Aux(p.config.ProgressOutput, PushResult{Tag: ref.Tag(), Digest: manifestDigest, Size: len(canonicalManifest)})
|
|
| 208 |
+ |
|
| 209 |
+ return nil |
|
| 210 |
+} |
|
| 211 |
+ |
|
| 212 |
+func manifestFromBuilder(ctx context.Context, builder distribution.ManifestBuilder, descriptors []xfer.UploadDescriptor) (distribution.Manifest, error) {
|
|
| 213 |
+ // descriptors is in reverse order; iterate backwards to get references |
|
| 214 |
+ // appended in the right order. |
|
| 215 |
+ for i := len(descriptors) - 1; i >= 0; i-- {
|
|
| 216 |
+ if err := builder.AppendReference(descriptors[i].(*v2PushDescriptor)); err != nil {
|
|
| 217 |
+ return nil, err |
|
| 218 |
+ } |
|
| 196 | 219 |
} |
| 197 |
- return manSvc.Put(signed) |
|
| 220 |
+ |
|
| 221 |
+ return builder.Build(ctx) |
|
| 198 | 222 |
} |
| 199 | 223 |
|
| 200 | 224 |
type v2PushDescriptor struct {
|
| 201 | 225 |
layer layer.Layer |
| 202 | 226 |
blobSumService *metadata.BlobSumService |
| 203 | 227 |
repo distribution.Repository |
| 204 |
- layersPushed *pushMap |
|
| 205 |
- confirmedV2 *bool |
|
| 228 |
+ pushState *pushState |
|
| 206 | 229 |
} |
| 207 | 230 |
|
| 208 | 231 |
func (pd *v2PushDescriptor) Key() string {
|
| ... | ... |
@@ -217,25 +227,38 @@ func (pd *v2PushDescriptor) DiffID() layer.DiffID {
|
| 217 | 217 |
return pd.layer.DiffID() |
| 218 | 218 |
} |
| 219 | 219 |
|
| 220 |
-func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.Output) (digest.Digest, error) {
|
|
| 220 |
+func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.Output) error {
|
|
| 221 | 221 |
diffID := pd.DiffID() |
| 222 | 222 |
|
| 223 |
- logrus.Debugf("Pushing layer: %s", diffID)
|
|
| 223 |
+ pd.pushState.Lock() |
|
| 224 |
+ if _, ok := pd.pushState.remoteLayers[diffID]; ok {
|
|
| 225 |
+ // it is already known that the push is not needed and |
|
| 226 |
+ // therefore doing a stat is unnecessary |
|
| 227 |
+ pd.pushState.Unlock() |
|
| 228 |
+ progress.Update(progressOutput, pd.ID(), "Layer already exists") |
|
| 229 |
+ return nil |
|
| 230 |
+ } |
|
| 231 |
+ pd.pushState.Unlock() |
|
| 224 | 232 |
|
| 225 | 233 |
// Do we have any blobsums associated with this layer's DiffID? |
| 226 | 234 |
possibleBlobsums, err := pd.blobSumService.GetBlobSums(diffID) |
| 227 | 235 |
if err == nil {
|
| 228 |
- dgst, exists, err := blobSumAlreadyExists(ctx, possibleBlobsums, pd.repo, pd.layersPushed) |
|
| 236 |
+ descriptor, exists, err := blobSumAlreadyExists(ctx, possibleBlobsums, pd.repo, pd.pushState) |
|
| 229 | 237 |
if err != nil {
|
| 230 | 238 |
progress.Update(progressOutput, pd.ID(), "Image push failed") |
| 231 |
- return "", retryOnError(err) |
|
| 239 |
+ return retryOnError(err) |
|
| 232 | 240 |
} |
| 233 | 241 |
if exists {
|
| 234 | 242 |
progress.Update(progressOutput, pd.ID(), "Layer already exists") |
| 235 |
- return dgst, nil |
|
| 243 |
+ pd.pushState.Lock() |
|
| 244 |
+ pd.pushState.remoteLayers[diffID] = descriptor |
|
| 245 |
+ pd.pushState.Unlock() |
|
| 246 |
+ return nil |
|
| 236 | 247 |
} |
| 237 | 248 |
} |
| 238 | 249 |
|
| 250 |
+ logrus.Debugf("Pushing layer: %s", diffID)
|
|
| 251 |
+ |
|
| 239 | 252 |
// if digest was empty or not saved, or if blob does not exist on the remote repository, |
| 240 | 253 |
// then push the blob. |
| 241 | 254 |
bs := pd.repo.Blobs(ctx) |
| ... | ... |
@@ -243,13 +266,13 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress. |
| 243 | 243 |
// Send the layer |
| 244 | 244 |
layerUpload, err := bs.Create(ctx) |
| 245 | 245 |
if err != nil {
|
| 246 |
- return "", retryOnError(err) |
|
| 246 |
+ return retryOnError(err) |
|
| 247 | 247 |
} |
| 248 | 248 |
defer layerUpload.Close() |
| 249 | 249 |
|
| 250 | 250 |
arch, err := pd.layer.TarStream() |
| 251 | 251 |
if err != nil {
|
| 252 |
- return "", xfer.DoNotRetry{Err: err}
|
|
| 252 |
+ return xfer.DoNotRetry{Err: err}
|
|
| 253 | 253 |
} |
| 254 | 254 |
|
| 255 | 255 |
// don't care if this fails; best effort |
| ... | ... |
@@ -265,183 +288,62 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress. |
| 265 | 265 |
nn, err := layerUpload.ReadFrom(tee) |
| 266 | 266 |
compressedReader.Close() |
| 267 | 267 |
if err != nil {
|
| 268 |
- return "", retryOnError(err) |
|
| 268 |
+ return retryOnError(err) |
|
| 269 | 269 |
} |
| 270 | 270 |
|
| 271 | 271 |
pushDigest := digester.Digest() |
| 272 | 272 |
if _, err := layerUpload.Commit(ctx, distribution.Descriptor{Digest: pushDigest}); err != nil {
|
| 273 |
- return "", retryOnError(err) |
|
| 273 |
+ return retryOnError(err) |
|
| 274 | 274 |
} |
| 275 | 275 |
|
| 276 |
- // If Commit succeded, that's an indication that the remote registry |
|
| 277 |
- // speaks the v2 protocol. |
|
| 278 |
- *pd.confirmedV2 = true |
|
| 279 |
- |
|
| 280 | 276 |
logrus.Debugf("uploaded layer %s (%s), %d bytes", diffID, pushDigest, nn)
|
| 281 | 277 |
progress.Update(progressOutput, pd.ID(), "Pushed") |
| 282 | 278 |
|
| 283 | 279 |
// Cache mapping from this layer's DiffID to the blobsum |
| 284 | 280 |
if err := pd.blobSumService.Add(diffID, pushDigest); err != nil {
|
| 285 |
- return "", xfer.DoNotRetry{Err: err}
|
|
| 281 |
+ return xfer.DoNotRetry{Err: err}
|
|
| 282 |
+ } |
|
| 283 |
+ |
|
| 284 |
+ pd.pushState.Lock() |
|
| 285 |
+ |
|
| 286 |
+ // If Commit succeded, that's an indication that the remote registry |
|
| 287 |
+ // speaks the v2 protocol. |
|
| 288 |
+ pd.pushState.confirmedV2 = true |
|
| 289 |
+ |
|
| 290 |
+ pd.pushState.remoteLayers[diffID] = distribution.Descriptor{
|
|
| 291 |
+ Digest: pushDigest, |
|
| 292 |
+ MediaType: schema2.MediaTypeLayer, |
|
| 293 |
+ Size: nn, |
|
| 286 | 294 |
} |
| 287 | 295 |
|
| 288 |
- pd.layersPushed.Lock() |
|
| 289 |
- pd.layersPushed.layersPushed[pushDigest] = true |
|
| 290 |
- pd.layersPushed.Unlock() |
|
| 296 |
+ pd.pushState.Unlock() |
|
| 291 | 297 |
|
| 292 |
- return pushDigest, nil |
|
| 298 |
+ return nil |
|
| 299 |
+} |
|
| 300 |
+ |
|
| 301 |
+func (pd *v2PushDescriptor) Descriptor() distribution.Descriptor {
|
|
| 302 |
+ // Not necessary to lock pushStatus because this is always |
|
| 303 |
+ // called after all the mutation in pushStatus. |
|
| 304 |
+ // By the time this function is called, every layer will have |
|
| 305 |
+ // an entry in remoteLayers. |
|
| 306 |
+ return pd.pushState.remoteLayers[pd.DiffID()] |
|
| 293 | 307 |
} |
| 294 | 308 |
|
| 295 | 309 |
// blobSumAlreadyExists checks if the registry already know about any of the |
| 296 | 310 |
// blobsums passed in the "blobsums" slice. If it finds one that the registry |
| 297 | 311 |
// knows about, it returns the known digest and "true". |
| 298 |
-func blobSumAlreadyExists(ctx context.Context, blobsums []digest.Digest, repo distribution.Repository, layersPushed *pushMap) (digest.Digest, bool, error) {
|
|
| 299 |
- layersPushed.Lock() |
|
| 300 |
- for _, dgst := range blobsums {
|
|
| 301 |
- if layersPushed.layersPushed[dgst] {
|
|
| 302 |
- // it is already known that the push is not needed and |
|
| 303 |
- // therefore doing a stat is unnecessary |
|
| 304 |
- layersPushed.Unlock() |
|
| 305 |
- return dgst, true, nil |
|
| 306 |
- } |
|
| 307 |
- } |
|
| 308 |
- layersPushed.Unlock() |
|
| 309 |
- |
|
| 312 |
+func blobSumAlreadyExists(ctx context.Context, blobsums []digest.Digest, repo distribution.Repository, pushState *pushState) (distribution.Descriptor, bool, error) {
|
|
| 310 | 313 |
for _, dgst := range blobsums {
|
| 311 |
- _, err := repo.Blobs(ctx).Stat(ctx, dgst) |
|
| 314 |
+ descriptor, err := repo.Blobs(ctx).Stat(ctx, dgst) |
|
| 312 | 315 |
switch err {
|
| 313 | 316 |
case nil: |
| 314 |
- return dgst, true, nil |
|
| 317 |
+ descriptor.MediaType = schema2.MediaTypeLayer |
|
| 318 |
+ return descriptor, true, nil |
|
| 315 | 319 |
case distribution.ErrBlobUnknown: |
| 316 | 320 |
// nop |
| 317 | 321 |
default: |
| 318 |
- return "", false, err |
|
| 322 |
+ return distribution.Descriptor{}, false, err
|
|
| 319 | 323 |
} |
| 320 | 324 |
} |
| 321 |
- return "", false, nil |
|
| 322 |
-} |
|
| 323 |
- |
|
| 324 |
-// CreateV2Manifest creates a V2 manifest from an image config and set of |
|
| 325 |
-// FSLayer digests. |
|
| 326 |
-// FIXME: This should be moved to the distribution repo, since it will also |
|
| 327 |
-// be useful for converting new manifests to the old format. |
|
| 328 |
-func CreateV2Manifest(name, tag string, img *image.Image, fsLayers map[layer.DiffID]digest.Digest) (*schema1.Manifest, error) {
|
|
| 329 |
- if len(img.History) == 0 {
|
|
| 330 |
- return nil, errors.New("empty history when trying to create V2 manifest")
|
|
| 331 |
- } |
|
| 332 |
- |
|
| 333 |
- // Generate IDs for each layer |
|
| 334 |
- // For non-top-level layers, create fake V1Compatibility strings that |
|
| 335 |
- // fit the format and don't collide with anything else, but don't |
|
| 336 |
- // result in runnable images on their own. |
|
| 337 |
- type v1Compatibility struct {
|
|
| 338 |
- ID string `json:"id"` |
|
| 339 |
- Parent string `json:"parent,omitempty"` |
|
| 340 |
- Comment string `json:"comment,omitempty"` |
|
| 341 |
- Created time.Time `json:"created"` |
|
| 342 |
- ContainerConfig struct {
|
|
| 343 |
- Cmd []string |
|
| 344 |
- } `json:"container_config,omitempty"` |
|
| 345 |
- ThrowAway bool `json:"throwaway,omitempty"` |
|
| 346 |
- } |
|
| 347 |
- |
|
| 348 |
- fsLayerList := make([]schema1.FSLayer, len(img.History)) |
|
| 349 |
- history := make([]schema1.History, len(img.History)) |
|
| 350 |
- |
|
| 351 |
- parent := "" |
|
| 352 |
- layerCounter := 0 |
|
| 353 |
- for i, h := range img.History {
|
|
| 354 |
- if i == len(img.History)-1 {
|
|
| 355 |
- break |
|
| 356 |
- } |
|
| 357 |
- |
|
| 358 |
- var diffID layer.DiffID |
|
| 359 |
- if h.EmptyLayer {
|
|
| 360 |
- diffID = layer.EmptyLayer.DiffID() |
|
| 361 |
- } else {
|
|
| 362 |
- if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
| 363 |
- return nil, errors.New("too many non-empty layers in History section")
|
|
| 364 |
- } |
|
| 365 |
- diffID = img.RootFS.DiffIDs[layerCounter] |
|
| 366 |
- layerCounter++ |
|
| 367 |
- } |
|
| 368 |
- |
|
| 369 |
- fsLayer, present := fsLayers[diffID] |
|
| 370 |
- if !present {
|
|
| 371 |
- return nil, fmt.Errorf("missing layer in CreateV2Manifest: %s", diffID.String())
|
|
| 372 |
- } |
|
| 373 |
- dgst, err := digest.FromBytes([]byte(fsLayer.Hex() + " " + parent)) |
|
| 374 |
- if err != nil {
|
|
| 375 |
- return nil, err |
|
| 376 |
- } |
|
| 377 |
- v1ID := dgst.Hex() |
|
| 378 |
- |
|
| 379 |
- v1Compatibility := v1Compatibility{
|
|
| 380 |
- ID: v1ID, |
|
| 381 |
- Parent: parent, |
|
| 382 |
- Comment: h.Comment, |
|
| 383 |
- Created: h.Created, |
|
| 384 |
- } |
|
| 385 |
- v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy}
|
|
| 386 |
- if h.EmptyLayer {
|
|
| 387 |
- v1Compatibility.ThrowAway = true |
|
| 388 |
- } |
|
| 389 |
- jsonBytes, err := json.Marshal(&v1Compatibility) |
|
| 390 |
- if err != nil {
|
|
| 391 |
- return nil, err |
|
| 392 |
- } |
|
| 393 |
- |
|
| 394 |
- reversedIndex := len(img.History) - i - 1 |
|
| 395 |
- history[reversedIndex].V1Compatibility = string(jsonBytes) |
|
| 396 |
- fsLayerList[reversedIndex] = schema1.FSLayer{BlobSum: fsLayer}
|
|
| 397 |
- |
|
| 398 |
- parent = v1ID |
|
| 399 |
- } |
|
| 400 |
- |
|
| 401 |
- latestHistory := img.History[len(img.History)-1] |
|
| 402 |
- |
|
| 403 |
- var diffID layer.DiffID |
|
| 404 |
- if latestHistory.EmptyLayer {
|
|
| 405 |
- diffID = layer.EmptyLayer.DiffID() |
|
| 406 |
- } else {
|
|
| 407 |
- if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
| 408 |
- return nil, errors.New("too many non-empty layers in History section")
|
|
| 409 |
- } |
|
| 410 |
- diffID = img.RootFS.DiffIDs[layerCounter] |
|
| 411 |
- } |
|
| 412 |
- fsLayer, present := fsLayers[diffID] |
|
| 413 |
- if !present {
|
|
| 414 |
- return nil, fmt.Errorf("missing layer in CreateV2Manifest: %s", diffID.String())
|
|
| 415 |
- } |
|
| 416 |
- |
|
| 417 |
- dgst, err := digest.FromBytes([]byte(fsLayer.Hex() + " " + parent + " " + string(img.RawJSON()))) |
|
| 418 |
- if err != nil {
|
|
| 419 |
- return nil, err |
|
| 420 |
- } |
|
| 421 |
- fsLayerList[0] = schema1.FSLayer{BlobSum: fsLayer}
|
|
| 422 |
- |
|
| 423 |
- // Top-level v1compatibility string should be a modified version of the |
|
| 424 |
- // image config. |
|
| 425 |
- transformedConfig, err := v1.MakeV1ConfigFromConfig(img, dgst.Hex(), parent, latestHistory.EmptyLayer) |
|
| 426 |
- if err != nil {
|
|
| 427 |
- return nil, err |
|
| 428 |
- } |
|
| 429 |
- |
|
| 430 |
- history[0].V1Compatibility = string(transformedConfig) |
|
| 431 |
- |
|
| 432 |
- // windows-only baselayer setup |
|
| 433 |
- if err := setupBaseLayer(history, *img.RootFS); err != nil {
|
|
| 434 |
- return nil, err |
|
| 435 |
- } |
|
| 436 |
- |
|
| 437 |
- return &schema1.Manifest{
|
|
| 438 |
- Versioned: manifest.Versioned{
|
|
| 439 |
- SchemaVersion: 1, |
|
| 440 |
- }, |
|
| 441 |
- Name: name, |
|
| 442 |
- Tag: tag, |
|
| 443 |
- Architecture: img.Architecture, |
|
| 444 |
- FSLayers: fsLayerList, |
|
| 445 |
- History: history, |
|
| 446 |
- }, nil |
|
| 325 |
+ return distribution.Descriptor{}, false, nil
|
|
| 447 | 326 |
} |
| 448 | 327 |
deleted file mode 100644 |
| ... | ... |
@@ -1,176 +0,0 @@ |
| 1 |
-package distribution |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "reflect" |
|
| 5 |
- "testing" |
|
| 6 |
- |
|
| 7 |
- "github.com/docker/distribution/digest" |
|
| 8 |
- "github.com/docker/distribution/manifest/schema1" |
|
| 9 |
- "github.com/docker/docker/image" |
|
| 10 |
- "github.com/docker/docker/layer" |
|
| 11 |
-) |
|
| 12 |
- |
|
| 13 |
-func TestCreateV2Manifest(t *testing.T) {
|
|
| 14 |
- imgJSON := `{
|
|
| 15 |
- "architecture": "amd64", |
|
| 16 |
- "config": {
|
|
| 17 |
- "AttachStderr": false, |
|
| 18 |
- "AttachStdin": false, |
|
| 19 |
- "AttachStdout": false, |
|
| 20 |
- "Cmd": [ |
|
| 21 |
- "/bin/sh", |
|
| 22 |
- "-c", |
|
| 23 |
- "echo hi" |
|
| 24 |
- ], |
|
| 25 |
- "Domainname": "", |
|
| 26 |
- "Entrypoint": null, |
|
| 27 |
- "Env": [ |
|
| 28 |
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", |
|
| 29 |
- "derived=true", |
|
| 30 |
- "asdf=true" |
|
| 31 |
- ], |
|
| 32 |
- "Hostname": "23304fc829f9", |
|
| 33 |
- "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246", |
|
| 34 |
- "Labels": {},
|
|
| 35 |
- "OnBuild": [], |
|
| 36 |
- "OpenStdin": false, |
|
| 37 |
- "StdinOnce": false, |
|
| 38 |
- "Tty": false, |
|
| 39 |
- "User": "", |
|
| 40 |
- "Volumes": null, |
|
| 41 |
- "WorkingDir": "" |
|
| 42 |
- }, |
|
| 43 |
- "container": "e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001", |
|
| 44 |
- "container_config": {
|
|
| 45 |
- "AttachStderr": false, |
|
| 46 |
- "AttachStdin": false, |
|
| 47 |
- "AttachStdout": false, |
|
| 48 |
- "Cmd": [ |
|
| 49 |
- "/bin/sh", |
|
| 50 |
- "-c", |
|
| 51 |
- "#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]" |
|
| 52 |
- ], |
|
| 53 |
- "Domainname": "", |
|
| 54 |
- "Entrypoint": null, |
|
| 55 |
- "Env": [ |
|
| 56 |
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", |
|
| 57 |
- "derived=true", |
|
| 58 |
- "asdf=true" |
|
| 59 |
- ], |
|
| 60 |
- "Hostname": "23304fc829f9", |
|
| 61 |
- "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246", |
|
| 62 |
- "Labels": {},
|
|
| 63 |
- "OnBuild": [], |
|
| 64 |
- "OpenStdin": false, |
|
| 65 |
- "StdinOnce": false, |
|
| 66 |
- "Tty": false, |
|
| 67 |
- "User": "", |
|
| 68 |
- "Volumes": null, |
|
| 69 |
- "WorkingDir": "" |
|
| 70 |
- }, |
|
| 71 |
- "created": "2015-11-04T23:06:32.365666163Z", |
|
| 72 |
- "docker_version": "1.9.0-dev", |
|
| 73 |
- "history": [ |
|
| 74 |
- {
|
|
| 75 |
- "created": "2015-10-31T22:22:54.690851953Z", |
|
| 76 |
- "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" |
|
| 77 |
- }, |
|
| 78 |
- {
|
|
| 79 |
- "created": "2015-10-31T22:22:55.613815829Z", |
|
| 80 |
- "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]" |
|
| 81 |
- }, |
|
| 82 |
- {
|
|
| 83 |
- "created": "2015-11-04T23:06:30.934316144Z", |
|
| 84 |
- "created_by": "/bin/sh -c #(nop) ENV derived=true", |
|
| 85 |
- "empty_layer": true |
|
| 86 |
- }, |
|
| 87 |
- {
|
|
| 88 |
- "created": "2015-11-04T23:06:31.192097572Z", |
|
| 89 |
- "created_by": "/bin/sh -c #(nop) ENV asdf=true", |
|
| 90 |
- "empty_layer": true |
|
| 91 |
- }, |
|
| 92 |
- {
|
|
| 93 |
- "created": "2015-11-04T23:06:32.083868454Z", |
|
| 94 |
- "created_by": "/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024" |
|
| 95 |
- }, |
|
| 96 |
- {
|
|
| 97 |
- "created": "2015-11-04T23:06:32.365666163Z", |
|
| 98 |
- "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]", |
|
| 99 |
- "empty_layer": true |
|
| 100 |
- } |
|
| 101 |
- ], |
|
| 102 |
- "os": "linux", |
|
| 103 |
- "rootfs": {
|
|
| 104 |
- "diff_ids": [ |
|
| 105 |
- "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", |
|
| 106 |
- "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", |
|
| 107 |
- "sha256:13f53e08df5a220ab6d13c58b2bf83a59cbdc2e04d0a3f041ddf4b0ba4112d49" |
|
| 108 |
- ], |
|
| 109 |
- "type": "layers" |
|
| 110 |
- } |
|
| 111 |
-}` |
|
| 112 |
- |
|
| 113 |
- // To fill in rawJSON |
|
| 114 |
- img, err := image.NewFromJSON([]byte(imgJSON)) |
|
| 115 |
- if err != nil {
|
|
| 116 |
- t.Fatalf("json decoding failed: %v", err)
|
|
| 117 |
- } |
|
| 118 |
- |
|
| 119 |
- fsLayers := map[layer.DiffID]digest.Digest{
|
|
| 120 |
- layer.DiffID("sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1"): digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"),
|
|
| 121 |
- layer.DiffID("sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"): digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"),
|
|
| 122 |
- layer.DiffID("sha256:13f53e08df5a220ab6d13c58b2bf83a59cbdc2e04d0a3f041ddf4b0ba4112d49"): digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"),
|
|
| 123 |
- } |
|
| 124 |
- |
|
| 125 |
- manifest, err := CreateV2Manifest("testrepo", "testtag", img, fsLayers)
|
|
| 126 |
- if err != nil {
|
|
| 127 |
- t.Fatalf("CreateV2Manifest returned error: %v", err)
|
|
| 128 |
- } |
|
| 129 |
- |
|
| 130 |
- if manifest.Versioned.SchemaVersion != 1 {
|
|
| 131 |
- t.Fatal("SchemaVersion != 1")
|
|
| 132 |
- } |
|
| 133 |
- if manifest.Name != "testrepo" {
|
|
| 134 |
- t.Fatal("incorrect name in manifest")
|
|
| 135 |
- } |
|
| 136 |
- if manifest.Tag != "testtag" {
|
|
| 137 |
- t.Fatal("incorrect tag in manifest")
|
|
| 138 |
- } |
|
| 139 |
- if manifest.Architecture != "amd64" {
|
|
| 140 |
- t.Fatal("incorrect arch in manifest")
|
|
| 141 |
- } |
|
| 142 |
- |
|
| 143 |
- expectedFSLayers := []schema1.FSLayer{
|
|
| 144 |
- {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
|
| 145 |
- {BlobSum: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
| 146 |
- {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
|
| 147 |
- {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
|
| 148 |
- {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
|
| 149 |
- {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
| 150 |
- } |
|
| 151 |
- |
|
| 152 |
- if len(manifest.FSLayers) != len(expectedFSLayers) {
|
|
| 153 |
- t.Fatalf("wrong number of FSLayers: %d", len(manifest.FSLayers))
|
|
| 154 |
- } |
|
| 155 |
- if !reflect.DeepEqual(manifest.FSLayers, expectedFSLayers) {
|
|
| 156 |
- t.Fatal("wrong FSLayers list")
|
|
| 157 |
- } |
|
| 158 |
- |
|
| 159 |
- expectedV1Compatibility := []string{
|
|
| 160 |
- `{"architecture":"amd64","config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","echo hi"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"container":"e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001","container_config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"created":"2015-11-04T23:06:32.365666163Z","docker_version":"1.9.0-dev","id":"d728140d3fd23dfcac505954af0b2224b3579b177029eded62916579eb19ac64","os":"linux","parent":"0594e66a9830fa5ba73b66349eb221ea4beb6bac8d2148b90a0f371f8d67bcd5","throwaway":true}`,
|
|
| 161 |
- `{"id":"0594e66a9830fa5ba73b66349eb221ea4beb6bac8d2148b90a0f371f8d67bcd5","parent":"39bc0dbed47060dd8952b048e73744ae471fe50354d2c267d308292c53b83ce1","created":"2015-11-04T23:06:32.083868454Z","container_config":{"Cmd":["/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"]}}`,
|
|
| 162 |
- `{"id":"39bc0dbed47060dd8952b048e73744ae471fe50354d2c267d308292c53b83ce1","parent":"875d7f206c023dc979e1677567a01364074f82b61e220c9b83a4610170490381","created":"2015-11-04T23:06:31.192097572Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV asdf=true"]},"throwaway":true}`,
|
|
| 163 |
- `{"id":"875d7f206c023dc979e1677567a01364074f82b61e220c9b83a4610170490381","parent":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","created":"2015-11-04T23:06:30.934316144Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV derived=true"]},"throwaway":true}`,
|
|
| 164 |
- `{"id":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","parent":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:55.613815829Z","container_config":{"Cmd":["/bin/sh -c #(nop) CMD [\"sh\"]"]}}`,
|
|
| 165 |
- `{"id":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:54.690851953Z","container_config":{"Cmd":["/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"]}}`,
|
|
| 166 |
- } |
|
| 167 |
- |
|
| 168 |
- if len(manifest.History) != len(expectedV1Compatibility) {
|
|
| 169 |
- t.Fatalf("wrong number of history entries: %d", len(manifest.History))
|
|
| 170 |
- } |
|
| 171 |
- for i := range expectedV1Compatibility {
|
|
| 172 |
- if manifest.History[i].V1Compatibility != expectedV1Compatibility[i] {
|
|
| 173 |
- t.Fatalf("wrong V1Compatibility %d. expected:\n%s\ngot:\n%s", i, expectedV1Compatibility[i], manifest.History[i].V1Compatibility)
|
|
| 174 |
- } |
|
| 175 |
- } |
|
| 176 |
-} |
| 177 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,12 +0,0 @@ |
| 1 |
-// +build !windows |
|
| 2 |
- |
|
| 3 |
-package distribution |
|
| 4 |
- |
|
| 5 |
-import ( |
|
| 6 |
- "github.com/docker/distribution/manifest/schema1" |
|
| 7 |
- "github.com/docker/docker/image" |
|
| 8 |
-) |
|
| 9 |
- |
|
| 10 |
-func setupBaseLayer(history []schema1.History, rootFS image.RootFS) error {
|
|
| 11 |
- return nil |
|
| 12 |
-} |
| 13 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,28 +0,0 @@ |
| 1 |
-// +build windows |
|
| 2 |
- |
|
| 3 |
-package distribution |
|
| 4 |
- |
|
| 5 |
-import ( |
|
| 6 |
- "encoding/json" |
|
| 7 |
- |
|
| 8 |
- "github.com/docker/distribution/manifest/schema1" |
|
| 9 |
- "github.com/docker/docker/image" |
|
| 10 |
-) |
|
| 11 |
- |
|
| 12 |
-func setupBaseLayer(history []schema1.History, rootFS image.RootFS) error {
|
|
| 13 |
- var v1Config map[string]*json.RawMessage |
|
| 14 |
- if err := json.Unmarshal([]byte(history[len(history)-1].V1Compatibility), &v1Config); err != nil {
|
|
| 15 |
- return err |
|
| 16 |
- } |
|
| 17 |
- baseID, err := json.Marshal(rootFS.BaseLayerID()) |
|
| 18 |
- if err != nil {
|
|
| 19 |
- return err |
|
| 20 |
- } |
|
| 21 |
- v1Config["parent"] = (*json.RawMessage)(&baseID) |
|
| 22 |
- configJSON, err := json.Marshal(v1Config) |
|
| 23 |
- if err != nil {
|
|
| 24 |
- return err |
|
| 25 |
- } |
|
| 26 |
- history[len(history)-1].V1Compatibility = string(configJSON) |
|
| 27 |
- return nil |
|
| 28 |
-} |
| ... | ... |
@@ -8,16 +8,12 @@ import ( |
| 8 | 8 |
"strings" |
| 9 | 9 |
"time" |
| 10 | 10 |
|
| 11 |
- "github.com/Sirupsen/logrus" |
|
| 12 | 11 |
"github.com/docker/distribution" |
| 13 |
- "github.com/docker/distribution/digest" |
|
| 14 |
- "github.com/docker/distribution/manifest/schema1" |
|
| 15 | 12 |
"github.com/docker/distribution/registry/api/errcode" |
| 16 | 13 |
"github.com/docker/distribution/registry/client" |
| 17 | 14 |
"github.com/docker/distribution/registry/client/auth" |
| 18 | 15 |
"github.com/docker/distribution/registry/client/transport" |
| 19 | 16 |
"github.com/docker/docker/distribution/xfer" |
| 20 |
- "github.com/docker/docker/reference" |
|
| 21 | 17 |
"github.com/docker/docker/registry" |
| 22 | 18 |
"github.com/docker/engine-api/types" |
| 23 | 19 |
"golang.org/x/net/context" |
| ... | ... |
@@ -125,20 +121,6 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end |
| 125 | 125 |
return repo, foundVersion, err |
| 126 | 126 |
} |
| 127 | 127 |
|
| 128 |
-func digestFromManifest(m *schema1.SignedManifest, name reference.Named) (digest.Digest, int, error) {
|
|
| 129 |
- payload, err := m.Payload() |
|
| 130 |
- if err != nil {
|
|
| 131 |
- // If this failed, the signatures section was corrupted |
|
| 132 |
- // or missing. Treat the entire manifest as the payload. |
|
| 133 |
- payload = m.Raw |
|
| 134 |
- } |
|
| 135 |
- manifestDigest, err := digest.FromBytes(payload) |
|
| 136 |
- if err != nil {
|
|
| 137 |
- logrus.Infof("Could not compute manifest digest for %s:%s : %v", name.Name(), m.Tag, err)
|
|
| 138 |
- } |
|
| 139 |
- return manifestDigest, len(payload), nil |
|
| 140 |
-} |
|
| 141 |
- |
|
| 142 | 128 |
type existingTokenHandler struct {
|
| 143 | 129 |
token string |
| 144 | 130 |
} |
| ... | ... |
@@ -65,12 +65,7 @@ func createChainIDFromParent(parent layer.ChainID, dgsts ...layer.DiffID) layer. |
| 65 | 65 |
return createChainIDFromParent(layer.ChainID(dgsts[0]), dgsts[1:]...) |
| 66 | 66 |
} |
| 67 | 67 |
// H = "H(n-1) SHA256(n)" |
| 68 |
- dgst, err := digest.FromBytes([]byte(string(parent) + " " + string(dgsts[0]))) |
|
| 69 |
- if err != nil {
|
|
| 70 |
- // Digest calculation is not expected to throw an error, |
|
| 71 |
- // any error at this point is a program error |
|
| 72 |
- panic(err) |
|
| 73 |
- } |
|
| 68 |
+ dgst := digest.FromBytes([]byte(string(parent) + " " + string(dgsts[0]))) |
|
| 74 | 69 |
return createChainIDFromParent(layer.ChainID(dgst), dgsts[1:]...) |
| 75 | 70 |
} |
| 76 | 71 |
|
| ... | ... |
@@ -92,11 +87,7 @@ func (ls *mockLayerStore) Register(reader io.Reader, parentID layer.ChainID) (la |
| 92 | 92 |
if err != nil {
|
| 93 | 93 |
return nil, err |
| 94 | 94 |
} |
| 95 |
- diffID, err := digest.FromBytes(l.layerData.Bytes()) |
|
| 96 |
- if err != nil {
|
|
| 97 |
- return nil, err |
|
| 98 |
- } |
|
| 99 |
- l.diffID = layer.DiffID(diffID) |
|
| 95 |
+ l.diffID = layer.DiffID(digest.FromBytes(l.layerData.Bytes())) |
|
| 100 | 96 |
l.chainID = createChainIDFromParent(parentID, l.diffID) |
| 101 | 97 |
|
| 102 | 98 |
ls.layers[l.chainID] = l |
| ... | ... |
@@ -5,7 +5,6 @@ import ( |
| 5 | 5 |
"time" |
| 6 | 6 |
|
| 7 | 7 |
"github.com/Sirupsen/logrus" |
| 8 |
- "github.com/docker/distribution/digest" |
|
| 9 | 8 |
"github.com/docker/docker/layer" |
| 10 | 9 |
"github.com/docker/docker/pkg/progress" |
| 11 | 10 |
"golang.org/x/net/context" |
| ... | ... |
@@ -30,7 +29,6 @@ type uploadTransfer struct {
|
| 30 | 30 |
Transfer |
| 31 | 31 |
|
| 32 | 32 |
diffID layer.DiffID |
| 33 |
- digest digest.Digest |
|
| 34 | 33 |
err error |
| 35 | 34 |
} |
| 36 | 35 |
|
| ... | ... |
@@ -43,16 +41,15 @@ type UploadDescriptor interface {
|
| 43 | 43 |
// DiffID should return the DiffID for this layer. |
| 44 | 44 |
DiffID() layer.DiffID |
| 45 | 45 |
// Upload is called to perform the Upload. |
| 46 |
- Upload(ctx context.Context, progressOutput progress.Output) (digest.Digest, error) |
|
| 46 |
+ Upload(ctx context.Context, progressOutput progress.Output) error |
|
| 47 | 47 |
} |
| 48 | 48 |
|
| 49 | 49 |
// Upload is a blocking function which ensures the listed layers are present on |
| 50 | 50 |
// the remote registry. It uses the string returned by the Key method to |
| 51 | 51 |
// deduplicate uploads. |
| 52 |
-func (lum *LayerUploadManager) Upload(ctx context.Context, layers []UploadDescriptor, progressOutput progress.Output) (map[layer.DiffID]digest.Digest, error) {
|
|
| 52 |
+func (lum *LayerUploadManager) Upload(ctx context.Context, layers []UploadDescriptor, progressOutput progress.Output) error {
|
|
| 53 | 53 |
var ( |
| 54 | 54 |
uploads []*uploadTransfer |
| 55 |
- digests = make(map[layer.DiffID]digest.Digest) |
|
| 56 | 55 |
dedupDescriptors = make(map[string]struct{})
|
| 57 | 56 |
) |
| 58 | 57 |
|
| ... | ... |
@@ -74,16 +71,15 @@ func (lum *LayerUploadManager) Upload(ctx context.Context, layers []UploadDescri |
| 74 | 74 |
for _, upload := range uploads {
|
| 75 | 75 |
select {
|
| 76 | 76 |
case <-ctx.Done(): |
| 77 |
- return nil, ctx.Err() |
|
| 77 |
+ return ctx.Err() |
|
| 78 | 78 |
case <-upload.Transfer.Done(): |
| 79 | 79 |
if upload.err != nil {
|
| 80 |
- return nil, upload.err |
|
| 80 |
+ return upload.err |
|
| 81 | 81 |
} |
| 82 |
- digests[upload.diffID] = upload.digest |
|
| 83 | 82 |
} |
| 84 | 83 |
} |
| 85 | 84 |
|
| 86 |
- return digests, nil |
|
| 85 |
+ return nil |
|
| 87 | 86 |
} |
| 88 | 87 |
|
| 89 | 88 |
func (lum *LayerUploadManager) makeUploadFunc(descriptor UploadDescriptor) DoFunc {
|
| ... | ... |
@@ -109,9 +105,8 @@ func (lum *LayerUploadManager) makeUploadFunc(descriptor UploadDescriptor) DoFun |
| 109 | 109 |
|
| 110 | 110 |
retries := 0 |
| 111 | 111 |
for {
|
| 112 |
- digest, err := descriptor.Upload(u.Transfer.Context(), progressOutput) |
|
| 112 |
+ err := descriptor.Upload(u.Transfer.Context(), progressOutput) |
|
| 113 | 113 |
if err == nil {
|
| 114 |
- u.digest = digest |
|
| 115 | 114 |
break |
| 116 | 115 |
} |
| 117 | 116 |
|
| ... | ... |
@@ -36,12 +36,12 @@ func (u *mockUploadDescriptor) DiffID() layer.DiffID {
|
| 36 | 36 |
} |
| 37 | 37 |
|
| 38 | 38 |
// Upload is called to perform the upload. |
| 39 |
-func (u *mockUploadDescriptor) Upload(ctx context.Context, progressOutput progress.Output) (digest.Digest, error) {
|
|
| 39 |
+func (u *mockUploadDescriptor) Upload(ctx context.Context, progressOutput progress.Output) error {
|
|
| 40 | 40 |
if u.currentUploads != nil {
|
| 41 | 41 |
defer atomic.AddInt32(u.currentUploads, -1) |
| 42 | 42 |
|
| 43 | 43 |
if atomic.AddInt32(u.currentUploads, 1) > maxUploadConcurrency {
|
| 44 |
- return "", errors.New("concurrency limit exceeded")
|
|
| 44 |
+ return errors.New("concurrency limit exceeded")
|
|
| 45 | 45 |
} |
| 46 | 46 |
} |
| 47 | 47 |
|
| ... | ... |
@@ -49,7 +49,7 @@ func (u *mockUploadDescriptor) Upload(ctx context.Context, progressOutput progre |
| 49 | 49 |
for i := int64(0); i <= 10; i++ {
|
| 50 | 50 |
select {
|
| 51 | 51 |
case <-ctx.Done(): |
| 52 |
- return "", ctx.Err() |
|
| 52 |
+ return ctx.Err() |
|
| 53 | 53 |
case <-time.After(10 * time.Millisecond): |
| 54 | 54 |
progressOutput.WriteProgress(progress.Progress{ID: u.ID(), Current: i, Total: 10})
|
| 55 | 55 |
} |
| ... | ... |
@@ -57,12 +57,10 @@ func (u *mockUploadDescriptor) Upload(ctx context.Context, progressOutput progre |
| 57 | 57 |
|
| 58 | 58 |
if u.simulateRetries != 0 {
|
| 59 | 59 |
u.simulateRetries-- |
| 60 |
- return "", errors.New("simulating retry")
|
|
| 60 |
+ return errors.New("simulating retry")
|
|
| 61 | 61 |
} |
| 62 | 62 |
|
| 63 |
- // For the mock implementation, use SHA256(DiffID) as the returned |
|
| 64 |
- // digest. |
|
| 65 |
- return digest.FromBytes([]byte(u.diffID.String())) |
|
| 63 |
+ return nil |
|
| 66 | 64 |
} |
| 67 | 65 |
|
| 68 | 66 |
func uploadDescriptors(currentUploads *int32) []UploadDescriptor {
|
| ... | ... |
@@ -101,26 +99,13 @@ func TestSuccessfulUpload(t *testing.T) {
|
| 101 | 101 |
var currentUploads int32 |
| 102 | 102 |
descriptors := uploadDescriptors(¤tUploads) |
| 103 | 103 |
|
| 104 |
- digests, err := lum.Upload(context.Background(), descriptors, progress.ChanOutput(progressChan)) |
|
| 104 |
+ err := lum.Upload(context.Background(), descriptors, progress.ChanOutput(progressChan)) |
|
| 105 | 105 |
if err != nil {
|
| 106 | 106 |
t.Fatalf("upload error: %v", err)
|
| 107 | 107 |
} |
| 108 | 108 |
|
| 109 | 109 |
close(progressChan) |
| 110 | 110 |
<-progressDone |
| 111 |
- |
|
| 112 |
- if len(digests) != len(expectedDigests) {
|
|
| 113 |
- t.Fatal("wrong number of keys in digests map")
|
|
| 114 |
- } |
|
| 115 |
- |
|
| 116 |
- for key, val := range expectedDigests {
|
|
| 117 |
- if digests[key] != val {
|
|
| 118 |
- t.Fatalf("mismatch in digest array for key %v (expected %v, got %v)", key, val, digests[key])
|
|
| 119 |
- } |
|
| 120 |
- if receivedProgress[key.String()] != 10 {
|
|
| 121 |
- t.Fatalf("missing or wrong progress output for %v", key)
|
|
| 122 |
- } |
|
| 123 |
- } |
|
| 124 | 111 |
} |
| 125 | 112 |
|
| 126 | 113 |
func TestCancelledUpload(t *testing.T) {
|
| ... | ... |
@@ -143,7 +128,7 @@ func TestCancelledUpload(t *testing.T) {
|
| 143 | 143 |
}() |
| 144 | 144 |
|
| 145 | 145 |
descriptors := uploadDescriptors(nil) |
| 146 |
- _, err := lum.Upload(ctx, descriptors, progress.ChanOutput(progressChan)) |
|
| 146 |
+ err := lum.Upload(ctx, descriptors, progress.ChanOutput(progressChan)) |
|
| 147 | 147 |
if err != context.Canceled {
|
| 148 | 148 |
t.Fatal("expected upload to be cancelled")
|
| 149 | 149 |
} |
| ... | ... |
@@ -44,7 +44,7 @@ clone git github.com/boltdb/bolt v1.1.0 |
| 44 | 44 |
clone git github.com/miekg/dns d27455715200c7d3e321a1e5cadb27c9ee0b0f02 |
| 45 | 45 |
|
| 46 | 46 |
# get graph and distribution packages |
| 47 |
-clone git github.com/docker/distribution 568bf038af6d65b376165d02886b1c7fcaef1f61 |
|
| 47 |
+clone git github.com/docker/distribution a7ae88da459b98b481a245e5b1750134724ac67d |
|
| 48 | 48 |
clone git github.com/vbatts/tar-split v0.9.11 |
| 49 | 49 |
|
| 50 | 50 |
# get desired notary commit, might also need to be updated in Dockerfile |
| ... | ... |
@@ -101,11 +101,7 @@ func (s *fs) get(id ID) ([]byte, error) {
|
| 101 | 101 |
} |
| 102 | 102 |
|
| 103 | 103 |
// todo: maybe optional |
| 104 |
- validated, err := digest.FromBytes(content) |
|
| 105 |
- if err != nil {
|
|
| 106 |
- return nil, err |
|
| 107 |
- } |
|
| 108 |
- if ID(validated) != id {
|
|
| 104 |
+ if ID(digest.FromBytes(content)) != id {
|
|
| 109 | 105 |
return nil, fmt.Errorf("failed to verify image: %v", id)
|
| 110 | 106 |
} |
| 111 | 107 |
|
| ... | ... |
@@ -121,11 +117,7 @@ func (s *fs) Set(data []byte) (ID, error) {
|
| 121 | 121 |
return "", fmt.Errorf("Invalid empty data")
|
| 122 | 122 |
} |
| 123 | 123 |
|
| 124 |
- dgst, err := digest.FromBytes(data) |
|
| 125 |
- if err != nil {
|
|
| 126 |
- return "", err |
|
| 127 |
- } |
|
| 128 |
- id := ID(dgst) |
|
| 124 |
+ id := ID(digest.FromBytes(data)) |
|
| 129 | 125 |
filePath := s.contentFile(id) |
| 130 | 126 |
tempFilePath := s.contentFile(id) + ".tmp" |
| 131 | 127 |
if err := ioutil.WriteFile(tempFilePath, data, 0600); err != nil {
|
| ... | ... |
@@ -67,10 +67,7 @@ func TestFSInvalidSet(t *testing.T) {
|
| 67 | 67 |
t.Fatal(err) |
| 68 | 68 |
} |
| 69 | 69 |
|
| 70 |
- id, err := digest.FromBytes([]byte("foobar"))
|
|
| 71 |
- if err != nil {
|
|
| 72 |
- t.Fatal(err) |
|
| 73 |
- } |
|
| 70 |
+ id := digest.FromBytes([]byte("foobar"))
|
|
| 74 | 71 |
err = os.Mkdir(filepath.Join(tmpdir, contentDirName, string(id.Algorithm()), id.Hex()), 0700) |
| 75 | 72 |
if err != nil {
|
| 76 | 73 |
t.Fatal(err) |
| ... | ... |
@@ -160,11 +157,7 @@ func testMetadataGetSet(t *testing.T, store StoreBackend) {
|
| 160 | 160 |
t.Fatal("Expected error for getting metadata for unknown key")
|
| 161 | 161 |
} |
| 162 | 162 |
|
| 163 |
- id3, err := digest.FromBytes([]byte("baz"))
|
|
| 164 |
- if err != nil {
|
|
| 165 |
- t.Fatal(err) |
|
| 166 |
- } |
|
| 167 |
- |
|
| 163 |
+ id3 := digest.FromBytes([]byte("baz"))
|
|
| 168 | 164 |
err = store.SetMetadata(ID(id3), "tkey", []byte("tval"))
|
| 169 | 165 |
if err == nil {
|
| 170 | 166 |
t.Fatal("Expected error for setting metadata for unknown ID.")
|
| ... | ... |
@@ -27,7 +27,7 @@ func (r *RootFS) BaseLayerID() string {
|
| 27 | 27 |
|
| 28 | 28 |
// ChainID returns the ChainID for the top layer in RootFS. |
| 29 | 29 |
func (r *RootFS) ChainID() layer.ChainID {
|
| 30 |
- baseDiffID, _ := digest.FromBytes([]byte(r.BaseLayerID())) // can never error |
|
| 30 |
+ baseDiffID := digest.FromBytes([]byte(r.BaseLayerID())) |
|
| 31 | 31 |
return layer.CreateChainID(append([]layer.DiffID{layer.DiffID(baseDiffID)}, r.DiffIDs...))
|
| 32 | 32 |
} |
| 33 | 33 |
|
| ... | ... |
@@ -63,7 +63,7 @@ func CreateID(v1Image image.V1Image, layerID layer.ChainID, parent digest.Digest |
| 63 | 63 |
} |
| 64 | 64 |
logrus.Debugf("CreateV1ID %s", configJSON)
|
| 65 | 65 |
|
| 66 |
- return digest.FromBytes(configJSON) |
|
| 66 |
+ return digest.FromBytes(configJSON), nil |
|
| 67 | 67 |
} |
| 68 | 68 |
|
| 69 | 69 |
// MakeConfigFromV1Config creates an image config from the legacy V1 config format. |
| ... | ... |
@@ -48,7 +48,7 @@ type DockerRegistrySuite struct {
|
| 48 | 48 |
|
| 49 | 49 |
func (s *DockerRegistrySuite) SetUpTest(c *check.C) {
|
| 50 | 50 |
testRequires(c, DaemonIsLinux) |
| 51 |
- s.reg = setupRegistry(c) |
|
| 51 |
+ s.reg = setupRegistry(c, false) |
|
| 52 | 52 |
s.d = NewDaemon(c) |
| 53 | 53 |
} |
| 54 | 54 |
|
| ... | ... |
@@ -63,6 +63,34 @@ func (s *DockerRegistrySuite) TearDownTest(c *check.C) {
|
| 63 | 63 |
} |
| 64 | 64 |
|
| 65 | 65 |
func init() {
|
| 66 |
+ check.Suite(&DockerSchema1RegistrySuite{
|
|
| 67 |
+ ds: &DockerSuite{},
|
|
| 68 |
+ }) |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+type DockerSchema1RegistrySuite struct {
|
|
| 72 |
+ ds *DockerSuite |
|
| 73 |
+ reg *testRegistryV2 |
|
| 74 |
+ d *Daemon |
|
| 75 |
+} |
|
| 76 |
+ |
|
| 77 |
+func (s *DockerSchema1RegistrySuite) SetUpTest(c *check.C) {
|
|
| 78 |
+ testRequires(c, DaemonIsLinux) |
|
| 79 |
+ s.reg = setupRegistry(c, true) |
|
| 80 |
+ s.d = NewDaemon(c) |
|
| 81 |
+} |
|
| 82 |
+ |
|
| 83 |
+func (s *DockerSchema1RegistrySuite) TearDownTest(c *check.C) {
|
|
| 84 |
+ if s.reg != nil {
|
|
| 85 |
+ s.reg.Close() |
|
| 86 |
+ } |
|
| 87 |
+ if s.ds != nil {
|
|
| 88 |
+ s.ds.TearDownTest(c) |
|
| 89 |
+ } |
|
| 90 |
+ s.d.Stop() |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+func init() {
|
|
| 66 | 94 |
check.Suite(&DockerDaemonSuite{
|
| 67 | 95 |
ds: &DockerSuite{},
|
| 68 | 96 |
}) |
| ... | ... |
@@ -97,7 +125,7 @@ type DockerTrustSuite struct {
|
| 97 | 97 |
} |
| 98 | 98 |
|
| 99 | 99 |
func (s *DockerTrustSuite) SetUpTest(c *check.C) {
|
| 100 |
- s.reg = setupRegistry(c) |
|
| 100 |
+ s.reg = setupRegistry(c, false) |
|
| 101 | 101 |
s.not = setupNotary(c) |
| 102 | 102 |
} |
| 103 | 103 |
|
| ... | ... |
@@ -10,6 +10,7 @@ import ( |
| 10 | 10 |
|
| 11 | 11 |
"github.com/docker/distribution/digest" |
| 12 | 12 |
"github.com/docker/distribution/manifest/schema1" |
| 13 |
+ "github.com/docker/distribution/manifest/schema2" |
|
| 13 | 14 |
"github.com/docker/docker/pkg/integration/checker" |
| 14 | 15 |
"github.com/docker/docker/pkg/stringutils" |
| 15 | 16 |
"github.com/docker/engine-api/types" |
| ... | ... |
@@ -56,7 +57,7 @@ func setupImageWithTag(c *check.C, tag string) (digest.Digest, error) {
|
| 56 | 56 |
return digest.Digest(pushDigest), nil |
| 57 | 57 |
} |
| 58 | 58 |
|
| 59 |
-func (s *DockerRegistrySuite) TestPullByTagDisplaysDigest(c *check.C) {
|
|
| 59 |
+func testPullByTagDisplaysDigest(c *check.C) {
|
|
| 60 | 60 |
testRequires(c, DaemonIsLinux) |
| 61 | 61 |
pushDigest, err := setupImage(c) |
| 62 | 62 |
c.Assert(err, checker.IsNil, check.Commentf("error setting up image"))
|
| ... | ... |
@@ -73,7 +74,15 @@ func (s *DockerRegistrySuite) TestPullByTagDisplaysDigest(c *check.C) {
|
| 73 | 73 |
c.Assert(pushDigest.String(), checker.Equals, pullDigest) |
| 74 | 74 |
} |
| 75 | 75 |
|
| 76 |
-func (s *DockerRegistrySuite) TestPullByDigest(c *check.C) {
|
|
| 76 |
+func (s *DockerRegistrySuite) TestPullByTagDisplaysDigest(c *check.C) {
|
|
| 77 |
+ testPullByTagDisplaysDigest(c) |
|
| 78 |
+} |
|
| 79 |
+ |
|
| 80 |
+func (s *DockerSchema1RegistrySuite) TestPullByTagDisplaysDigest(c *check.C) {
|
|
| 81 |
+ testPullByTagDisplaysDigest(c) |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+func testPullByDigest(c *check.C) {
|
|
| 77 | 85 |
testRequires(c, DaemonIsLinux) |
| 78 | 86 |
pushDigest, err := setupImage(c) |
| 79 | 87 |
c.Assert(err, checker.IsNil, check.Commentf("error setting up image"))
|
| ... | ... |
@@ -91,7 +100,15 @@ func (s *DockerRegistrySuite) TestPullByDigest(c *check.C) {
|
| 91 | 91 |
c.Assert(pushDigest.String(), checker.Equals, pullDigest) |
| 92 | 92 |
} |
| 93 | 93 |
|
| 94 |
-func (s *DockerRegistrySuite) TestPullByDigestNoFallback(c *check.C) {
|
|
| 94 |
+func (s *DockerRegistrySuite) TestPullByDigest(c *check.C) {
|
|
| 95 |
+ testPullByDigest(c) |
|
| 96 |
+} |
|
| 97 |
+ |
|
| 98 |
+func (s *DockerSchema1RegistrySuite) TestPullByDigest(c *check.C) {
|
|
| 99 |
+ testPullByDigest(c) |
|
| 100 |
+} |
|
| 101 |
+ |
|
| 102 |
+func testPullByDigestNoFallback(c *check.C) {
|
|
| 95 | 103 |
testRequires(c, DaemonIsLinux) |
| 96 | 104 |
// pull from the registry using the <name>@<digest> reference |
| 97 | 105 |
imageReference := fmt.Sprintf("%s@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", repoName)
|
| ... | ... |
@@ -100,6 +117,14 @@ func (s *DockerRegistrySuite) TestPullByDigestNoFallback(c *check.C) {
|
| 100 | 100 |
c.Assert(out, checker.Contains, "manifest unknown", check.Commentf("expected non-zero exit status and correct error message when pulling non-existing image"))
|
| 101 | 101 |
} |
| 102 | 102 |
|
| 103 |
+func (s *DockerRegistrySuite) TestPullByDigestNoFallback(c *check.C) {
|
|
| 104 |
+ testPullByDigestNoFallback(c) |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+func (s *DockerSchema1RegistrySuite) TestPullByDigestNoFallback(c *check.C) {
|
|
| 108 |
+ testPullByDigestNoFallback(c) |
|
| 109 |
+} |
|
| 110 |
+ |
|
| 103 | 111 |
func (s *DockerRegistrySuite) TestCreateByDigest(c *check.C) {
|
| 104 | 112 |
pushDigest, err := setupImage(c) |
| 105 | 113 |
c.Assert(err, checker.IsNil, check.Commentf("error setting up image"))
|
| ... | ... |
@@ -372,6 +397,7 @@ func (s *DockerRegistrySuite) TestDeleteImageByIDOnlyPulledByDigest(c *check.C) |
| 372 | 372 |
|
| 373 | 373 |
// TestPullFailsWithAlteredManifest tests that a `docker pull` fails when |
| 374 | 374 |
// we have modified a manifest blob and its digest cannot be verified. |
| 375 |
+// This is the schema2 version of the test. |
|
| 375 | 376 |
func (s *DockerRegistrySuite) TestPullFailsWithAlteredManifest(c *check.C) {
|
| 376 | 377 |
testRequires(c, DaemonIsLinux) |
| 377 | 378 |
manifestDigest, err := setupImage(c) |
| ... | ... |
@@ -380,6 +406,46 @@ func (s *DockerRegistrySuite) TestPullFailsWithAlteredManifest(c *check.C) {
|
| 380 | 380 |
// Load the target manifest blob. |
| 381 | 381 |
manifestBlob := s.reg.readBlobContents(c, manifestDigest) |
| 382 | 382 |
|
| 383 |
+ var imgManifest schema2.Manifest |
|
| 384 |
+ err = json.Unmarshal(manifestBlob, &imgManifest) |
|
| 385 |
+ c.Assert(err, checker.IsNil, check.Commentf("unable to decode image manifest from blob"))
|
|
| 386 |
+ |
|
| 387 |
+ // Change a layer in the manifest. |
|
| 388 |
+ imgManifest.Layers[0].Digest = digest.Digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
|
| 389 |
+ |
|
| 390 |
+ // Move the existing data file aside, so that we can replace it with a |
|
| 391 |
+ // malicious blob of data. NOTE: we defer the returned undo func. |
|
| 392 |
+ undo := s.reg.tempMoveBlobData(c, manifestDigest) |
|
| 393 |
+ defer undo() |
|
| 394 |
+ |
|
| 395 |
+ alteredManifestBlob, err := json.MarshalIndent(imgManifest, "", " ") |
|
| 396 |
+ c.Assert(err, checker.IsNil, check.Commentf("unable to encode altered image manifest to JSON"))
|
|
| 397 |
+ |
|
| 398 |
+ s.reg.writeBlobContents(c, manifestDigest, alteredManifestBlob) |
|
| 399 |
+ |
|
| 400 |
+ // Now try pulling that image by digest. We should get an error about |
|
| 401 |
+ // digest verification for the manifest digest. |
|
| 402 |
+ |
|
| 403 |
+ // Pull from the registry using the <name>@<digest> reference. |
|
| 404 |
+ imageReference := fmt.Sprintf("%s@%s", repoName, manifestDigest)
|
|
| 405 |
+ out, exitStatus, _ := dockerCmdWithError("pull", imageReference)
|
|
| 406 |
+ c.Assert(exitStatus, checker.Not(check.Equals), 0) |
|
| 407 |
+ |
|
| 408 |
+ expectedErrorMsg := fmt.Sprintf("manifest verification failed for digest %s", manifestDigest)
|
|
| 409 |
+ c.Assert(out, checker.Contains, expectedErrorMsg) |
|
| 410 |
+} |
|
| 411 |
+ |
|
| 412 |
+// TestPullFailsWithAlteredManifest tests that a `docker pull` fails when |
|
| 413 |
+// we have modified a manifest blob and its digest cannot be verified. |
|
| 414 |
+// This is the schema1 version of the test. |
|
| 415 |
+func (s *DockerSchema1RegistrySuite) TestPullFailsWithAlteredManifest(c *check.C) {
|
|
| 416 |
+ testRequires(c, DaemonIsLinux) |
|
| 417 |
+ manifestDigest, err := setupImage(c) |
|
| 418 |
+ c.Assert(err, checker.IsNil, check.Commentf("error setting up image"))
|
|
| 419 |
+ |
|
| 420 |
+ // Load the target manifest blob. |
|
| 421 |
+ manifestBlob := s.reg.readBlobContents(c, manifestDigest) |
|
| 422 |
+ |
|
| 383 | 423 |
var imgManifest schema1.Manifest |
| 384 | 424 |
err = json.Unmarshal(manifestBlob, &imgManifest) |
| 385 | 425 |
c.Assert(err, checker.IsNil, check.Commentf("unable to decode image manifest from blob"))
|
| ... | ... |
@@ -413,6 +479,7 @@ func (s *DockerRegistrySuite) TestPullFailsWithAlteredManifest(c *check.C) {
|
| 413 | 413 |
|
| 414 | 414 |
// TestPullFailsWithAlteredLayer tests that a `docker pull` fails when |
| 415 | 415 |
// we have modified a layer blob and its digest cannot be verified. |
| 416 |
+// This is the schema2 version of the test. |
|
| 416 | 417 |
func (s *DockerRegistrySuite) TestPullFailsWithAlteredLayer(c *check.C) {
|
| 417 | 418 |
testRequires(c, DaemonIsLinux) |
| 418 | 419 |
manifestDigest, err := setupImage(c) |
| ... | ... |
@@ -421,6 +488,49 @@ func (s *DockerRegistrySuite) TestPullFailsWithAlteredLayer(c *check.C) {
|
| 421 | 421 |
// Load the target manifest blob. |
| 422 | 422 |
manifestBlob := s.reg.readBlobContents(c, manifestDigest) |
| 423 | 423 |
|
| 424 |
+ var imgManifest schema2.Manifest |
|
| 425 |
+ err = json.Unmarshal(manifestBlob, &imgManifest) |
|
| 426 |
+ c.Assert(err, checker.IsNil) |
|
| 427 |
+ |
|
| 428 |
+ // Next, get the digest of one of the layers from the manifest. |
|
| 429 |
+ targetLayerDigest := imgManifest.Layers[0].Digest |
|
| 430 |
+ |
|
| 431 |
+ // Move the existing data file aside, so that we can replace it with a |
|
| 432 |
+ // malicious blob of data. NOTE: we defer the returned undo func. |
|
| 433 |
+ undo := s.reg.tempMoveBlobData(c, targetLayerDigest) |
|
| 434 |
+ defer undo() |
|
| 435 |
+ |
|
| 436 |
+ // Now make a fake data blob in this directory. |
|
| 437 |
+ s.reg.writeBlobContents(c, targetLayerDigest, []byte("This is not the data you are looking for."))
|
|
| 438 |
+ |
|
| 439 |
+ // Now try pulling that image by digest. We should get an error about |
|
| 440 |
+ // digest verification for the target layer digest. |
|
| 441 |
+ |
|
| 442 |
+ // Remove distribution cache to force a re-pull of the blobs |
|
| 443 |
+ if err := os.RemoveAll(filepath.Join(dockerBasePath, "image", s.d.storageDriver, "distribution")); err != nil {
|
|
| 444 |
+ c.Fatalf("error clearing distribution cache: %v", err)
|
|
| 445 |
+ } |
|
| 446 |
+ |
|
| 447 |
+ // Pull from the registry using the <name>@<digest> reference. |
|
| 448 |
+ imageReference := fmt.Sprintf("%s@%s", repoName, manifestDigest)
|
|
| 449 |
+ out, exitStatus, _ := dockerCmdWithError("pull", imageReference)
|
|
| 450 |
+ c.Assert(exitStatus, checker.Not(check.Equals), 0, check.Commentf("expected a zero exit status"))
|
|
| 451 |
+ |
|
| 452 |
+ expectedErrorMsg := fmt.Sprintf("filesystem layer verification failed for digest %s", targetLayerDigest)
|
|
| 453 |
+ c.Assert(out, checker.Contains, expectedErrorMsg, check.Commentf("expected error message in output: %s", out))
|
|
| 454 |
+} |
|
| 455 |
+ |
|
| 456 |
+// TestPullFailsWithAlteredLayer tests that a `docker pull` fails when |
|
| 457 |
+// we have modified a layer blob and its digest cannot be verified. |
|
| 458 |
+// This is the schema1 version of the test. |
|
| 459 |
+func (s *DockerSchema1RegistrySuite) TestPullFailsWithAlteredLayer(c *check.C) {
|
|
| 460 |
+ testRequires(c, DaemonIsLinux) |
|
| 461 |
+ manifestDigest, err := setupImage(c) |
|
| 462 |
+ c.Assert(err, checker.IsNil) |
|
| 463 |
+ |
|
| 464 |
+ // Load the target manifest blob. |
|
| 465 |
+ manifestBlob := s.reg.readBlobContents(c, manifestDigest) |
|
| 466 |
+ |
|
| 424 | 467 |
var imgManifest schema1.Manifest |
| 425 | 468 |
err = json.Unmarshal(manifestBlob, &imgManifest) |
| 426 | 469 |
c.Assert(err, checker.IsNil) |
| ... | ... |
@@ -1,19 +1,29 @@ |
| 1 | 1 |
package main |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "encoding/json" |
|
| 4 | 5 |
"fmt" |
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "os" |
|
| 5 | 8 |
"os/exec" |
| 9 |
+ "path/filepath" |
|
| 10 |
+ "runtime" |
|
| 6 | 11 |
"strings" |
| 7 | 12 |
|
| 13 |
+ "github.com/docker/distribution" |
|
| 14 |
+ "github.com/docker/distribution/digest" |
|
| 15 |
+ "github.com/docker/distribution/manifest" |
|
| 16 |
+ "github.com/docker/distribution/manifest/manifestlist" |
|
| 17 |
+ "github.com/docker/distribution/manifest/schema2" |
|
| 8 | 18 |
"github.com/docker/docker/pkg/integration/checker" |
| 9 | 19 |
"github.com/go-check/check" |
| 10 | 20 |
) |
| 11 | 21 |
|
| 12 |
-// TestPullImageWithAliases pulls a specific image tag and verifies that any aliases (i.e., other |
|
| 22 |
+// testPullImageWithAliases pulls a specific image tag and verifies that any aliases (i.e., other |
|
| 13 | 23 |
// tags for the same image) are not also pulled down. |
| 14 | 24 |
// |
| 15 | 25 |
// Ref: docker/docker#8141 |
| 16 |
-func (s *DockerRegistrySuite) TestPullImageWithAliases(c *check.C) {
|
|
| 26 |
+func testPullImageWithAliases(c *check.C) {
|
|
| 17 | 27 |
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
| 18 | 28 |
|
| 19 | 29 |
repos := []string{}
|
| ... | ... |
@@ -40,8 +50,16 @@ func (s *DockerRegistrySuite) TestPullImageWithAliases(c *check.C) {
|
| 40 | 40 |
} |
| 41 | 41 |
} |
| 42 | 42 |
|
| 43 |
-// TestConcurrentPullWholeRepo pulls the same repo concurrently. |
|
| 44 |
-func (s *DockerRegistrySuite) TestConcurrentPullWholeRepo(c *check.C) {
|
|
| 43 |
+func (s *DockerRegistrySuite) TestPullImageWithAliases(c *check.C) {
|
|
| 44 |
+ testPullImageWithAliases(c) |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+func (s *DockerSchema1RegistrySuite) TestPullImageWithAliases(c *check.C) {
|
|
| 48 |
+ testPullImageWithAliases(c) |
|
| 49 |
+} |
|
| 50 |
+ |
|
| 51 |
+// testConcurrentPullWholeRepo pulls the same repo concurrently. |
|
| 52 |
+func testConcurrentPullWholeRepo(c *check.C) {
|
|
| 45 | 53 |
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
| 46 | 54 |
|
| 47 | 55 |
repos := []string{}
|
| ... | ... |
@@ -89,8 +107,16 @@ func (s *DockerRegistrySuite) TestConcurrentPullWholeRepo(c *check.C) {
|
| 89 | 89 |
} |
| 90 | 90 |
} |
| 91 | 91 |
|
| 92 |
-// TestConcurrentFailingPull tries a concurrent pull that doesn't succeed. |
|
| 93 |
-func (s *DockerRegistrySuite) TestConcurrentFailingPull(c *check.C) {
|
|
| 92 |
+func (s *DockerRegistrySuite) testConcurrentPullWholeRepo(c *check.C) {
|
|
| 93 |
+ testConcurrentPullWholeRepo(c) |
|
| 94 |
+} |
|
| 95 |
+ |
|
| 96 |
+func (s *DockerSchema1RegistrySuite) testConcurrentPullWholeRepo(c *check.C) {
|
|
| 97 |
+ testConcurrentPullWholeRepo(c) |
|
| 98 |
+} |
|
| 99 |
+ |
|
| 100 |
+// testConcurrentFailingPull tries a concurrent pull that doesn't succeed. |
|
| 101 |
+func testConcurrentFailingPull(c *check.C) {
|
|
| 94 | 102 |
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
| 95 | 103 |
|
| 96 | 104 |
// Run multiple pulls concurrently |
| ... | ... |
@@ -112,9 +138,17 @@ func (s *DockerRegistrySuite) TestConcurrentFailingPull(c *check.C) {
|
| 112 | 112 |
} |
| 113 | 113 |
} |
| 114 | 114 |
|
| 115 |
-// TestConcurrentPullMultipleTags pulls multiple tags from the same repo |
|
| 115 |
+func (s *DockerRegistrySuite) testConcurrentFailingPull(c *check.C) {
|
|
| 116 |
+ testConcurrentFailingPull(c) |
|
| 117 |
+} |
|
| 118 |
+ |
|
| 119 |
+func (s *DockerSchema1RegistrySuite) testConcurrentFailingPull(c *check.C) {
|
|
| 120 |
+ testConcurrentFailingPull(c) |
|
| 121 |
+} |
|
| 122 |
+ |
|
| 123 |
+// testConcurrentPullMultipleTags pulls multiple tags from the same repo |
|
| 116 | 124 |
// concurrently. |
| 117 |
-func (s *DockerRegistrySuite) TestConcurrentPullMultipleTags(c *check.C) {
|
|
| 125 |
+func testConcurrentPullMultipleTags(c *check.C) {
|
|
| 118 | 126 |
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
| 119 | 127 |
|
| 120 | 128 |
repos := []string{}
|
| ... | ... |
@@ -161,9 +195,17 @@ func (s *DockerRegistrySuite) TestConcurrentPullMultipleTags(c *check.C) {
|
| 161 | 161 |
} |
| 162 | 162 |
} |
| 163 | 163 |
|
| 164 |
-// TestPullIDStability verifies that pushing an image and pulling it back |
|
| 164 |
+func (s *DockerRegistrySuite) TestConcurrentPullMultipleTags(c *check.C) {
|
|
| 165 |
+ testConcurrentPullMultipleTags(c) |
|
| 166 |
+} |
|
| 167 |
+ |
|
| 168 |
+func (s *DockerSchema1RegistrySuite) TestConcurrentPullMultipleTags(c *check.C) {
|
|
| 169 |
+ testConcurrentPullMultipleTags(c) |
|
| 170 |
+} |
|
| 171 |
+ |
|
| 172 |
+// testPullIDStability verifies that pushing an image and pulling it back |
|
| 165 | 173 |
// preserves the image ID. |
| 166 |
-func (s *DockerRegistrySuite) TestPullIDStability(c *check.C) {
|
|
| 174 |
+func testPullIDStability(c *check.C) {
|
|
| 167 | 175 |
derivedImage := privateRegistryURL + "/dockercli/id-stability" |
| 168 | 176 |
baseImage := "busybox" |
| 169 | 177 |
|
| ... | ... |
@@ -229,6 +271,14 @@ func (s *DockerRegistrySuite) TestPullIDStability(c *check.C) {
|
| 229 | 229 |
} |
| 230 | 230 |
} |
| 231 | 231 |
|
| 232 |
+func (s *DockerRegistrySuite) TestPullIDStability(c *check.C) {
|
|
| 233 |
+ testPullIDStability(c) |
|
| 234 |
+} |
|
| 235 |
+ |
|
| 236 |
+func (s *DockerSchema1RegistrySuite) TestPullIDStability(c *check.C) {
|
|
| 237 |
+ testPullIDStability(c) |
|
| 238 |
+} |
|
| 239 |
+ |
|
| 232 | 240 |
// TestPullFallbackOn404 tries to pull a nonexistent manifest and confirms that |
| 233 | 241 |
// the pull falls back to the v1 protocol. |
| 234 | 242 |
// |
| ... | ... |
@@ -240,3 +290,85 @@ func (s *DockerRegistrySuite) TestPullFallbackOn404(c *check.C) {
|
| 240 | 240 |
|
| 241 | 241 |
c.Assert(out, checker.Contains, "v1 ping attempt") |
| 242 | 242 |
} |
| 243 |
+ |
|
| 244 |
+func (s *DockerRegistrySuite) TestPullManifestList(c *check.C) {
|
|
| 245 |
+ pushDigest, err := setupImage(c) |
|
| 246 |
+ c.Assert(err, checker.IsNil, check.Commentf("error setting up image"))
|
|
| 247 |
+ |
|
| 248 |
+ // Inject a manifest list into the registry |
|
| 249 |
+ manifestList := &manifestlist.ManifestList{
|
|
| 250 |
+ Versioned: manifest.Versioned{
|
|
| 251 |
+ SchemaVersion: 2, |
|
| 252 |
+ MediaType: manifestlist.MediaTypeManifestList, |
|
| 253 |
+ }, |
|
| 254 |
+ Manifests: []manifestlist.ManifestDescriptor{
|
|
| 255 |
+ {
|
|
| 256 |
+ Descriptor: distribution.Descriptor{
|
|
| 257 |
+ Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", |
|
| 258 |
+ Size: 3253, |
|
| 259 |
+ MediaType: schema2.MediaTypeManifest, |
|
| 260 |
+ }, |
|
| 261 |
+ Platform: manifestlist.PlatformSpec{
|
|
| 262 |
+ Architecture: "bogus_arch", |
|
| 263 |
+ OS: "bogus_os", |
|
| 264 |
+ }, |
|
| 265 |
+ }, |
|
| 266 |
+ {
|
|
| 267 |
+ Descriptor: distribution.Descriptor{
|
|
| 268 |
+ Digest: pushDigest, |
|
| 269 |
+ Size: 3253, |
|
| 270 |
+ MediaType: schema2.MediaTypeManifest, |
|
| 271 |
+ }, |
|
| 272 |
+ Platform: manifestlist.PlatformSpec{
|
|
| 273 |
+ Architecture: runtime.GOARCH, |
|
| 274 |
+ OS: runtime.GOOS, |
|
| 275 |
+ }, |
|
| 276 |
+ }, |
|
| 277 |
+ }, |
|
| 278 |
+ } |
|
| 279 |
+ |
|
| 280 |
+ manifestListJSON, err := json.MarshalIndent(manifestList, "", " ") |
|
| 281 |
+ c.Assert(err, checker.IsNil, check.Commentf("error marshalling manifest list"))
|
|
| 282 |
+ |
|
| 283 |
+ manifestListDigest := digest.FromBytes(manifestListJSON) |
|
| 284 |
+ hexDigest := manifestListDigest.Hex() |
|
| 285 |
+ |
|
| 286 |
+ registryV2Path := filepath.Join(s.reg.dir, "docker", "registry", "v2") |
|
| 287 |
+ |
|
| 288 |
+ // Write manifest list to blob store |
|
| 289 |
+ blobDir := filepath.Join(registryV2Path, "blobs", "sha256", hexDigest[:2], hexDigest) |
|
| 290 |
+ err = os.MkdirAll(blobDir, 0755) |
|
| 291 |
+ c.Assert(err, checker.IsNil, check.Commentf("error creating blob dir"))
|
|
| 292 |
+ blobPath := filepath.Join(blobDir, "data") |
|
| 293 |
+ err = ioutil.WriteFile(blobPath, []byte(manifestListJSON), 0644) |
|
| 294 |
+ c.Assert(err, checker.IsNil, check.Commentf("error writing manifest list"))
|
|
| 295 |
+ |
|
| 296 |
+ // Add to revision store |
|
| 297 |
+ revisionDir := filepath.Join(registryV2Path, "repositories", remoteRepoName, "_manifests", "revisions", "sha256", hexDigest) |
|
| 298 |
+ err = os.Mkdir(revisionDir, 0755) |
|
| 299 |
+ c.Assert(err, checker.IsNil, check.Commentf("error creating revision dir"))
|
|
| 300 |
+ revisionPath := filepath.Join(revisionDir, "link") |
|
| 301 |
+ err = ioutil.WriteFile(revisionPath, []byte(manifestListDigest.String()), 0644) |
|
| 302 |
+ c.Assert(err, checker.IsNil, check.Commentf("error writing revision link"))
|
|
| 303 |
+ |
|
| 304 |
+ // Update tag |
|
| 305 |
+ tagPath := filepath.Join(registryV2Path, "repositories", remoteRepoName, "_manifests", "tags", "latest", "current", "link") |
|
| 306 |
+ err = ioutil.WriteFile(tagPath, []byte(manifestListDigest.String()), 0644) |
|
| 307 |
+ c.Assert(err, checker.IsNil, check.Commentf("error writing tag link"))
|
|
| 308 |
+ |
|
| 309 |
+ // Verify that the image can be pulled through the manifest list. |
|
| 310 |
+ out, _ := dockerCmd(c, "pull", repoName) |
|
| 311 |
+ |
|
| 312 |
+ // The pull output includes "Digest: <digest>", so find that |
|
| 313 |
+ matches := digestRegex.FindStringSubmatch(out) |
|
| 314 |
+ c.Assert(matches, checker.HasLen, 2, check.Commentf("unable to parse digest from pull output: %s", out))
|
|
| 315 |
+ pullDigest := matches[1] |
|
| 316 |
+ |
|
| 317 |
+ // Make sure the pushed and pull digests match |
|
| 318 |
+ c.Assert(manifestListDigest.String(), checker.Equals, pullDigest) |
|
| 319 |
+ |
|
| 320 |
+ // Was the image actually created? |
|
| 321 |
+ dockerCmd(c, "inspect", repoName) |
|
| 322 |
+ |
|
| 323 |
+ dockerCmd(c, "rmi", repoName) |
|
| 324 |
+} |
| ... | ... |
@@ -16,7 +16,7 @@ import ( |
| 16 | 16 |
) |
| 17 | 17 |
|
| 18 | 18 |
// Pushing an image to a private registry. |
| 19 |
-func (s *DockerRegistrySuite) TestPushBusyboxImage(c *check.C) {
|
|
| 19 |
+func testPushBusyboxImage(c *check.C) {
|
|
| 20 | 20 |
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
| 21 | 21 |
// tag the image to upload it to the private registry |
| 22 | 22 |
dockerCmd(c, "tag", "busybox", repoName) |
| ... | ... |
@@ -24,13 +24,21 @@ func (s *DockerRegistrySuite) TestPushBusyboxImage(c *check.C) {
|
| 24 | 24 |
dockerCmd(c, "push", repoName) |
| 25 | 25 |
} |
| 26 | 26 |
|
| 27 |
+func (s *DockerRegistrySuite) TestPushBusyboxImage(c *check.C) {
|
|
| 28 |
+ testPushBusyboxImage(c) |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+func (s *DockerSchema1RegistrySuite) TestPushBusyboxImage(c *check.C) {
|
|
| 32 |
+ testPushBusyboxImage(c) |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 27 | 35 |
// pushing an image without a prefix should throw an error |
| 28 | 36 |
func (s *DockerSuite) TestPushUnprefixedRepo(c *check.C) {
|
| 29 | 37 |
out, _, err := dockerCmdWithError("push", "busybox")
|
| 30 | 38 |
c.Assert(err, check.NotNil, check.Commentf("pushing an unprefixed repo didn't result in a non-zero exit status: %s", out))
|
| 31 | 39 |
} |
| 32 | 40 |
|
| 33 |
-func (s *DockerRegistrySuite) TestPushUntagged(c *check.C) {
|
|
| 41 |
+func testPushUntagged(c *check.C) {
|
|
| 34 | 42 |
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
| 35 | 43 |
expected := "Repository does not exist" |
| 36 | 44 |
|
| ... | ... |
@@ -39,7 +47,15 @@ func (s *DockerRegistrySuite) TestPushUntagged(c *check.C) {
|
| 39 | 39 |
c.Assert(out, checker.Contains, expected, check.Commentf("pushing the image failed"))
|
| 40 | 40 |
} |
| 41 | 41 |
|
| 42 |
-func (s *DockerRegistrySuite) TestPushBadTag(c *check.C) {
|
|
| 42 |
+func (s *DockerRegistrySuite) TestPushUntagged(c *check.C) {
|
|
| 43 |
+ testPushUntagged(c) |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+func (s *DockerSchema1RegistrySuite) TestPushUntagged(c *check.C) {
|
|
| 47 |
+ testPushUntagged(c) |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+func testPushBadTag(c *check.C) {
|
|
| 43 | 51 |
repoName := fmt.Sprintf("%v/dockercli/busybox:latest", privateRegistryURL)
|
| 44 | 52 |
expected := "does not exist" |
| 45 | 53 |
|
| ... | ... |
@@ -48,7 +64,15 @@ func (s *DockerRegistrySuite) TestPushBadTag(c *check.C) {
|
| 48 | 48 |
c.Assert(out, checker.Contains, expected, check.Commentf("pushing the image failed"))
|
| 49 | 49 |
} |
| 50 | 50 |
|
| 51 |
-func (s *DockerRegistrySuite) TestPushMultipleTags(c *check.C) {
|
|
| 51 |
+func (s *DockerRegistrySuite) TestPushBadTag(c *check.C) {
|
|
| 52 |
+ testPushBadTag(c) |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+func (s *DockerSchema1RegistrySuite) TestPushBadTag(c *check.C) {
|
|
| 56 |
+ testPushBadTag(c) |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+func testPushMultipleTags(c *check.C) {
|
|
| 52 | 60 |
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
| 53 | 61 |
repoTag1 := fmt.Sprintf("%v/dockercli/busybox:t1", privateRegistryURL)
|
| 54 | 62 |
repoTag2 := fmt.Sprintf("%v/dockercli/busybox:t2", privateRegistryURL)
|
| ... | ... |
@@ -85,7 +109,15 @@ func (s *DockerRegistrySuite) TestPushMultipleTags(c *check.C) {
|
| 85 | 85 |
} |
| 86 | 86 |
} |
| 87 | 87 |
|
| 88 |
-func (s *DockerRegistrySuite) TestPushEmptyLayer(c *check.C) {
|
|
| 88 |
+func (s *DockerRegistrySuite) TestPushMultipleTags(c *check.C) {
|
|
| 89 |
+ testPushMultipleTags(c) |
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+func (s *DockerSchema1RegistrySuite) TestPushMultipleTags(c *check.C) {
|
|
| 93 |
+ testPushMultipleTags(c) |
|
| 94 |
+} |
|
| 95 |
+ |
|
| 96 |
+func testPushEmptyLayer(c *check.C) {
|
|
| 89 | 97 |
repoName := fmt.Sprintf("%v/dockercli/emptylayer", privateRegistryURL)
|
| 90 | 98 |
emptyTarball, err := ioutil.TempFile("", "empty_tarball")
|
| 91 | 99 |
c.Assert(err, check.IsNil, check.Commentf("Unable to create test file"))
|
| ... | ... |
@@ -107,6 +139,14 @@ func (s *DockerRegistrySuite) TestPushEmptyLayer(c *check.C) {
|
| 107 | 107 |
c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out))
|
| 108 | 108 |
} |
| 109 | 109 |
|
| 110 |
+func (s *DockerRegistrySuite) TestPushEmptyLayer(c *check.C) {
|
|
| 111 |
+ testPushEmptyLayer(c) |
|
| 112 |
+} |
|
| 113 |
+ |
|
| 114 |
+func (s *DockerSchema1RegistrySuite) TestPushEmptyLayer(c *check.C) {
|
|
| 115 |
+ testPushEmptyLayer(c) |
|
| 116 |
+} |
|
| 117 |
+ |
|
| 110 | 118 |
func (s *DockerTrustSuite) TestTrustedPush(c *check.C) {
|
| 111 | 119 |
repoName := fmt.Sprintf("%v/dockercli/trusted:latest", privateRegistryURL)
|
| 112 | 120 |
// tag the image and upload it to the private registry |
| ... | ... |
@@ -1554,9 +1554,9 @@ func daemonTime(c *check.C) time.Time {
|
| 1554 | 1554 |
return dt |
| 1555 | 1555 |
} |
| 1556 | 1556 |
|
| 1557 |
-func setupRegistry(c *check.C) *testRegistryV2 {
|
|
| 1557 |
+func setupRegistry(c *check.C, schema1 bool) *testRegistryV2 {
|
|
| 1558 | 1558 |
testRequires(c, RegistryHosting) |
| 1559 |
- reg, err := newTestRegistryV2(c) |
|
| 1559 |
+ reg, err := newTestRegistryV2(c, schema1) |
|
| 1560 | 1560 |
c.Assert(err, check.IsNil) |
| 1561 | 1561 |
|
| 1562 | 1562 |
// Wait for registry to be ready to serve requests. |
| ... | ... |
@@ -12,14 +12,17 @@ import ( |
| 12 | 12 |
"github.com/go-check/check" |
| 13 | 13 |
) |
| 14 | 14 |
|
| 15 |
-const v2binary = "registry-v2" |
|
| 15 |
+const ( |
|
| 16 |
+ v2binary = "registry-v2" |
|
| 17 |
+ v2binarySchema1 = "registry-v2-schema1" |
|
| 18 |
+) |
|
| 16 | 19 |
|
| 17 | 20 |
type testRegistryV2 struct {
|
| 18 | 21 |
cmd *exec.Cmd |
| 19 | 22 |
dir string |
| 20 | 23 |
} |
| 21 | 24 |
|
| 22 |
-func newTestRegistryV2(c *check.C) (*testRegistryV2, error) {
|
|
| 25 |
+func newTestRegistryV2(c *check.C, schema1 bool) (*testRegistryV2, error) {
|
|
| 23 | 26 |
template := `version: 0.1 |
| 24 | 27 |
loglevel: debug |
| 25 | 28 |
storage: |
| ... | ... |
@@ -41,7 +44,11 @@ http: |
| 41 | 41 |
return nil, err |
| 42 | 42 |
} |
| 43 | 43 |
|
| 44 |
- cmd := exec.Command(v2binary, confPath) |
|
| 44 |
+ binary := v2binary |
|
| 45 |
+ if schema1 {
|
|
| 46 |
+ binary = v2binarySchema1 |
|
| 47 |
+ } |
|
| 48 |
+ cmd := exec.Command(binary, confPath) |
|
| 45 | 49 |
if err := cmd.Start(); err != nil {
|
| 46 | 50 |
os.RemoveAll(tmp) |
| 47 | 51 |
if os.IsNotExist(err) {
|
| ... | ... |
@@ -15,12 +15,8 @@ import ( |
| 15 | 15 |
|
| 16 | 16 |
func randomLayerID(seed int64) ChainID {
|
| 17 | 17 |
r := rand.New(rand.NewSource(seed)) |
| 18 |
- dgst, err := digest.FromBytes([]byte(fmt.Sprintf("%d", r.Int63())))
|
|
| 19 |
- if err != nil {
|
|
| 20 |
- panic(err) |
|
| 21 |
- } |
|
| 22 | 18 |
|
| 23 |
- return ChainID(dgst) |
|
| 19 |
+ return ChainID(digest.FromBytes([]byte(fmt.Sprintf("%d", r.Int63()))))
|
|
| 24 | 20 |
} |
| 25 | 21 |
|
| 26 | 22 |
func newFileMetadataStore(t *testing.T) (*fileMetadataStore, string, func()) {
|
| ... | ... |
@@ -233,12 +233,7 @@ func createChainIDFromParent(parent ChainID, dgsts ...DiffID) ChainID {
|
| 233 | 233 |
return createChainIDFromParent(ChainID(dgsts[0]), dgsts[1:]...) |
| 234 | 234 |
} |
| 235 | 235 |
// H = "H(n-1) SHA256(n)" |
| 236 |
- dgst, err := digest.FromBytes([]byte(string(parent) + " " + string(dgsts[0]))) |
|
| 237 |
- if err != nil {
|
|
| 238 |
- // Digest calculation is not expected to throw an error, |
|
| 239 |
- // any error at this point is a program error |
|
| 240 |
- panic(err) |
|
| 241 |
- } |
|
| 236 |
+ dgst := digest.FromBytes([]byte(string(parent) + " " + string(dgsts[0]))) |
|
| 242 | 237 |
return createChainIDFromParent(ChainID(dgst), dgsts[1:]...) |
| 243 | 238 |
} |
| 244 | 239 |
|
| ... | ... |
@@ -548,10 +548,7 @@ func TestTarStreamStability(t *testing.T) {
|
| 548 | 548 |
} |
| 549 | 549 |
|
| 550 | 550 |
func assertLayerDiff(t *testing.T, expected []byte, layer Layer) {
|
| 551 |
- expectedDigest, err := digest.FromBytes(expected) |
|
| 552 |
- if err != nil {
|
|
| 553 |
- t.Fatal(err) |
|
| 554 |
- } |
|
| 551 |
+ expectedDigest := digest.FromBytes(expected) |
|
| 555 | 552 |
|
| 556 | 553 |
if digest.Digest(layer.DiffID()) != expectedDigest {
|
| 557 | 554 |
t.Fatalf("Mismatched diff id for %s, got %s, expected %s", layer.ChainID(), layer.DiffID(), expected)
|
| ... | ... |
@@ -573,10 +570,7 @@ func assertLayerDiff(t *testing.T, expected []byte, layer Layer) {
|
| 573 | 573 |
t.Fatalf("Mismatched tar stream size for %s, got %d, expected %d", layer.ChainID(), len(actual), len(expected))
|
| 574 | 574 |
} |
| 575 | 575 |
|
| 576 |
- actualDigest, err := digest.FromBytes(actual) |
|
| 577 |
- if err != nil {
|
|
| 578 |
- t.Fatal(err) |
|
| 579 |
- } |
|
| 576 |
+ actualDigest := digest.FromBytes(actual) |
|
| 580 | 577 |
|
| 581 | 578 |
if actualDigest != expectedDigest {
|
| 582 | 579 |
logByteDiff(t, actual, expected) |
| ... | ... |
@@ -37,10 +37,7 @@ func GetLayerPath(s Store, layer ChainID) (string, error) {
|
| 37 | 37 |
|
| 38 | 38 |
func (ls *layerStore) RegisterDiffID(graphID string, size int64) (Layer, error) {
|
| 39 | 39 |
var err error // this is used for cleanup in existingLayer case |
| 40 |
- diffID, err := digest.FromBytes([]byte(graphID)) |
|
| 41 |
- if err != nil {
|
|
| 42 |
- return nil, err |
|
| 43 |
- } |
|
| 40 |
+ diffID := digest.FromBytes([]byte(graphID)) |
|
| 44 | 41 |
|
| 45 | 42 |
// Create new roLayer |
| 46 | 43 |
layer := &roLayer{
|
| ... | ... |
@@ -5,3 +5,10 @@ Brian Bland <brian.bland@docker.com> Brian Bland <r4nd0m1n4t0r@gmail.com> |
| 5 | 5 |
Josh Hawn <josh.hawn@docker.com> Josh Hawn <jlhawn@berkeley.edu> |
| 6 | 6 |
Richard Scothern <richard.scothern@docker.com> Richard <richard.scothern@gmail.com> |
| 7 | 7 |
Richard Scothern <richard.scothern@docker.com> Richard Scothern <richard.scothern@gmail.com> |
| 8 |
+Andrew Meredith <andymeredith@gmail.com> Andrew Meredith <kendru@users.noreply.github.com> |
|
| 9 |
+harche <p.harshal@gmail.com> harche <harche@users.noreply.github.com> |
|
| 10 |
+Jessie Frazelle <jessie@docker.com> <jfrazelle@users.noreply.github.com> |
|
| 11 |
+Sharif Nassar <sharif@mrwacky.com> Sharif Nassar <mrwacky42@users.noreply.github.com> |
|
| 12 |
+Sven Dowideit <SvenDowideit@home.org.au> Sven Dowideit <SvenDowideit@users.noreply.github.com> |
|
| 13 |
+Vincent Giersch <vincent.giersch@ovh.net> Vincent Giersch <vincent@giersch.fr> |
|
| 14 |
+davidli <wenquan.li@hp.com> davidli <wenquan.li@hpe.com> |
|
| 8 | 15 |
\ No newline at end of file |
| ... | ... |
@@ -5,13 +5,16 @@ Adrian Mouat <adrian.mouat@gmail.com> |
| 5 | 5 |
Ahmet Alp Balkan <ahmetalpbalkan@gmail.com> |
| 6 | 6 |
Alex Chan <alex.chan@metaswitch.com> |
| 7 | 7 |
Alex Elman <aelman@indeed.com> |
| 8 |
+amitshukla <ashukla73@hotmail.com> |
|
| 8 | 9 |
Amy Lindburg <amy.lindburg@docker.com> |
| 10 |
+Andrew Meredith <andymeredith@gmail.com> |
|
| 9 | 11 |
Andrey Kostov <kostov.andrey@gmail.com> |
| 10 | 12 |
Andy Goldstein <agoldste@redhat.com> |
| 11 | 13 |
Anton Tiurin <noxiouz@yandex.ru> |
| 12 | 14 |
Antonio Mercado <amercado@thinknode.com> |
| 13 | 15 |
Arnaud Porterie <arnaud.porterie@docker.com> |
| 14 | 16 |
Arthur Baars <arthur@semmle.com> |
| 17 |
+Avi Miller <avi.miller@oracle.com> |
|
| 15 | 18 |
Ayose Cazorla <ayosec@gmail.com> |
| 16 | 19 |
BadZen <dave.trombley@gmail.com> |
| 17 | 20 |
Ben Firshman <ben@firshman.co.uk> |
| ... | ... |
@@ -32,9 +35,10 @@ Derek McGowan <derek@mcgstyle.net> |
| 32 | 32 |
Diogo Mónica <diogo.monica@gmail.com> |
| 33 | 33 |
Donald Huang <don.hcd@gmail.com> |
| 34 | 34 |
Doug Davis <dug@us.ibm.com> |
| 35 |
+farmerworking <farmerworking@gmail.com> |
|
| 35 | 36 |
Florentin Raud <florentin.raud@gmail.com> |
| 36 | 37 |
Frederick F. Kautz IV <fkautz@alumni.cmu.edu> |
| 37 |
-harche <harche@users.noreply.github.com> |
|
| 38 |
+harche <p.harshal@gmail.com> |
|
| 38 | 39 |
Henri Gomez <henri.gomez@gmail.com> |
| 39 | 40 |
Hu Keping <hukeping@huawei.com> |
| 40 | 41 |
Hua Wang <wanghua.humble@gmail.com> |
| ... | ... |
@@ -42,9 +46,10 @@ Ian Babrou <ibobrik@gmail.com> |
| 42 | 42 |
Jack Griffin <jackpg14@gmail.com> |
| 43 | 43 |
Jason Freidman <jason.freidman@gmail.com> |
| 44 | 44 |
Jeff Nickoloff <jeff@allingeek.com> |
| 45 |
-Jessie Frazelle <jfrazelle@users.noreply.github.com> |
|
| 45 |
+Jessie Frazelle <jessie@docker.com> |
|
| 46 | 46 |
Jianqing Wang <tsing@jianqing.org> |
| 47 | 47 |
Jon Poler <jonathan.poler@apcera.com> |
| 48 |
+Jonathan Boulle <jonathanboulle@gmail.com> |
|
| 48 | 49 |
Jordan Liggitt <jliggitt@redhat.com> |
| 49 | 50 |
Josh Hawn <josh.hawn@docker.com> |
| 50 | 51 |
Julien Fernandez <julien.fernandez@gmail.com> |
| ... | ... |
@@ -59,6 +64,7 @@ Matt Moore <mattmoor@google.com> |
| 59 | 59 |
Matt Robenolt <matt@ydekproductions.com> |
| 60 | 60 |
Michael Prokop <mika@grml.org> |
| 61 | 61 |
Miquel Sabaté <msabate@suse.com> |
| 62 |
+Morgan Bauer <mbauer@us.ibm.com> |
|
| 62 | 63 |
moxiegirl <mary@docker.com> |
| 63 | 64 |
Nathan Sullivan <nathan@nightsys.net> |
| 64 | 65 |
nevermosby <robolwq@qq.com> |
| ... | ... |
@@ -70,8 +76,8 @@ Olivier Jacques <olivier.jacques@hp.com> |
| 70 | 70 |
Patrick Devine <patrick.devine@docker.com> |
| 71 | 71 |
Philip Misiowiec <philip@atlashealth.com> |
| 72 | 72 |
Richard Scothern <richard.scothern@docker.com> |
| 73 |
+Rusty Conover <rusty@luckydinosaur.com> |
|
| 73 | 74 |
Sebastiaan van Stijn <github@gone.nl> |
| 74 |
-Sharif Nassar <mrwacky42@users.noreply.github.com> |
|
| 75 | 75 |
Sharif Nassar <sharif@mrwacky.com> |
| 76 | 76 |
Shawn Falkner-Horine <dreadpirateshawn@gmail.com> |
| 77 | 77 |
Shreyas Karnik <karnik.shreyas@gmail.com> |
| ... | ... |
@@ -81,15 +87,16 @@ Stephen J Day <stephen.day@docker.com> |
| 81 | 81 |
Sungho Moon <sungho.moon@navercorp.com> |
| 82 | 82 |
Sven Dowideit <SvenDowideit@home.org.au> |
| 83 | 83 |
Sylvain Baubeau <sbaubeau@redhat.com> |
| 84 |
+Ted Reed <ted.reed@gmail.com> |
|
| 84 | 85 |
tgic <farmer1992@gmail.com> |
| 85 | 86 |
Thomas Sjögren <konstruktoid@users.noreply.github.com> |
| 86 | 87 |
Tianon Gravi <admwiggin@gmail.com> |
| 87 | 88 |
Tibor Vass <teabee89@gmail.com> |
| 89 |
+Tonis Tiigi <tonistiigi@gmail.com> |
|
| 88 | 90 |
Troels Thomsen <troels@thomsen.io> |
| 89 | 91 |
Vincent Batts <vbatts@redhat.com> |
| 90 | 92 |
Vincent Demeester <vincent@sbr.pm> |
| 91 | 93 |
Vincent Giersch <vincent.giersch@ovh.net> |
| 92 |
-Vincent Giersch <vincent@giersch.fr> |
|
| 93 | 94 |
W. Trevor King <wking@tremily.us> |
| 94 | 95 |
xg.song <xg.song@venusource.com> |
| 95 | 96 |
xiekeyang <xiekeyang@huawei.com> |
| ... | ... |
@@ -1,8 +1,58 @@ |
| 1 |
-Solomon Hykes <solomon@docker.com> (@shykes) |
|
| 2 |
-Olivier Gambier <olivier@docker.com> (@dmp42) |
|
| 3 |
-Stephen Day <stephen.day@docker.com> (@stevvooe) |
|
| 4 |
-Derek McGowan <derek@mcgstyle.net> (@dmcgowan) |
|
| 5 |
-Richard Scothern <richard.scothern@gmail.com> (@richardscothern) |
|
| 6 |
-Aaron Lehmann <aaron.lehmann@docker.com> (@aaronlehmann) |
|
| 1 |
+# Distribution maintainers file |
|
| 2 |
+# |
|
| 3 |
+# This file describes who runs the docker/distribution project and how. |
|
| 4 |
+# This is a living document - if you see something out of date or missing, speak up! |
|
| 5 |
+# |
|
| 6 |
+# It is structured to be consumable by both humans and programs. |
|
| 7 |
+# To extract its contents programmatically, use any TOML-compliant parser. |
|
| 8 |
+# |
|
| 9 |
+# This file is compiled into the MAINTAINERS file in docker/opensource. |
|
| 10 |
+# |
|
| 11 |
+[Org] |
|
| 12 |
+ [Org."Core maintainers"] |
|
| 13 |
+ people = [ |
|
| 14 |
+ "aaronlehmann", |
|
| 15 |
+ "dmcgowan", |
|
| 16 |
+ "dmp42", |
|
| 17 |
+ "richardscothern", |
|
| 18 |
+ "shykes", |
|
| 19 |
+ "stevvooe", |
|
| 20 |
+ ] |
|
| 7 | 21 |
|
| 22 |
+[people] |
|
| 8 | 23 |
|
| 24 |
+# A reference list of all people associated with the project. |
|
| 25 |
+# All other sections should refer to people by their canonical key |
|
| 26 |
+# in the people section. |
|
| 27 |
+ |
|
| 28 |
+ # ADD YOURSELF HERE IN ALPHABETICAL ORDER |
|
| 29 |
+ |
|
| 30 |
+ [people.aaronlehmann] |
|
| 31 |
+ Name = "Aaron Lehmann" |
|
| 32 |
+ Email = "aaron.lehmann@docker.com" |
|
| 33 |
+ GitHub = "aaronlehmann" |
|
| 34 |
+ |
|
| 35 |
+ [people.dmcgowan] |
|
| 36 |
+ Name = "Derek McGowan" |
|
| 37 |
+ Email = "derek@mcgstyle.net" |
|
| 38 |
+ GitHub = "dmcgowan" |
|
| 39 |
+ |
|
| 40 |
+ [people.dmp42] |
|
| 41 |
+ Name = "Olivier Gambier" |
|
| 42 |
+ Email = "olivier@docker.com" |
|
| 43 |
+ GitHub = "dmp42" |
|
| 44 |
+ |
|
| 45 |
+ [people.richardscothern] |
|
| 46 |
+ Name = "Richard Scothern" |
|
| 47 |
+ Email = "richard.scothern@gmail.com" |
|
| 48 |
+ GitHub = "richardscothern" |
|
| 49 |
+ |
|
| 50 |
+ [people.shykes] |
|
| 51 |
+ Name = "Solomon Hykes" |
|
| 52 |
+ Email = "solomon@docker.com" |
|
| 53 |
+ GitHub = "shykes" |
|
| 54 |
+ |
|
| 55 |
+ [people.stevvooe] |
|
| 56 |
+ Name = "Stephen Day" |
|
| 57 |
+ Email = "stephen.day@docker.com" |
|
| 58 |
+ GitHub = "stevvooe" |
| ... | ... |
@@ -17,9 +17,9 @@ This repository contains the following components: |
| 17 | 17 |
|**Component** |Description | |
| 18 | 18 |
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
| 19 | 19 |
| **registry** | An implementation of the [Docker Registry HTTP API V2](docs/spec/api.md) for use with docker 1.6+. | |
| 20 |
-| **libraries** | A rich set of libraries for interacting with,distribution components. Please see [godoc](http://godoc.org/github.com/docker/distribution) for details. **Note**: These libraries are **unstable**. | |
|
| 20 |
+| **libraries** | A rich set of libraries for interacting with,distribution components. Please see [godoc](https://godoc.org/github.com/docker/distribution) for details. **Note**: These libraries are **unstable**. | |
|
| 21 | 21 |
| **specifications** | _Distribution_ related specifications are available in [docs/spec](docs/spec) | |
| 22 |
-| **documentation** | Docker's full documentation set is available at [docs.docker.com](http://docs.docker.com). This repository [contains the subset](docs/index.md) related just to the registry. | |
|
| 22 |
+| **documentation** | Docker's full documentation set is available at [docs.docker.com](https://docs.docker.com). This repository [contains the subset](docs/index.md) related just to the registry. | |
|
| 23 | 23 |
|
| 24 | 24 |
### How does this integrate with Docker engine? |
| 25 | 25 |
|
| ... | ... |
@@ -58,7 +58,7 @@ For information on upcoming functionality, please see [ROADMAP.md](ROADMAP.md). |
| 58 | 58 |
### Who needs to deploy a registry? |
| 59 | 59 |
|
| 60 | 60 |
By default, Docker users pull images from Docker's public registry instance. |
| 61 |
-[Installing Docker](http://docs.docker.com/installation) gives users this |
|
| 61 |
+[Installing Docker](https://docs.docker.com/engine/installation/) gives users this |
|
| 62 | 62 |
ability. Users can also push images to a repository on Docker's public registry, |
| 63 | 63 |
if they have a [Docker Hub](https://hub.docker.com/) account. |
| 64 | 64 |
|
| ... | ... |
@@ -61,6 +61,15 @@ type Descriptor struct {
|
| 61 | 61 |
// depend on the simplicity of this type. |
| 62 | 62 |
} |
| 63 | 63 |
|
| 64 |
+// Descriptor returns the descriptor, to make it satisfy the Describable |
|
| 65 |
+// interface. Note that implementations of Describable are generally objects |
|
| 66 |
+// which can be described, not simply descriptors; this exception is in place |
|
| 67 |
+// to make it more convenient to pass actual descriptors to functions that |
|
| 68 |
+// expect Describable objects. |
|
| 69 |
+func (d Descriptor) Descriptor() Descriptor {
|
|
| 70 |
+ return d |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 64 | 73 |
// BlobStatter makes blob descriptors available by digest. The service may |
| 65 | 74 |
// provide a descriptor of a different digest if the provided digest is not |
| 66 | 75 |
// canonical. |
| ... | ... |
@@ -6,6 +6,8 @@ machine: |
| 6 | 6 |
# Install ceph to test rados driver & create pool |
| 7 | 7 |
- sudo -i ~/distribution/contrib/ceph/ci-setup.sh |
| 8 | 8 |
- ceph osd pool create docker-distribution 1 |
| 9 |
+ # Install codecov for coverage |
|
| 10 |
+ - pip install --user codecov |
|
| 9 | 11 |
|
| 10 | 12 |
post: |
| 11 | 13 |
# go |
| ... | ... |
@@ -45,9 +47,6 @@ dependencies: |
| 45 | 45 |
- > |
| 46 | 46 |
gvm use stable && |
| 47 | 47 |
go get github.com/axw/gocov/gocov github.com/golang/lint/golint |
| 48 |
- |
|
| 49 |
- # Disabling goveralls for now |
|
| 50 |
- # go get github.com/axw/gocov/gocov github.com/mattn/goveralls github.com/golang/lint/golint |
|
| 51 | 48 |
|
| 52 | 49 |
test: |
| 53 | 50 |
pre: |
| ... | ... |
@@ -73,25 +72,17 @@ test: |
| 73 | 73 |
pwd: $BASE_STABLE |
| 74 | 74 |
|
| 75 | 75 |
override: |
| 76 |
- |
|
| 77 | 76 |
# Test stable, and report |
| 78 |
- # Preset the goverall report file |
|
| 79 |
- # - echo "$CIRCLE_PAIN" > ~/goverage.report |
|
| 80 |
- |
|
| 81 |
- - gvm use stable; go list ./... | xargs -L 1 -I{} rm -f $GOPATH/src/{}/coverage.out:
|
|
| 82 |
- pwd: $BASE_STABLE |
|
| 83 |
- |
|
| 84 |
- - gvm use stable; go list -tags "$DOCKER_BUILDTAGS" ./... | xargs -L 1 -I{} godep go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/{}/coverage.out {}:
|
|
| 77 |
+ - gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | xargs -L 1 -I{} bash -c 'export PACKAGE={}; godep go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/$PACKAGE/coverage.out -coverpkg=$(./coverpkg.sh $PACKAGE $ROOT_PACKAGE) $PACKAGE':
|
|
| 85 | 78 |
timeout: 600 |
| 86 | 79 |
pwd: $BASE_STABLE |
| 87 | 80 |
|
| 88 | 81 |
post: |
| 89 |
- # Aggregate and report to coveralls |
|
| 90 |
- - gvm use stable; go list -tags "$DOCKER_BUILDTAGS" ./... | xargs -L 1 -I{} cat "$GOPATH/src/{}/coverage.out" | grep -v "$CIRCLE_PAIN" >> ~/goverage.report:
|
|
| 82 |
+ # Report to codecov |
|
| 83 |
+ - bash <(curl -s https://codecov.io/bash): |
|
| 91 | 84 |
pwd: $BASE_STABLE |
| 92 | 85 |
|
| 93 | 86 |
## Notes |
| 94 |
- # Disabled coveralls reporting: build breaking sending coverage data to coveralls |
|
| 95 | 87 |
# Disabled the -race detector due to massive memory usage. |
| 96 | 88 |
# Do we want these as well? |
| 97 | 89 |
# - go get code.google.com/p/go.tools/cmd/goimports |
| 98 | 90 |
new file mode 100755 |
| ... | ... |
@@ -0,0 +1,7 @@ |
| 0 |
+#!/usr/bin/env bash |
|
| 1 |
+# Given a subpackage and the containing package, figures out which packages |
|
| 2 |
+# need to be passed to `go test -coverpkg`: this includes all of the |
|
| 3 |
+# subpackage's dependencies within the containing package, as well as the |
|
| 4 |
+# subpackage itself. |
|
| 5 |
+DEPENDENCIES="$(go list -f $'{{range $f := .Deps}}{{$f}}\n{{end}}' ${1} | grep ${2})"
|
|
| 6 |
+echo "${1} ${DEPENDENCIES}" | xargs echo -n | tr ' ' ','
|
| ... | ... |
@@ -1,21 +1,14 @@ |
| 1 | 1 |
package digest |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "bytes" |
|
| 5 | 4 |
"fmt" |
| 6 | 5 |
"hash" |
| 7 | 6 |
"io" |
| 8 |
- "io/ioutil" |
|
| 9 | 7 |
"regexp" |
| 10 | 8 |
"strings" |
| 11 |
- |
|
| 12 |
- "github.com/docker/docker/pkg/tarsum" |
|
| 13 | 9 |
) |
| 14 | 10 |
|
| 15 | 11 |
const ( |
| 16 |
- // DigestTarSumV1EmptyTar is the digest for the empty tar file. |
|
| 17 |
- DigestTarSumV1EmptyTar = "tarsum.v1+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" |
|
| 18 |
- |
|
| 19 | 12 |
// DigestSha256EmptyTar is the canonical sha256 digest of empty data |
| 20 | 13 |
DigestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" |
| 21 | 14 |
) |
| ... | ... |
@@ -29,18 +22,21 @@ const ( |
| 29 | 29 |
// |
| 30 | 30 |
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc |
| 31 | 31 |
// |
| 32 |
-// More important for this code base, this type is compatible with tarsum |
|
| 33 |
-// digests. For example, the following would be a valid Digest: |
|
| 34 |
-// |
|
| 35 |
-// tarsum+sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b |
|
| 36 |
-// |
|
| 37 | 32 |
// This allows to abstract the digest behind this type and work only in those |
| 38 | 33 |
// terms. |
| 39 | 34 |
type Digest string |
| 40 | 35 |
|
| 41 | 36 |
// NewDigest returns a Digest from alg and a hash.Hash object. |
| 42 | 37 |
func NewDigest(alg Algorithm, h hash.Hash) Digest {
|
| 43 |
- return Digest(fmt.Sprintf("%s:%x", alg, h.Sum(nil)))
|
|
| 38 |
+ return NewDigestFromBytes(alg, h.Sum(nil)) |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+// NewDigestFromBytes returns a new digest from the byte contents of p. |
|
| 42 |
+// Typically, this can come from hash.Hash.Sum(...) or xxx.SumXXX(...) |
|
| 43 |
+// functions. This is also useful for rebuilding digests from binary |
|
| 44 |
+// serializations. |
|
| 45 |
+func NewDigestFromBytes(alg Algorithm, p []byte) Digest {
|
|
| 46 |
+ return Digest(fmt.Sprintf("%s:%x", alg, p))
|
|
| 44 | 47 |
} |
| 45 | 48 |
|
| 46 | 49 |
// NewDigestFromHex returns a Digest from alg and a the hex encoded digest. |
| ... | ... |
@@ -79,41 +75,15 @@ func FromReader(rd io.Reader) (Digest, error) {
|
| 79 | 79 |
return Canonical.FromReader(rd) |
| 80 | 80 |
} |
| 81 | 81 |
|
| 82 |
-// FromTarArchive produces a tarsum digest from reader rd. |
|
| 83 |
-func FromTarArchive(rd io.Reader) (Digest, error) {
|
|
| 84 |
- ts, err := tarsum.NewTarSum(rd, true, tarsum.Version1) |
|
| 85 |
- if err != nil {
|
|
| 86 |
- return "", err |
|
| 87 |
- } |
|
| 88 |
- |
|
| 89 |
- if _, err := io.Copy(ioutil.Discard, ts); err != nil {
|
|
| 90 |
- return "", err |
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- d, err := ParseDigest(ts.Sum(nil)) |
|
| 94 |
- if err != nil {
|
|
| 95 |
- return "", err |
|
| 96 |
- } |
|
| 97 |
- |
|
| 98 |
- return d, nil |
|
| 99 |
-} |
|
| 100 |
- |
|
| 101 | 82 |
// FromBytes digests the input and returns a Digest. |
| 102 |
-func FromBytes(p []byte) (Digest, error) {
|
|
| 103 |
- return FromReader(bytes.NewReader(p)) |
|
| 83 |
+func FromBytes(p []byte) Digest {
|
|
| 84 |
+ return Canonical.FromBytes(p) |
|
| 104 | 85 |
} |
| 105 | 86 |
|
| 106 | 87 |
// Validate checks that the contents of d is a valid digest, returning an |
| 107 | 88 |
// error if not. |
| 108 | 89 |
func (d Digest) Validate() error {
|
| 109 | 90 |
s := string(d) |
| 110 |
- // Common case will be tarsum |
|
| 111 |
- _, err := ParseTarSum(s) |
|
| 112 |
- if err == nil {
|
|
| 113 |
- return nil |
|
| 114 |
- } |
|
| 115 |
- |
|
| 116 |
- // Continue on for general parser |
|
| 117 | 91 |
|
| 118 | 92 |
if !DigestRegexpAnchored.MatchString(s) {
|
| 119 | 93 |
return ErrDigestInvalidFormat |
| ... | ... |
@@ -2,6 +2,7 @@ package digest |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"crypto" |
| 5 |
+ "fmt" |
|
| 5 | 6 |
"hash" |
| 6 | 7 |
"io" |
| 7 | 8 |
) |
| ... | ... |
@@ -13,10 +14,9 @@ type Algorithm string |
| 13 | 13 |
|
| 14 | 14 |
// supported digest types |
| 15 | 15 |
const ( |
| 16 |
- SHA256 Algorithm = "sha256" // sha256 with hex encoding |
|
| 17 |
- SHA384 Algorithm = "sha384" // sha384 with hex encoding |
|
| 18 |
- SHA512 Algorithm = "sha512" // sha512 with hex encoding |
|
| 19 |
- TarsumV1SHA256 Algorithm = "tarsum+v1+sha256" // supported tarsum version, verification only |
|
| 16 |
+ SHA256 Algorithm = "sha256" // sha256 with hex encoding |
|
| 17 |
+ SHA384 Algorithm = "sha384" // sha384 with hex encoding |
|
| 18 |
+ SHA512 Algorithm = "sha512" // sha512 with hex encoding |
|
| 20 | 19 |
|
| 21 | 20 |
// Canonical is the primary digest algorithm used with the distribution |
| 22 | 21 |
// project. Other digests may be used but this one is the primary storage |
| ... | ... |
@@ -85,11 +85,18 @@ func (a Algorithm) New() Digester {
|
| 85 | 85 |
} |
| 86 | 86 |
} |
| 87 | 87 |
|
| 88 |
-// Hash returns a new hash as used by the algorithm. If not available, nil is |
|
| 89 |
-// returned. Make sure to check Available before calling. |
|
| 88 |
+// Hash returns a new hash as used by the algorithm. If not available, the |
|
| 89 |
+// method will panic. Check Algorithm.Available() before calling. |
|
| 90 | 90 |
func (a Algorithm) Hash() hash.Hash {
|
| 91 | 91 |
if !a.Available() {
|
| 92 |
- return nil |
|
| 92 |
+ // NOTE(stevvooe): A missing hash is usually a programming error that |
|
| 93 |
+ // must be resolved at compile time. We don't import in the digest |
|
| 94 |
+ // package to allow users to choose their hash implementation (such as |
|
| 95 |
+ // when using stevvooe/resumable or a hardware accelerated package). |
|
| 96 |
+ // |
|
| 97 |
+ // Applications that may want to resolve the hash at runtime should |
|
| 98 |
+ // call Algorithm.Available before call Algorithm.Hash(). |
|
| 99 |
+ panic(fmt.Sprintf("%v not available (make sure it is imported)", a))
|
|
| 93 | 100 |
} |
| 94 | 101 |
|
| 95 | 102 |
return algorithms[a].New() |
| ... | ... |
@@ -106,6 +113,22 @@ func (a Algorithm) FromReader(rd io.Reader) (Digest, error) {
|
| 106 | 106 |
return digester.Digest(), nil |
| 107 | 107 |
} |
| 108 | 108 |
|
| 109 |
+// FromBytes digests the input and returns a Digest. |
|
| 110 |
+func (a Algorithm) FromBytes(p []byte) Digest {
|
|
| 111 |
+ digester := a.New() |
|
| 112 |
+ |
|
| 113 |
+ if _, err := digester.Hash().Write(p); err != nil {
|
|
| 114 |
+ // Writes to a Hash should never fail. None of the existing |
|
| 115 |
+ // hash implementations in the stdlib or hashes vendored |
|
| 116 |
+ // here can return errors from Write. Having a panic in this |
|
| 117 |
+ // condition instead of having FromBytes return an error value |
|
| 118 |
+ // avoids unnecessary error handling paths in all callers. |
|
| 119 |
+ panic("write to hash function returned error: " + err.Error())
|
|
| 120 |
+ } |
|
| 121 |
+ |
|
| 122 |
+ return digester.Digest() |
|
| 123 |
+} |
|
| 124 |
+ |
|
| 109 | 125 |
// TODO(stevvooe): Allow resolution of verifiers using the digest type and |
| 110 | 126 |
// this registration system. |
| 111 | 127 |
|
| ... | ... |
@@ -1,7 +1,7 @@ |
| 1 | 1 |
// Package digest provides a generalized type to opaquely represent message |
| 2 | 2 |
// digests and their operations within the registry. The Digest type is |
| 3 | 3 |
// designed to serve as a flexible identifier in a content-addressable system. |
| 4 |
-// More importantly, it provides tools and wrappers to work with tarsums and |
|
| 4 |
+// More importantly, it provides tools and wrappers to work with |
|
| 5 | 5 |
// hash.Hash-based digests with little effort. |
| 6 | 6 |
// |
| 7 | 7 |
// Basics |
| ... | ... |
@@ -16,17 +16,7 @@ |
| 16 | 16 |
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc |
| 17 | 17 |
// |
| 18 | 18 |
// In this case, the string "sha256" is the algorithm and the hex bytes are |
| 19 |
-// the "digest". A tarsum example will be more illustrative of the use case |
|
| 20 |
-// involved in the registry: |
|
| 21 |
-// |
|
| 22 |
-// tarsum+sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b |
|
| 23 |
-// |
|
| 24 |
-// For this, we consider the algorithm to be "tarsum+sha256". Prudent |
|
| 25 |
-// applications will favor the ParseDigest function to verify the format over |
|
| 26 |
-// using simple type casts. However, a normal string can be cast as a digest |
|
| 27 |
-// with a simple type conversion: |
|
| 28 |
-// |
|
| 29 |
-// Digest("tarsum+sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b")
|
|
| 19 |
+// the "digest". |
|
| 30 | 20 |
// |
| 31 | 21 |
// Because the Digest type is simply a string, once a valid Digest is |
| 32 | 22 |
// obtained, comparisons are cheap, quick and simple to express with the |
| 33 | 23 |
deleted file mode 100644 |
| ... | ... |
@@ -1,70 +0,0 @@ |
| 1 |
-package digest |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "fmt" |
|
| 5 |
- |
|
| 6 |
- "regexp" |
|
| 7 |
-) |
|
| 8 |
- |
|
| 9 |
-// TarsumRegexp defines a regular expression to match tarsum identifiers. |
|
| 10 |
-var TarsumRegexp = regexp.MustCompile("tarsum(?:.[a-z0-9]+)?\\+[a-zA-Z0-9]+:[A-Fa-f0-9]+")
|
|
| 11 |
- |
|
| 12 |
-// TarsumRegexpCapturing defines a regular expression to match tarsum identifiers with |
|
| 13 |
-// capture groups corresponding to each component. |
|
| 14 |
-var TarsumRegexpCapturing = regexp.MustCompile("(tarsum)(.([a-z0-9]+))?\\+([a-zA-Z0-9]+):([A-Fa-f0-9]+)")
|
|
| 15 |
- |
|
| 16 |
-// TarSumInfo contains information about a parsed tarsum. |
|
| 17 |
-type TarSumInfo struct {
|
|
| 18 |
- // Version contains the version of the tarsum. |
|
| 19 |
- Version string |
|
| 20 |
- |
|
| 21 |
- // Algorithm contains the algorithm for the final digest |
|
| 22 |
- Algorithm string |
|
| 23 |
- |
|
| 24 |
- // Digest contains the hex-encoded digest. |
|
| 25 |
- Digest string |
|
| 26 |
-} |
|
| 27 |
- |
|
| 28 |
-// InvalidTarSumError provides informations about a TarSum that cannot be parsed |
|
| 29 |
-// by ParseTarSum. |
|
| 30 |
-type InvalidTarSumError string |
|
| 31 |
- |
|
| 32 |
-func (e InvalidTarSumError) Error() string {
|
|
| 33 |
- return fmt.Sprintf("invalid tarsum: %q", string(e))
|
|
| 34 |
-} |
|
| 35 |
- |
|
| 36 |
-// ParseTarSum parses a tarsum string into its components of interest. For |
|
| 37 |
-// example, this method may receive the tarsum in the following format: |
|
| 38 |
-// |
|
| 39 |
-// tarsum.v1+sha256:220a60ecd4a3c32c282622a625a54db9ba0ff55b5ba9c29c7064a2bc358b6a3e |
|
| 40 |
-// |
|
| 41 |
-// The function will return the following: |
|
| 42 |
-// |
|
| 43 |
-// TarSumInfo{
|
|
| 44 |
-// Version: "v1", |
|
| 45 |
-// Algorithm: "sha256", |
|
| 46 |
-// Digest: "220a60ecd4a3c32c282622a625a54db9ba0ff55b5ba9c29c7064a2bc358b6a3e", |
|
| 47 |
-// } |
|
| 48 |
-// |
|
| 49 |
-func ParseTarSum(tarSum string) (tsi TarSumInfo, err error) {
|
|
| 50 |
- components := TarsumRegexpCapturing.FindStringSubmatch(tarSum) |
|
| 51 |
- |
|
| 52 |
- if len(components) != 1+TarsumRegexpCapturing.NumSubexp() {
|
|
| 53 |
- return TarSumInfo{}, InvalidTarSumError(tarSum)
|
|
| 54 |
- } |
|
| 55 |
- |
|
| 56 |
- return TarSumInfo{
|
|
| 57 |
- Version: components[3], |
|
| 58 |
- Algorithm: components[4], |
|
| 59 |
- Digest: components[5], |
|
| 60 |
- }, nil |
|
| 61 |
-} |
|
| 62 |
- |
|
| 63 |
-// String returns the valid, string representation of the tarsum info. |
|
| 64 |
-func (tsi TarSumInfo) String() string {
|
|
| 65 |
- if tsi.Version == "" {
|
|
| 66 |
- return fmt.Sprintf("tarsum+%s:%s", tsi.Algorithm, tsi.Digest)
|
|
| 67 |
- } |
|
| 68 |
- |
|
| 69 |
- return fmt.Sprintf("tarsum.%s+%s:%s", tsi.Version, tsi.Algorithm, tsi.Digest)
|
|
| 70 |
-} |
| ... | ... |
@@ -3,9 +3,6 @@ package digest |
| 3 | 3 |
import ( |
| 4 | 4 |
"hash" |
| 5 | 5 |
"io" |
| 6 |
- "io/ioutil" |
|
| 7 |
- |
|
| 8 |
- "github.com/docker/docker/pkg/tarsum" |
|
| 9 | 6 |
) |
| 10 | 7 |
|
| 11 | 8 |
// Verifier presents a general verification interface to be used with message |
| ... | ... |
@@ -27,70 +24,10 @@ func NewDigestVerifier(d Digest) (Verifier, error) {
|
| 27 | 27 |
return nil, err |
| 28 | 28 |
} |
| 29 | 29 |
|
| 30 |
- alg := d.Algorithm() |
|
| 31 |
- switch alg {
|
|
| 32 |
- case "sha256", "sha384", "sha512": |
|
| 33 |
- return hashVerifier{
|
|
| 34 |
- hash: alg.Hash(), |
|
| 35 |
- digest: d, |
|
| 36 |
- }, nil |
|
| 37 |
- default: |
|
| 38 |
- // Assume we have a tarsum. |
|
| 39 |
- version, err := tarsum.GetVersionFromTarsum(string(d)) |
|
| 40 |
- if err != nil {
|
|
| 41 |
- return nil, err |
|
| 42 |
- } |
|
| 43 |
- |
|
| 44 |
- pr, pw := io.Pipe() |
|
| 45 |
- |
|
| 46 |
- // TODO(stevvooe): We may actually want to ban the earlier versions of |
|
| 47 |
- // tarsum. That decision may not be the place of the verifier. |
|
| 48 |
- |
|
| 49 |
- ts, err := tarsum.NewTarSum(pr, true, version) |
|
| 50 |
- if err != nil {
|
|
| 51 |
- return nil, err |
|
| 52 |
- } |
|
| 53 |
- |
|
| 54 |
- // TODO(sday): Ick! A goroutine per digest verification? We'll have to |
|
| 55 |
- // get the tarsum library to export an io.Writer variant. |
|
| 56 |
- go func() {
|
|
| 57 |
- if _, err := io.Copy(ioutil.Discard, ts); err != nil {
|
|
| 58 |
- pr.CloseWithError(err) |
|
| 59 |
- } else {
|
|
| 60 |
- pr.Close() |
|
| 61 |
- } |
|
| 62 |
- }() |
|
| 63 |
- |
|
| 64 |
- return &tarsumVerifier{
|
|
| 65 |
- digest: d, |
|
| 66 |
- ts: ts, |
|
| 67 |
- pr: pr, |
|
| 68 |
- pw: pw, |
|
| 69 |
- }, nil |
|
| 70 |
- } |
|
| 71 |
-} |
|
| 72 |
- |
|
| 73 |
-// NewLengthVerifier returns a verifier that returns true when the number of |
|
| 74 |
-// read bytes equals the expected parameter. |
|
| 75 |
-func NewLengthVerifier(expected int64) Verifier {
|
|
| 76 |
- return &lengthVerifier{
|
|
| 77 |
- expected: expected, |
|
| 78 |
- } |
|
| 79 |
-} |
|
| 80 |
- |
|
| 81 |
-type lengthVerifier struct {
|
|
| 82 |
- expected int64 // expected bytes read |
|
| 83 |
- len int64 // bytes read |
|
| 84 |
-} |
|
| 85 |
- |
|
| 86 |
-func (lv *lengthVerifier) Write(p []byte) (n int, err error) {
|
|
| 87 |
- n = len(p) |
|
| 88 |
- lv.len += int64(n) |
|
| 89 |
- return n, err |
|
| 90 |
-} |
|
| 91 |
- |
|
| 92 |
-func (lv *lengthVerifier) Verified() bool {
|
|
| 93 |
- return lv.expected == lv.len |
|
| 30 |
+ return hashVerifier{
|
|
| 31 |
+ hash: d.Algorithm().Hash(), |
|
| 32 |
+ digest: d, |
|
| 33 |
+ }, nil |
|
| 94 | 34 |
} |
| 95 | 35 |
|
| 96 | 36 |
type hashVerifier struct {
|
| ... | ... |
@@ -105,18 +42,3 @@ func (hv hashVerifier) Write(p []byte) (n int, err error) {
|
| 105 | 105 |
func (hv hashVerifier) Verified() bool {
|
| 106 | 106 |
return hv.digest == NewDigest(hv.digest.Algorithm(), hv.hash) |
| 107 | 107 |
} |
| 108 |
- |
|
| 109 |
-type tarsumVerifier struct {
|
|
| 110 |
- digest Digest |
|
| 111 |
- ts tarsum.TarSum |
|
| 112 |
- pr *io.PipeReader |
|
| 113 |
- pw *io.PipeWriter |
|
| 114 |
-} |
|
| 115 |
- |
|
| 116 |
-func (tv *tarsumVerifier) Write(p []byte) (n int, err error) {
|
|
| 117 |
- return tv.pw.Write(p) |
|
| 118 |
-} |
|
| 119 |
- |
|
| 120 |
-func (tv *tarsumVerifier) Verified() bool {
|
|
| 121 |
- return tv.digest == Digest(tv.ts.Sum(nil)) |
|
| 122 |
-} |
| ... | ... |
@@ -16,6 +16,15 @@ var ErrManifestNotModified = errors.New("manifest not modified")
|
| 16 | 16 |
// performed |
| 17 | 17 |
var ErrUnsupported = errors.New("operation unsupported")
|
| 18 | 18 |
|
| 19 |
+// ErrTagUnknown is returned if the given tag is not known by the tag service |
|
| 20 |
+type ErrTagUnknown struct {
|
|
| 21 |
+ Tag string |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (err ErrTagUnknown) Error() string {
|
|
| 25 |
+ return fmt.Sprintf("unknown tag=%s", err.Tag)
|
|
| 26 |
+} |
|
| 27 |
+ |
|
| 19 | 28 |
// ErrRepositoryUnknown is returned if the named repository is not known by |
| 20 | 29 |
// the registry. |
| 21 | 30 |
type ErrRepositoryUnknown struct {
|
| 22 | 31 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,147 @@ |
| 0 |
+package manifestlist |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/distribution" |
|
| 8 |
+ "github.com/docker/distribution/digest" |
|
| 9 |
+ "github.com/docker/distribution/manifest" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// MediaTypeManifestList specifies the mediaType for manifest lists. |
|
| 13 |
+const MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" |
|
| 14 |
+ |
|
| 15 |
+// SchemaVersion provides a pre-initialized version structure for this |
|
| 16 |
+// packages version of the manifest. |
|
| 17 |
+var SchemaVersion = manifest.Versioned{
|
|
| 18 |
+ SchemaVersion: 2, |
|
| 19 |
+ MediaType: MediaTypeManifestList, |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+func init() {
|
|
| 23 |
+ manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
|
| 24 |
+ m := new(DeserializedManifestList) |
|
| 25 |
+ err := m.UnmarshalJSON(b) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return nil, distribution.Descriptor{}, err
|
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ dgst := digest.FromBytes(b) |
|
| 31 |
+ return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifestList}, err
|
|
| 32 |
+ } |
|
| 33 |
+ err := distribution.RegisterManifestSchema(MediaTypeManifestList, manifestListFunc) |
|
| 34 |
+ if err != nil {
|
|
| 35 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 36 |
+ } |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+// PlatformSpec specifies a platform where a particular image manifest is |
|
| 40 |
+// applicable. |
|
| 41 |
+type PlatformSpec struct {
|
|
| 42 |
+ // Architecture field specifies the CPU architecture, for example |
|
| 43 |
+ // `amd64` or `ppc64`. |
|
| 44 |
+ Architecture string `json:"architecture"` |
|
| 45 |
+ |
|
| 46 |
+ // OS specifies the operating system, for example `linux` or `windows`. |
|
| 47 |
+ OS string `json:"os"` |
|
| 48 |
+ |
|
| 49 |
+ // Variant is an optional field specifying a variant of the CPU, for |
|
| 50 |
+ // example `ppc64le` to specify a little-endian version of a PowerPC CPU. |
|
| 51 |
+ Variant string `json:"variant,omitempty"` |
|
| 52 |
+ |
|
| 53 |
+ // Features is an optional field specifuing an array of strings, each |
|
| 54 |
+ // listing a required CPU feature (for example `sse4` or `aes`). |
|
| 55 |
+ Features []string `json:"features,omitempty"` |
|
| 56 |
+} |
|
| 57 |
+ |
|
| 58 |
+// A ManifestDescriptor references a platform-specific manifest. |
|
| 59 |
+type ManifestDescriptor struct {
|
|
| 60 |
+ distribution.Descriptor |
|
| 61 |
+ |
|
| 62 |
+ // Platform specifies which platform the manifest pointed to by the |
|
| 63 |
+ // descriptor runs on. |
|
| 64 |
+ Platform PlatformSpec `json:"platform"` |
|
| 65 |
+} |
|
| 66 |
+ |
|
| 67 |
+// ManifestList references manifests for various platforms. |
|
| 68 |
+type ManifestList struct {
|
|
| 69 |
+ manifest.Versioned |
|
| 70 |
+ |
|
| 71 |
+ // Config references the image configuration as a blob. |
|
| 72 |
+ Manifests []ManifestDescriptor `json:"manifests"` |
|
| 73 |
+} |
|
| 74 |
+ |
|
| 75 |
+// References returnes the distribution descriptors for the referenced image |
|
| 76 |
+// manifests. |
|
| 77 |
+func (m ManifestList) References() []distribution.Descriptor {
|
|
| 78 |
+ dependencies := make([]distribution.Descriptor, len(m.Manifests)) |
|
| 79 |
+ for i := range m.Manifests {
|
|
| 80 |
+ dependencies[i] = m.Manifests[i].Descriptor |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ return dependencies |
|
| 84 |
+} |
|
| 85 |
+ |
|
| 86 |
+// DeserializedManifestList wraps ManifestList with a copy of the original |
|
| 87 |
+// JSON. |
|
| 88 |
+type DeserializedManifestList struct {
|
|
| 89 |
+ ManifestList |
|
| 90 |
+ |
|
| 91 |
+ // canonical is the canonical byte representation of the Manifest. |
|
| 92 |
+ canonical []byte |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// FromDescriptors takes a slice of descriptors, and returns a |
|
| 96 |
+// DeserializedManifestList which contains the resulting manifest list |
|
| 97 |
+// and its JSON representation. |
|
| 98 |
+func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) {
|
|
| 99 |
+ m := ManifestList{
|
|
| 100 |
+ Versioned: SchemaVersion, |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors)) |
|
| 104 |
+ copy(m.Manifests, descriptors) |
|
| 105 |
+ |
|
| 106 |
+ deserialized := DeserializedManifestList{
|
|
| 107 |
+ ManifestList: m, |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 110 |
+ var err error |
|
| 111 |
+ deserialized.canonical, err = json.MarshalIndent(&m, "", " ") |
|
| 112 |
+ return &deserialized, err |
|
| 113 |
+} |
|
| 114 |
+ |
|
| 115 |
+// UnmarshalJSON populates a new ManifestList struct from JSON data. |
|
| 116 |
+func (m *DeserializedManifestList) UnmarshalJSON(b []byte) error {
|
|
| 117 |
+ m.canonical = make([]byte, len(b), len(b)) |
|
| 118 |
+ // store manifest list in canonical |
|
| 119 |
+ copy(m.canonical, b) |
|
| 120 |
+ |
|
| 121 |
+ // Unmarshal canonical JSON into ManifestList object |
|
| 122 |
+ var manifestList ManifestList |
|
| 123 |
+ if err := json.Unmarshal(m.canonical, &manifestList); err != nil {
|
|
| 124 |
+ return err |
|
| 125 |
+ } |
|
| 126 |
+ |
|
| 127 |
+ m.ManifestList = manifestList |
|
| 128 |
+ |
|
| 129 |
+ return nil |
|
| 130 |
+} |
|
| 131 |
+ |
|
| 132 |
+// MarshalJSON returns the contents of canonical. If canonical is empty, |
|
| 133 |
+// marshals the inner contents. |
|
| 134 |
+func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) {
|
|
| 135 |
+ if len(m.canonical) > 0 {
|
|
| 136 |
+ return m.canonical, nil |
|
| 137 |
+ } |
|
| 138 |
+ |
|
| 139 |
+ return nil, errors.New("JSON representation not initialized in DeserializedManifestList")
|
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+// Payload returns the raw content of the manifest list. The contents can be |
|
| 143 |
+// used to calculate the content identifier. |
|
| 144 |
+func (m DeserializedManifestList) Payload() (string, []byte, error) {
|
|
| 145 |
+ return m.MediaType, m.canonical, nil |
|
| 146 |
+} |
| 0 | 147 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,278 @@ |
| 0 |
+package schema1 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "crypto/sha512" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "errors" |
|
| 6 |
+ "fmt" |
|
| 7 |
+ "time" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/distribution" |
|
| 10 |
+ "github.com/docker/distribution/context" |
|
| 11 |
+ "github.com/docker/libtrust" |
|
| 12 |
+ |
|
| 13 |
+ "github.com/docker/distribution/digest" |
|
| 14 |
+ "github.com/docker/distribution/manifest" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+type diffID digest.Digest |
|
| 18 |
+ |
|
| 19 |
+// gzippedEmptyTar is a gzip-compressed version of an empty tar file |
|
| 20 |
+// (1024 NULL bytes) |
|
| 21 |
+var gzippedEmptyTar = []byte{
|
|
| 22 |
+ 31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88, |
|
| 23 |
+ 0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0, |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+// digestSHA256GzippedEmptyTar is the canonical sha256 digest of |
|
| 27 |
+// gzippedEmptyTar |
|
| 28 |
+const digestSHA256GzippedEmptyTar = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
|
|
| 29 |
+ |
|
| 30 |
+// configManifestBuilder is a type for constructing manifests from an image |
|
| 31 |
+// configuration and generic descriptors. |
|
| 32 |
+type configManifestBuilder struct {
|
|
| 33 |
+ // bs is a BlobService used to create empty layer tars in the |
|
| 34 |
+ // blob store if necessary. |
|
| 35 |
+ bs distribution.BlobService |
|
| 36 |
+ // pk is the libtrust private key used to sign the final manifest. |
|
| 37 |
+ pk libtrust.PrivateKey |
|
| 38 |
+ // configJSON is configuration supplied when the ManifestBuilder was |
|
| 39 |
+ // created. |
|
| 40 |
+ configJSON []byte |
|
| 41 |
+ // name is the name provided to NewConfigManifestBuilder |
|
| 42 |
+ name string |
|
| 43 |
+ // tag is the tag provided to NewConfigManifestBuilder |
|
| 44 |
+ tag string |
|
| 45 |
+ // descriptors is the set of descriptors referencing the layers. |
|
| 46 |
+ descriptors []distribution.Descriptor |
|
| 47 |
+ // emptyTarDigest is set to a valid digest if an empty tar has been |
|
| 48 |
+ // put in the blob store; otherwise it is empty. |
|
| 49 |
+ emptyTarDigest digest.Digest |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+// NewConfigManifestBuilder is used to build new manifests for the current |
|
| 53 |
+// schema version from an image configuration and a set of descriptors. |
|
| 54 |
+// It takes a BlobService so that it can add an empty tar to the blob store |
|
| 55 |
+// if the resulting manifest needs empty layers. |
|
| 56 |
+func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, name, tag string, configJSON []byte) distribution.ManifestBuilder {
|
|
| 57 |
+ return &configManifestBuilder{
|
|
| 58 |
+ bs: bs, |
|
| 59 |
+ pk: pk, |
|
| 60 |
+ configJSON: configJSON, |
|
| 61 |
+ name: name, |
|
| 62 |
+ tag: tag, |
|
| 63 |
+ } |
|
| 64 |
+} |
|
| 65 |
+ |
|
| 66 |
+// Build produces a final manifest from the given references |
|
| 67 |
+func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) {
|
|
| 68 |
+ type imageRootFS struct {
|
|
| 69 |
+ Type string `json:"type"` |
|
| 70 |
+ DiffIDs []diffID `json:"diff_ids,omitempty"` |
|
| 71 |
+ BaseLayer string `json:"base_layer,omitempty"` |
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ type imageHistory struct {
|
|
| 75 |
+ Created time.Time `json:"created"` |
|
| 76 |
+ Author string `json:"author,omitempty"` |
|
| 77 |
+ CreatedBy string `json:"created_by,omitempty"` |
|
| 78 |
+ Comment string `json:"comment,omitempty"` |
|
| 79 |
+ EmptyLayer bool `json:"empty_layer,omitempty"` |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ type imageConfig struct {
|
|
| 83 |
+ RootFS *imageRootFS `json:"rootfs,omitempty"` |
|
| 84 |
+ History []imageHistory `json:"history,omitempty"` |
|
| 85 |
+ Architecture string `json:"architecture,omitempty"` |
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ var img imageConfig |
|
| 89 |
+ |
|
| 90 |
+ if err := json.Unmarshal(mb.configJSON, &img); err != nil {
|
|
| 91 |
+ return nil, err |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ if len(img.History) == 0 {
|
|
| 95 |
+ return nil, errors.New("empty history when trying to create schema1 manifest")
|
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ if len(img.RootFS.DiffIDs) != len(mb.descriptors) {
|
|
| 99 |
+ return nil, errors.New("number of descriptors and number of layers in rootfs must match")
|
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ // Generate IDs for each layer |
|
| 103 |
+ // For non-top-level layers, create fake V1Compatibility strings that |
|
| 104 |
+ // fit the format and don't collide with anything else, but don't |
|
| 105 |
+ // result in runnable images on their own. |
|
| 106 |
+ type v1Compatibility struct {
|
|
| 107 |
+ ID string `json:"id"` |
|
| 108 |
+ Parent string `json:"parent,omitempty"` |
|
| 109 |
+ Comment string `json:"comment,omitempty"` |
|
| 110 |
+ Created time.Time `json:"created"` |
|
| 111 |
+ ContainerConfig struct {
|
|
| 112 |
+ Cmd []string |
|
| 113 |
+ } `json:"container_config,omitempty"` |
|
| 114 |
+ ThrowAway bool `json:"throwaway,omitempty"` |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ fsLayerList := make([]FSLayer, len(img.History)) |
|
| 118 |
+ history := make([]History, len(img.History)) |
|
| 119 |
+ |
|
| 120 |
+ parent := "" |
|
| 121 |
+ layerCounter := 0 |
|
| 122 |
+ for i, h := range img.History[:len(img.History)-1] {
|
|
| 123 |
+ var blobsum digest.Digest |
|
| 124 |
+ if h.EmptyLayer {
|
|
| 125 |
+ if blobsum, err = mb.emptyTar(ctx); err != nil {
|
|
| 126 |
+ return nil, err |
|
| 127 |
+ } |
|
| 128 |
+ } else {
|
|
| 129 |
+ if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
| 130 |
+ return nil, errors.New("too many non-empty layers in History section")
|
|
| 131 |
+ } |
|
| 132 |
+ blobsum = mb.descriptors[layerCounter].Digest |
|
| 133 |
+ layerCounter++ |
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 136 |
+ v1ID := digest.FromBytes([]byte(blobsum.Hex() + " " + parent)).Hex() |
|
| 137 |
+ |
|
| 138 |
+ if i == 0 && img.RootFS.BaseLayer != "" {
|
|
| 139 |
+ // windows-only baselayer setup |
|
| 140 |
+ baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer)) |
|
| 141 |
+ parent = fmt.Sprintf("%x", baseID[:32])
|
|
| 142 |
+ } |
|
| 143 |
+ |
|
| 144 |
+ v1Compatibility := v1Compatibility{
|
|
| 145 |
+ ID: v1ID, |
|
| 146 |
+ Parent: parent, |
|
| 147 |
+ Comment: h.Comment, |
|
| 148 |
+ Created: h.Created, |
|
| 149 |
+ } |
|
| 150 |
+ v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy}
|
|
| 151 |
+ if h.EmptyLayer {
|
|
| 152 |
+ v1Compatibility.ThrowAway = true |
|
| 153 |
+ } |
|
| 154 |
+ jsonBytes, err := json.Marshal(&v1Compatibility) |
|
| 155 |
+ if err != nil {
|
|
| 156 |
+ return nil, err |
|
| 157 |
+ } |
|
| 158 |
+ |
|
| 159 |
+ reversedIndex := len(img.History) - i - 1 |
|
| 160 |
+ history[reversedIndex].V1Compatibility = string(jsonBytes) |
|
| 161 |
+ fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum}
|
|
| 162 |
+ |
|
| 163 |
+ parent = v1ID |
|
| 164 |
+ } |
|
| 165 |
+ |
|
| 166 |
+ latestHistory := img.History[len(img.History)-1] |
|
| 167 |
+ |
|
| 168 |
+ var blobsum digest.Digest |
|
| 169 |
+ if latestHistory.EmptyLayer {
|
|
| 170 |
+ if blobsum, err = mb.emptyTar(ctx); err != nil {
|
|
| 171 |
+ return nil, err |
|
| 172 |
+ } |
|
| 173 |
+ } else {
|
|
| 174 |
+ if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
| 175 |
+ return nil, errors.New("too many non-empty layers in History section")
|
|
| 176 |
+ } |
|
| 177 |
+ blobsum = mb.descriptors[layerCounter].Digest |
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ fsLayerList[0] = FSLayer{BlobSum: blobsum}
|
|
| 181 |
+ dgst := digest.FromBytes([]byte(blobsum.Hex() + " " + parent + " " + string(mb.configJSON))) |
|
| 182 |
+ |
|
| 183 |
+ // Top-level v1compatibility string should be a modified version of the |
|
| 184 |
+ // image config. |
|
| 185 |
+ transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Hex(), parent, latestHistory.EmptyLayer) |
|
| 186 |
+ if err != nil {
|
|
| 187 |
+ return nil, err |
|
| 188 |
+ } |
|
| 189 |
+ |
|
| 190 |
+ history[0].V1Compatibility = string(transformedConfig) |
|
| 191 |
+ |
|
| 192 |
+ mfst := Manifest{
|
|
| 193 |
+ Versioned: manifest.Versioned{
|
|
| 194 |
+ SchemaVersion: 1, |
|
| 195 |
+ }, |
|
| 196 |
+ Name: mb.name, |
|
| 197 |
+ Tag: mb.tag, |
|
| 198 |
+ Architecture: img.Architecture, |
|
| 199 |
+ FSLayers: fsLayerList, |
|
| 200 |
+ History: history, |
|
| 201 |
+ } |
|
| 202 |
+ |
|
| 203 |
+ return Sign(&mfst, mb.pk) |
|
| 204 |
+} |
|
| 205 |
+ |
|
| 206 |
+// emptyTar pushes a compressed empty tar to the blob store if one doesn't |
|
| 207 |
+// already exist, and returns its blobsum. |
|
| 208 |
+func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) {
|
|
| 209 |
+ if mb.emptyTarDigest != "" {
|
|
| 210 |
+ // Already put an empty tar |
|
| 211 |
+ return mb.emptyTarDigest, nil |
|
| 212 |
+ } |
|
| 213 |
+ |
|
| 214 |
+ descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar) |
|
| 215 |
+ switch err {
|
|
| 216 |
+ case nil: |
|
| 217 |
+ mb.emptyTarDigest = descriptor.Digest |
|
| 218 |
+ return descriptor.Digest, nil |
|
| 219 |
+ case distribution.ErrBlobUnknown: |
|
| 220 |
+ // nop |
|
| 221 |
+ default: |
|
| 222 |
+ return "", err |
|
| 223 |
+ } |
|
| 224 |
+ |
|
| 225 |
+ // Add gzipped empty tar to the blob store |
|
| 226 |
+ descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar) |
|
| 227 |
+ if err != nil {
|
|
| 228 |
+ return "", err |
|
| 229 |
+ } |
|
| 230 |
+ |
|
| 231 |
+ mb.emptyTarDigest = descriptor.Digest |
|
| 232 |
+ |
|
| 233 |
+ return descriptor.Digest, nil |
|
| 234 |
+} |
|
| 235 |
+ |
|
| 236 |
+// AppendReference adds a reference to the current ManifestBuilder |
|
| 237 |
+func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error {
|
|
| 238 |
+ // todo: verification here? |
|
| 239 |
+ mb.descriptors = append(mb.descriptors, d.Descriptor()) |
|
| 240 |
+ return nil |
|
| 241 |
+} |
|
| 242 |
+ |
|
| 243 |
+// References returns the current references added to this builder |
|
| 244 |
+func (mb *configManifestBuilder) References() []distribution.Descriptor {
|
|
| 245 |
+ return mb.descriptors |
|
| 246 |
+} |
|
| 247 |
+ |
|
| 248 |
+// MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON |
|
| 249 |
+func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
|
|
| 250 |
+ // Top-level v1compatibility string should be a modified version of the |
|
| 251 |
+ // image config. |
|
| 252 |
+ var configAsMap map[string]*json.RawMessage |
|
| 253 |
+ if err := json.Unmarshal(configJSON, &configAsMap); err != nil {
|
|
| 254 |
+ return nil, err |
|
| 255 |
+ } |
|
| 256 |
+ |
|
| 257 |
+ // Delete fields that didn't exist in old manifest |
|
| 258 |
+ delete(configAsMap, "rootfs") |
|
| 259 |
+ delete(configAsMap, "history") |
|
| 260 |
+ configAsMap["id"] = rawJSON(v1ID) |
|
| 261 |
+ if parentV1ID != "" {
|
|
| 262 |
+ configAsMap["parent"] = rawJSON(parentV1ID) |
|
| 263 |
+ } |
|
| 264 |
+ if throwaway {
|
|
| 265 |
+ configAsMap["throwaway"] = rawJSON(true) |
|
| 266 |
+ } |
|
| 267 |
+ |
|
| 268 |
+ return json.Marshal(configAsMap) |
|
| 269 |
+} |
|
| 270 |
+ |
|
| 271 |
+func rawJSON(value interface{}) *json.RawMessage {
|
|
| 272 |
+ jsonval, err := json.Marshal(value) |
|
| 273 |
+ if err != nil {
|
|
| 274 |
+ return nil |
|
| 275 |
+ } |
|
| 276 |
+ return (*json.RawMessage)(&jsonval) |
|
| 277 |
+} |
| ... | ... |
@@ -2,20 +2,22 @@ package schema1 |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"encoding/json" |
| 5 |
+ "fmt" |
|
| 5 | 6 |
|
| 7 |
+ "github.com/docker/distribution" |
|
| 6 | 8 |
"github.com/docker/distribution/digest" |
| 7 | 9 |
"github.com/docker/distribution/manifest" |
| 8 | 10 |
"github.com/docker/libtrust" |
| 9 | 11 |
) |
| 10 | 12 |
|
| 11 |
-// TODO(stevvooe): When we rev the manifest format, the contents of this |
|
| 12 |
-// package should be moved to manifest/v1. |
|
| 13 |
- |
|
| 14 | 13 |
const ( |
| 15 |
- // ManifestMediaType specifies the mediaType for the current version. Note |
|
| 16 |
- // that for schema version 1, the the media is optionally |
|
| 17 |
- // "application/json". |
|
| 18 |
- ManifestMediaType = "application/vnd.docker.distribution.manifest.v1+json" |
|
| 14 |
+ // MediaTypeManifest specifies the mediaType for the current version. Note |
|
| 15 |
+ // that for schema version 1, the the media is optionally "application/json". |
|
| 16 |
+ MediaTypeManifest = "application/vnd.docker.distribution.manifest.v1+json" |
|
| 17 |
+ // MediaTypeSignedManifest specifies the mediatype for current SignedManifest version |
|
| 18 |
+ MediaTypeSignedManifest = "application/vnd.docker.distribution.manifest.v1+prettyjws" |
|
| 19 |
+ // MediaTypeManifestLayer specifies the media type for manifest layers |
|
| 20 |
+ MediaTypeManifestLayer = "application/vnd.docker.container.image.rootfs.diff+x-gtar" |
|
| 19 | 21 |
) |
| 20 | 22 |
|
| 21 | 23 |
var ( |
| ... | ... |
@@ -26,6 +28,47 @@ var ( |
| 26 | 26 |
} |
| 27 | 27 |
) |
| 28 | 28 |
|
| 29 |
+func init() {
|
|
| 30 |
+ schema1Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
|
| 31 |
+ sm := new(SignedManifest) |
|
| 32 |
+ err := sm.UnmarshalJSON(b) |
|
| 33 |
+ if err != nil {
|
|
| 34 |
+ return nil, distribution.Descriptor{}, err
|
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ desc := distribution.Descriptor{
|
|
| 38 |
+ Digest: digest.FromBytes(sm.Canonical), |
|
| 39 |
+ Size: int64(len(sm.Canonical)), |
|
| 40 |
+ MediaType: MediaTypeManifest, |
|
| 41 |
+ } |
|
| 42 |
+ return sm, desc, err |
|
| 43 |
+ } |
|
| 44 |
+ err := distribution.RegisterManifestSchema(MediaTypeManifest, schema1Func) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 47 |
+ } |
|
| 48 |
+ err = distribution.RegisterManifestSchema("", schema1Func)
|
|
| 49 |
+ if err != nil {
|
|
| 50 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 51 |
+ } |
|
| 52 |
+ err = distribution.RegisterManifestSchema("application/json; charset=utf-8", schema1Func)
|
|
| 53 |
+ if err != nil {
|
|
| 54 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 55 |
+ } |
|
| 56 |
+} |
|
| 57 |
+ |
|
| 58 |
+// FSLayer is a container struct for BlobSums defined in an image manifest |
|
| 59 |
+type FSLayer struct {
|
|
| 60 |
+ // BlobSum is the tarsum of the referenced filesystem image layer |
|
| 61 |
+ BlobSum digest.Digest `json:"blobSum"` |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// History stores unstructured v1 compatibility information |
|
| 65 |
+type History struct {
|
|
| 66 |
+ // V1Compatibility is the raw v1 compatibility information |
|
| 67 |
+ V1Compatibility string `json:"v1Compatibility"` |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 29 | 70 |
// Manifest provides the base accessible fields for working with V2 image |
| 30 | 71 |
// format in the registry. |
| 31 | 72 |
type Manifest struct {
|
| ... | ... |
@@ -49,59 +92,64 @@ type Manifest struct {
|
| 49 | 49 |
} |
| 50 | 50 |
|
| 51 | 51 |
// SignedManifest provides an envelope for a signed image manifest, including |
| 52 |
-// the format sensitive raw bytes. It contains fields to |
|
| 52 |
+// the format sensitive raw bytes. |
|
| 53 | 53 |
type SignedManifest struct {
|
| 54 | 54 |
Manifest |
| 55 | 55 |
|
| 56 |
- // Raw is the byte representation of the ImageManifest, used for signature |
|
| 57 |
- // verification. The value of Raw must be used directly during |
|
| 58 |
- // serialization, or the signature check will fail. The manifest byte |
|
| 56 |
+ // Canonical is the canonical byte representation of the ImageManifest, |
|
| 57 |
+ // without any attached signatures. The manifest byte |
|
| 59 | 58 |
// representation cannot change or it will have to be re-signed. |
| 60 |
- Raw []byte `json:"-"` |
|
| 59 |
+ Canonical []byte `json:"-"` |
|
| 60 |
+ |
|
| 61 |
+ // all contains the byte representation of the Manifest including signatures |
|
| 62 |
+ // and is retuend by Payload() |
|
| 63 |
+ all []byte |
|
| 61 | 64 |
} |
| 62 | 65 |
|
| 63 |
-// UnmarshalJSON populates a new ImageManifest struct from JSON data. |
|
| 66 |
+// UnmarshalJSON populates a new SignedManifest struct from JSON data. |
|
| 64 | 67 |
func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
|
| 65 |
- sm.Raw = make([]byte, len(b), len(b)) |
|
| 66 |
- copy(sm.Raw, b) |
|
| 68 |
+ sm.all = make([]byte, len(b), len(b)) |
|
| 69 |
+ // store manifest and signatures in all |
|
| 70 |
+ copy(sm.all, b) |
|
| 67 | 71 |
|
| 68 |
- p, err := sm.Payload() |
|
| 72 |
+ jsig, err := libtrust.ParsePrettySignature(b, "signatures") |
|
| 69 | 73 |
if err != nil {
|
| 70 | 74 |
return err |
| 71 | 75 |
} |
| 72 | 76 |
|
| 77 |
+ // Resolve the payload in the manifest. |
|
| 78 |
+ bytes, err := jsig.Payload() |
|
| 79 |
+ if err != nil {
|
|
| 80 |
+ return err |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ // sm.Canonical stores the canonical manifest JSON |
|
| 84 |
+ sm.Canonical = make([]byte, len(bytes), len(bytes)) |
|
| 85 |
+ copy(sm.Canonical, bytes) |
|
| 86 |
+ |
|
| 87 |
+ // Unmarshal canonical JSON into Manifest object |
|
| 73 | 88 |
var manifest Manifest |
| 74 |
- if err := json.Unmarshal(p, &manifest); err != nil {
|
|
| 89 |
+ if err := json.Unmarshal(sm.Canonical, &manifest); err != nil {
|
|
| 75 | 90 |
return err |
| 76 | 91 |
} |
| 77 | 92 |
|
| 78 | 93 |
sm.Manifest = manifest |
| 94 |
+ |
|
| 79 | 95 |
return nil |
| 80 | 96 |
} |
| 81 | 97 |
|
| 82 |
-// Payload returns the raw, signed content of the signed manifest. The |
|
| 83 |
-// contents can be used to calculate the content identifier. |
|
| 84 |
-func (sm *SignedManifest) Payload() ([]byte, error) {
|
|
| 85 |
- jsig, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") |
|
| 86 |
- if err != nil {
|
|
| 87 |
- return nil, err |
|
| 98 |
+// References returnes the descriptors of this manifests references |
|
| 99 |
+func (sm SignedManifest) References() []distribution.Descriptor {
|
|
| 100 |
+ dependencies := make([]distribution.Descriptor, len(sm.FSLayers)) |
|
| 101 |
+ for i, fsLayer := range sm.FSLayers {
|
|
| 102 |
+ dependencies[i] = distribution.Descriptor{
|
|
| 103 |
+ MediaType: "application/vnd.docker.container.image.rootfs.diff+x-gtar", |
|
| 104 |
+ Digest: fsLayer.BlobSum, |
|
| 105 |
+ } |
|
| 88 | 106 |
} |
| 89 | 107 |
|
| 90 |
- // Resolve the payload in the manifest. |
|
| 91 |
- return jsig.Payload() |
|
| 92 |
-} |
|
| 93 |
- |
|
| 94 |
-// Signatures returns the signatures as provided by |
|
| 95 |
-// (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws |
|
| 96 |
-// signatures. |
|
| 97 |
-func (sm *SignedManifest) Signatures() ([][]byte, error) {
|
|
| 98 |
- jsig, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") |
|
| 99 |
- if err != nil {
|
|
| 100 |
- return nil, err |
|
| 101 |
- } |
|
| 108 |
+ return dependencies |
|
| 102 | 109 |
|
| 103 |
- // Resolve the payload in the manifest. |
|
| 104 |
- return jsig.Signatures() |
|
| 105 | 110 |
} |
| 106 | 111 |
|
| 107 | 112 |
// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner |
| ... | ... |
@@ -109,22 +157,28 @@ func (sm *SignedManifest) Signatures() ([][]byte, error) {
|
| 109 | 109 |
// use Raw directly, since the the content produced by json.Marshal will be |
| 110 | 110 |
// compacted and will fail signature checks. |
| 111 | 111 |
func (sm *SignedManifest) MarshalJSON() ([]byte, error) {
|
| 112 |
- if len(sm.Raw) > 0 {
|
|
| 113 |
- return sm.Raw, nil |
|
| 112 |
+ if len(sm.all) > 0 {
|
|
| 113 |
+ return sm.all, nil |
|
| 114 | 114 |
} |
| 115 | 115 |
|
| 116 | 116 |
// If the raw data is not available, just dump the inner content. |
| 117 | 117 |
return json.Marshal(&sm.Manifest) |
| 118 | 118 |
} |
| 119 | 119 |
|
| 120 |
-// FSLayer is a container struct for BlobSums defined in an image manifest |
|
| 121 |
-type FSLayer struct {
|
|
| 122 |
- // BlobSum is the tarsum of the referenced filesystem image layer |
|
| 123 |
- BlobSum digest.Digest `json:"blobSum"` |
|
| 120 |
+// Payload returns the signed content of the signed manifest. |
|
| 121 |
+func (sm SignedManifest) Payload() (string, []byte, error) {
|
|
| 122 |
+ return MediaTypeManifest, sm.all, nil |
|
| 124 | 123 |
} |
| 125 | 124 |
|
| 126 |
-// History stores unstructured v1 compatibility information |
|
| 127 |
-type History struct {
|
|
| 128 |
- // V1Compatibility is the raw v1 compatibility information |
|
| 129 |
- V1Compatibility string `json:"v1Compatibility"` |
|
| 125 |
+// Signatures returns the signatures as provided by |
|
| 126 |
+// (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws |
|
| 127 |
+// signatures. |
|
| 128 |
+func (sm *SignedManifest) Signatures() ([][]byte, error) {
|
|
| 129 |
+ jsig, err := libtrust.ParsePrettySignature(sm.all, "signatures") |
|
| 130 |
+ if err != nil {
|
|
| 131 |
+ return nil, err |
|
| 132 |
+ } |
|
| 133 |
+ |
|
| 134 |
+ // Resolve the payload in the manifest. |
|
| 135 |
+ return jsig.Signatures() |
|
| 130 | 136 |
} |
| 131 | 137 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,92 @@ |
| 0 |
+package schema1 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "errors" |
|
| 6 |
+ "github.com/docker/distribution" |
|
| 7 |
+ "github.com/docker/distribution/context" |
|
| 8 |
+ "github.com/docker/distribution/digest" |
|
| 9 |
+ "github.com/docker/distribution/manifest" |
|
| 10 |
+ "github.com/docker/libtrust" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// referenceManifestBuilder is a type for constructing manifests from schema1 |
|
| 14 |
+// dependencies. |
|
| 15 |
+type referenceManifestBuilder struct {
|
|
| 16 |
+ Manifest |
|
| 17 |
+ pk libtrust.PrivateKey |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+// NewReferenceManifestBuilder is used to build new manifests for the current |
|
| 21 |
+// schema version using schema1 dependencies. |
|
| 22 |
+func NewReferenceManifestBuilder(pk libtrust.PrivateKey, name, tag, architecture string) distribution.ManifestBuilder {
|
|
| 23 |
+ return &referenceManifestBuilder{
|
|
| 24 |
+ Manifest: Manifest{
|
|
| 25 |
+ Versioned: manifest.Versioned{
|
|
| 26 |
+ SchemaVersion: 1, |
|
| 27 |
+ }, |
|
| 28 |
+ Name: name, |
|
| 29 |
+ Tag: tag, |
|
| 30 |
+ Architecture: architecture, |
|
| 31 |
+ }, |
|
| 32 |
+ pk: pk, |
|
| 33 |
+ } |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+func (mb *referenceManifestBuilder) Build(ctx context.Context) (distribution.Manifest, error) {
|
|
| 37 |
+ m := mb.Manifest |
|
| 38 |
+ if len(m.FSLayers) == 0 {
|
|
| 39 |
+ return nil, errors.New("cannot build manifest with zero layers or history")
|
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ m.FSLayers = make([]FSLayer, len(mb.Manifest.FSLayers)) |
|
| 43 |
+ m.History = make([]History, len(mb.Manifest.History)) |
|
| 44 |
+ copy(m.FSLayers, mb.Manifest.FSLayers) |
|
| 45 |
+ copy(m.History, mb.Manifest.History) |
|
| 46 |
+ |
|
| 47 |
+ return Sign(&m, mb.pk) |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// AppendReference adds a reference to the current ManifestBuilder |
|
| 51 |
+func (mb *referenceManifestBuilder) AppendReference(d distribution.Describable) error {
|
|
| 52 |
+ r, ok := d.(Reference) |
|
| 53 |
+ if !ok {
|
|
| 54 |
+ return fmt.Errorf("Unable to add non-reference type to v1 builder")
|
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ // Entries need to be prepended |
|
| 58 |
+ mb.Manifest.FSLayers = append([]FSLayer{{BlobSum: r.Digest}}, mb.Manifest.FSLayers...)
|
|
| 59 |
+ mb.Manifest.History = append([]History{r.History}, mb.Manifest.History...)
|
|
| 60 |
+ return nil |
|
| 61 |
+ |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// References returns the current references added to this builder |
|
| 65 |
+func (mb *referenceManifestBuilder) References() []distribution.Descriptor {
|
|
| 66 |
+ refs := make([]distribution.Descriptor, len(mb.Manifest.FSLayers)) |
|
| 67 |
+ for i := range mb.Manifest.FSLayers {
|
|
| 68 |
+ layerDigest := mb.Manifest.FSLayers[i].BlobSum |
|
| 69 |
+ history := mb.Manifest.History[i] |
|
| 70 |
+ ref := Reference{layerDigest, 0, history}
|
|
| 71 |
+ refs[i] = ref.Descriptor() |
|
| 72 |
+ } |
|
| 73 |
+ return refs |
|
| 74 |
+} |
|
| 75 |
+ |
|
| 76 |
+// Reference describes a manifest v2, schema version 1 dependency. |
|
| 77 |
+// An FSLayer associated with a history entry. |
|
| 78 |
+type Reference struct {
|
|
| 79 |
+ Digest digest.Digest |
|
| 80 |
+ Size int64 // if we know it, set it for the descriptor. |
|
| 81 |
+ History History |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+// Descriptor describes a reference |
|
| 85 |
+func (r Reference) Descriptor() distribution.Descriptor {
|
|
| 86 |
+ return distribution.Descriptor{
|
|
| 87 |
+ MediaType: MediaTypeManifestLayer, |
|
| 88 |
+ Digest: r.Digest, |
|
| 89 |
+ Size: r.Size, |
|
| 90 |
+ } |
|
| 91 |
+} |
| ... | ... |
@@ -31,8 +31,9 @@ func Sign(m *Manifest, pk libtrust.PrivateKey) (*SignedManifest, error) {
|
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 | 33 |
return &SignedManifest{
|
| 34 |
- Manifest: *m, |
|
| 35 |
- Raw: pretty, |
|
| 34 |
+ Manifest: *m, |
|
| 35 |
+ all: pretty, |
|
| 36 |
+ Canonical: p, |
|
| 36 | 37 |
}, nil |
| 37 | 38 |
} |
| 38 | 39 |
|
| ... | ... |
@@ -60,7 +61,8 @@ func SignWithChain(m *Manifest, key libtrust.PrivateKey, chain []*x509.Certifica |
| 60 | 60 |
} |
| 61 | 61 |
|
| 62 | 62 |
return &SignedManifest{
|
| 63 |
- Manifest: *m, |
|
| 64 |
- Raw: pretty, |
|
| 63 |
+ Manifest: *m, |
|
| 64 |
+ all: pretty, |
|
| 65 |
+ Canonical: p, |
|
| 65 | 66 |
}, nil |
| 66 | 67 |
} |
| ... | ... |
@@ -10,7 +10,7 @@ import ( |
| 10 | 10 |
// Verify verifies the signature of the signed manifest returning the public |
| 11 | 11 |
// keys used during signing. |
| 12 | 12 |
func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) {
|
| 13 |
- js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") |
|
| 13 |
+ js, err := libtrust.ParsePrettySignature(sm.all, "signatures") |
|
| 14 | 14 |
if err != nil {
|
| 15 | 15 |
logrus.WithField("err", err).Debugf("(*SignedManifest).Verify")
|
| 16 | 16 |
return nil, err |
| ... | ... |
@@ -23,7 +23,7 @@ func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) {
|
| 23 | 23 |
// certificate pool returning the list of verified chains. Signatures without |
| 24 | 24 |
// an x509 chain are not checked. |
| 25 | 25 |
func VerifyChains(sm *SignedManifest, ca *x509.CertPool) ([][]*x509.Certificate, error) {
|
| 26 |
- js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") |
|
| 26 |
+ js, err := libtrust.ParsePrettySignature(sm.all, "signatures") |
|
| 27 | 27 |
if err != nil {
|
| 28 | 28 |
return nil, err |
| 29 | 29 |
} |
| 30 | 30 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,74 @@ |
| 0 |
+package schema2 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/docker/distribution" |
|
| 4 |
+ "github.com/docker/distribution/context" |
|
| 5 |
+ "github.com/docker/distribution/digest" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// builder is a type for constructing manifests. |
|
| 9 |
+type builder struct {
|
|
| 10 |
+ // bs is a BlobService used to publish the configuration blob. |
|
| 11 |
+ bs distribution.BlobService |
|
| 12 |
+ |
|
| 13 |
+ // configJSON references |
|
| 14 |
+ configJSON []byte |
|
| 15 |
+ |
|
| 16 |
+ // layers is a list of layer descriptors that gets built by successive |
|
| 17 |
+ // calls to AppendReference. |
|
| 18 |
+ layers []distribution.Descriptor |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+// NewManifestBuilder is used to build new manifests for the current schema |
|
| 22 |
+// version. It takes a BlobService so it can publish the configuration blob |
|
| 23 |
+// as part of the Build process. |
|
| 24 |
+func NewManifestBuilder(bs distribution.BlobService, configJSON []byte) distribution.ManifestBuilder {
|
|
| 25 |
+ mb := &builder{
|
|
| 26 |
+ bs: bs, |
|
| 27 |
+ configJSON: make([]byte, len(configJSON)), |
|
| 28 |
+ } |
|
| 29 |
+ copy(mb.configJSON, configJSON) |
|
| 30 |
+ |
|
| 31 |
+ return mb |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// Build produces a final manifest from the given references. |
|
| 35 |
+func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
|
|
| 36 |
+ m := Manifest{
|
|
| 37 |
+ Versioned: SchemaVersion, |
|
| 38 |
+ Layers: make([]distribution.Descriptor, len(mb.layers)), |
|
| 39 |
+ } |
|
| 40 |
+ copy(m.Layers, mb.layers) |
|
| 41 |
+ |
|
| 42 |
+ configDigest := digest.FromBytes(mb.configJSON) |
|
| 43 |
+ |
|
| 44 |
+ var err error |
|
| 45 |
+ m.Config, err = mb.bs.Stat(ctx, configDigest) |
|
| 46 |
+ switch err {
|
|
| 47 |
+ case nil: |
|
| 48 |
+ return FromStruct(m) |
|
| 49 |
+ case distribution.ErrBlobUnknown: |
|
| 50 |
+ // nop |
|
| 51 |
+ default: |
|
| 52 |
+ return nil, err |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ // Add config to the blob store |
|
| 56 |
+ m.Config, err = mb.bs.Put(ctx, MediaTypeConfig, mb.configJSON) |
|
| 57 |
+ if err != nil {
|
|
| 58 |
+ return nil, err |
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ return FromStruct(m) |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// AppendReference adds a reference to the current ManifestBuilder. |
|
| 65 |
+func (mb *builder) AppendReference(d distribution.Describable) error {
|
|
| 66 |
+ mb.layers = append(mb.layers, d.Descriptor()) |
|
| 67 |
+ return nil |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+// References returns the current references added to this builder. |
|
| 71 |
+func (mb *builder) References() []distribution.Descriptor {
|
|
| 72 |
+ return mb.layers |
|
| 73 |
+} |
| 0 | 74 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,125 @@ |
| 0 |
+package schema2 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/distribution" |
|
| 8 |
+ "github.com/docker/distribution/digest" |
|
| 9 |
+ "github.com/docker/distribution/manifest" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+const ( |
|
| 13 |
+ // MediaTypeManifest specifies the mediaType for the current version. |
|
| 14 |
+ MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" |
|
| 15 |
+ |
|
| 16 |
+ // MediaTypeConfig specifies the mediaType for the image configuration. |
|
| 17 |
+ MediaTypeConfig = "application/vnd.docker.container.image.v1+json" |
|
| 18 |
+ |
|
| 19 |
+ // MediaTypeLayer is the mediaType used for layers referenced by the |
|
| 20 |
+ // manifest. |
|
| 21 |
+ MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip" |
|
| 22 |
+) |
|
| 23 |
+ |
|
| 24 |
+var ( |
|
| 25 |
+ // SchemaVersion provides a pre-initialized version structure for this |
|
| 26 |
+ // packages version of the manifest. |
|
| 27 |
+ SchemaVersion = manifest.Versioned{
|
|
| 28 |
+ SchemaVersion: 2, |
|
| 29 |
+ MediaType: MediaTypeManifest, |
|
| 30 |
+ } |
|
| 31 |
+) |
|
| 32 |
+ |
|
| 33 |
+func init() {
|
|
| 34 |
+ schema2Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
|
| 35 |
+ m := new(DeserializedManifest) |
|
| 36 |
+ err := m.UnmarshalJSON(b) |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ return nil, distribution.Descriptor{}, err
|
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ dgst := digest.FromBytes(b) |
|
| 42 |
+ return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifest}, err
|
|
| 43 |
+ } |
|
| 44 |
+ err := distribution.RegisterManifestSchema(MediaTypeManifest, schema2Func) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
| 47 |
+ } |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// Manifest defines a schema2 manifest. |
|
| 51 |
+type Manifest struct {
|
|
| 52 |
+ manifest.Versioned |
|
| 53 |
+ |
|
| 54 |
+ // Config references the image configuration as a blob. |
|
| 55 |
+ Config distribution.Descriptor `json:"config"` |
|
| 56 |
+ |
|
| 57 |
+ // Layers lists descriptors for the layers referenced by the |
|
| 58 |
+ // configuration. |
|
| 59 |
+ Layers []distribution.Descriptor `json:"layers"` |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// References returnes the descriptors of this manifests references. |
|
| 63 |
+func (m Manifest) References() []distribution.Descriptor {
|
|
| 64 |
+ return m.Layers |
|
| 65 |
+ |
|
| 66 |
+} |
|
| 67 |
+ |
|
| 68 |
+// Target returns the target of this signed manifest. |
|
| 69 |
+func (m Manifest) Target() distribution.Descriptor {
|
|
| 70 |
+ return m.Config |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+// DeserializedManifest wraps Manifest with a copy of the original JSON. |
|
| 74 |
+// It satisfies the distribution.Manifest interface. |
|
| 75 |
+type DeserializedManifest struct {
|
|
| 76 |
+ Manifest |
|
| 77 |
+ |
|
| 78 |
+ // canonical is the canonical byte representation of the Manifest. |
|
| 79 |
+ canonical []byte |
|
| 80 |
+} |
|
| 81 |
+ |
|
| 82 |
+// FromStruct takes a Manifest structure, marshals it to JSON, and returns a |
|
| 83 |
+// DeserializedManifest which contains the manifest and its JSON representation. |
|
| 84 |
+func FromStruct(m Manifest) (*DeserializedManifest, error) {
|
|
| 85 |
+ var deserialized DeserializedManifest |
|
| 86 |
+ deserialized.Manifest = m |
|
| 87 |
+ |
|
| 88 |
+ var err error |
|
| 89 |
+ deserialized.canonical, err = json.MarshalIndent(&m, "", " ") |
|
| 90 |
+ return &deserialized, err |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+// UnmarshalJSON populates a new Manifest struct from JSON data. |
|
| 94 |
+func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
|
|
| 95 |
+ m.canonical = make([]byte, len(b), len(b)) |
|
| 96 |
+ // store manifest in canonical |
|
| 97 |
+ copy(m.canonical, b) |
|
| 98 |
+ |
|
| 99 |
+ // Unmarshal canonical JSON into Manifest object |
|
| 100 |
+ var manifest Manifest |
|
| 101 |
+ if err := json.Unmarshal(m.canonical, &manifest); err != nil {
|
|
| 102 |
+ return err |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ m.Manifest = manifest |
|
| 106 |
+ |
|
| 107 |
+ return nil |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 110 |
+// MarshalJSON returns the contents of canonical. If canonical is empty, |
|
| 111 |
+// marshals the inner contents. |
|
| 112 |
+func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
|
|
| 113 |
+ if len(m.canonical) > 0 {
|
|
| 114 |
+ return m.canonical, nil |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ return nil, errors.New("JSON representation not initialized in DeserializedManifest")
|
|
| 118 |
+} |
|
| 119 |
+ |
|
| 120 |
+// Payload returns the raw content of the manifest. The contents can be used to |
|
| 121 |
+// calculate the content identifier. |
|
| 122 |
+func (m DeserializedManifest) Payload() (string, []byte, error) {
|
|
| 123 |
+ return m.MediaType, m.canonical, nil |
|
| 124 |
+} |
| ... | ... |
@@ -1,9 +1,12 @@ |
| 1 | 1 |
package manifest |
| 2 | 2 |
|
| 3 |
-// Versioned provides a struct with just the manifest schemaVersion. Incoming |
|
| 3 |
+// Versioned provides a struct with the manifest schemaVersion and . Incoming |
|
| 4 | 4 |
// content with unknown schema version can be decoded against this struct to |
| 5 | 5 |
// check the version. |
| 6 | 6 |
type Versioned struct {
|
| 7 | 7 |
// SchemaVersion is the image manifest schema that this image follows |
| 8 | 8 |
SchemaVersion int `json:"schemaVersion"` |
| 9 |
+ |
|
| 10 |
+ // MediaType is the media type of this schema. |
|
| 11 |
+ MediaType string `json:"mediaType,omitempty"` |
|
| 9 | 12 |
} |
| 10 | 13 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,100 @@ |
| 0 |
+package distribution |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/distribution/context" |
|
| 6 |
+ "github.com/docker/distribution/digest" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// Manifest represents a registry object specifying a set of |
|
| 10 |
+// references and an optional target |
|
| 11 |
+type Manifest interface {
|
|
| 12 |
+ // References returns a list of objects which make up this manifest. |
|
| 13 |
+ // The references are strictly ordered from base to head. A reference |
|
| 14 |
+ // is anything which can be represented by a distribution.Descriptor |
|
| 15 |
+ References() []Descriptor |
|
| 16 |
+ |
|
| 17 |
+ // Payload provides the serialized format of the manifest, in addition to |
|
| 18 |
+ // the mediatype. |
|
| 19 |
+ Payload() (mediatype string, payload []byte, err error) |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+// ManifestBuilder creates a manifest allowing one to include dependencies. |
|
| 23 |
+// Instances can be obtained from a version-specific manifest package. Manifest |
|
| 24 |
+// specific data is passed into the function which creates the builder. |
|
| 25 |
+type ManifestBuilder interface {
|
|
| 26 |
+ // Build creates the manifest from his builder. |
|
| 27 |
+ Build(ctx context.Context) (Manifest, error) |
|
| 28 |
+ |
|
| 29 |
+ // References returns a list of objects which have been added to this |
|
| 30 |
+ // builder. The dependencies are returned in the order they were added, |
|
| 31 |
+ // which should be from base to head. |
|
| 32 |
+ References() []Descriptor |
|
| 33 |
+ |
|
| 34 |
+ // AppendReference includes the given object in the manifest after any |
|
| 35 |
+ // existing dependencies. If the add fails, such as when adding an |
|
| 36 |
+ // unsupported dependency, an error may be returned. |
|
| 37 |
+ AppendReference(dependency Describable) error |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// ManifestService describes operations on image manifests. |
|
| 41 |
+type ManifestService interface {
|
|
| 42 |
+ // Exists returns true if the manifest exists. |
|
| 43 |
+ Exists(ctx context.Context, dgst digest.Digest) (bool, error) |
|
| 44 |
+ |
|
| 45 |
+ // Get retrieves the manifest specified by the given digest |
|
| 46 |
+ Get(ctx context.Context, dgst digest.Digest, options ...ManifestServiceOption) (Manifest, error) |
|
| 47 |
+ |
|
| 48 |
+ // Put creates or updates the given manifest returning the manifest digest |
|
| 49 |
+ Put(ctx context.Context, manifest Manifest, options ...ManifestServiceOption) (digest.Digest, error) |
|
| 50 |
+ |
|
| 51 |
+ // Delete removes the manifest specified by the given digest. Deleting |
|
| 52 |
+ // a manifest that doesn't exist will return ErrManifestNotFound |
|
| 53 |
+ Delete(ctx context.Context, dgst digest.Digest) error |
|
| 54 |
+ |
|
| 55 |
+ // Enumerate fills 'manifests' with the manifests in this service up |
|
| 56 |
+ // to the size of 'manifests' and returns 'n' for the number of entries |
|
| 57 |
+ // which were filled. 'last' contains an offset in the manifest set |
|
| 58 |
+ // and can be used to resume iteration. |
|
| 59 |
+ //Enumerate(ctx context.Context, manifests []Manifest, last Manifest) (n int, err error) |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// Describable is an interface for descriptors |
|
| 63 |
+type Describable interface {
|
|
| 64 |
+ Descriptor() Descriptor |
|
| 65 |
+} |
|
| 66 |
+ |
|
| 67 |
+// ManifestMediaTypes returns the supported media types for manifests. |
|
| 68 |
+func ManifestMediaTypes() (mediaTypes []string) {
|
|
| 69 |
+ for t := range mappings {
|
|
| 70 |
+ mediaTypes = append(mediaTypes, t) |
|
| 71 |
+ } |
|
| 72 |
+ return |
|
| 73 |
+} |
|
| 74 |
+ |
|
| 75 |
+// UnmarshalFunc implements manifest unmarshalling a given MediaType |
|
| 76 |
+type UnmarshalFunc func([]byte) (Manifest, Descriptor, error) |
|
| 77 |
+ |
|
| 78 |
+var mappings = make(map[string]UnmarshalFunc, 0) |
|
| 79 |
+ |
|
| 80 |
+// UnmarshalManifest looks up manifest unmarshall functions based on |
|
| 81 |
+// MediaType |
|
| 82 |
+func UnmarshalManifest(mediatype string, p []byte) (Manifest, Descriptor, error) {
|
|
| 83 |
+ unmarshalFunc, ok := mappings[mediatype] |
|
| 84 |
+ if !ok {
|
|
| 85 |
+ return nil, Descriptor{}, fmt.Errorf("unsupported manifest mediatype: %s", mediatype)
|
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ return unmarshalFunc(p) |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+// RegisterManifestSchema registers an UnmarshalFunc for a given schema type. This |
|
| 92 |
+// should be called from specific |
|
| 93 |
+func RegisterManifestSchema(mediatype string, u UnmarshalFunc) error {
|
|
| 94 |
+ if _, ok := mappings[mediatype]; ok {
|
|
| 95 |
+ return fmt.Errorf("manifest mediatype registration would overwrite existing: %s", mediatype)
|
|
| 96 |
+ } |
|
| 97 |
+ mappings[mediatype] = u |
|
| 98 |
+ return nil |
|
| 99 |
+} |
| ... | ... |
@@ -4,22 +4,16 @@ |
| 4 | 4 |
// Grammar |
| 5 | 5 |
// |
| 6 | 6 |
// reference := repository [ ":" tag ] [ "@" digest ] |
| 7 |
+// name := [hostname '/'] component ['/' component]* |
|
| 8 |
+// hostname := hostcomponent ['.' hostcomponent]* [':' port-number] |
|
| 9 |
+// hostcomponent := /([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])/ |
|
| 10 |
+// port-number := /[0-9]+/ |
|
| 11 |
+// component := alpha-numeric [separator alpha-numeric]* |
|
| 12 |
+// alpha-numeric := /[a-z0-9]+/ |
|
| 13 |
+// separator := /[_.]|__|[-]*/ |
|
| 7 | 14 |
// |
| 8 |
-// // repository.go |
|
| 9 |
-// repository := hostname ['/' component]+ |
|
| 10 |
-// hostname := hostcomponent [':' port-number] |
|
| 11 |
-// component := subcomponent [separator subcomponent]* |
|
| 12 |
-// subcomponent := alpha-numeric ['-'* alpha-numeric]* |
|
| 13 |
-// hostcomponent := [hostpart '.']* hostpart |
|
| 14 |
-// alpha-numeric := /[a-z0-9]+/ |
|
| 15 |
-// separator := /([_.]|__)/ |
|
| 16 |
-// port-number := /[0-9]+/ |
|
| 17 |
-// hostpart := /([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])/ |
|
| 18 |
-// |
|
| 19 |
-// // tag.go |
|
| 20 | 15 |
// tag := /[\w][\w.-]{0,127}/
|
| 21 | 16 |
// |
| 22 |
-// // from the digest package |
|
| 23 | 17 |
// digest := digest-algorithm ":" digest-hex |
| 24 | 18 |
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ] |
| 25 | 19 |
// digest-algorithm-separator := /[+.-_]/ |
| ... | ... |
@@ -52,8 +46,7 @@ var ( |
| 52 | 52 |
// ErrNameEmpty is returned for empty, invalid repository names. |
| 53 | 53 |
ErrNameEmpty = errors.New("repository name must have at least one component")
|
| 54 | 54 |
|
| 55 |
- // ErrNameTooLong is returned when a repository name is longer than |
|
| 56 |
- // RepositoryNameTotalLengthMax |
|
| 55 |
+ // ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax. |
|
| 57 | 56 |
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
|
| 58 | 57 |
) |
| 59 | 58 |
|
| ... | ... |
@@ -3,47 +3,122 @@ package reference |
| 3 | 3 |
import "regexp" |
| 4 | 4 |
|
| 5 | 5 |
var ( |
| 6 |
- // nameSubComponentRegexp defines the part of the name which must be |
|
| 7 |
- // begin and end with an alphanumeric character. These characters can |
|
| 8 |
- // be separated by any number of dashes. |
|
| 9 |
- nameSubComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[-]+[a-z0-9]+)*`) |
|
| 6 |
+ // alphaNumericRegexp defines the alpha numeric atom, typically a |
|
| 7 |
+ // component of names. This only allows lower case characters and digits. |
|
| 8 |
+ alphaNumericRegexp = match(`[a-z0-9]+`) |
|
| 10 | 9 |
|
| 11 |
- // nameComponentRegexp restricts registry path component names to |
|
| 12 |
- // start with at least one letter or number, with following parts able to |
|
| 13 |
- // be separated by one period, underscore or double underscore. |
|
| 14 |
- nameComponentRegexp = regexp.MustCompile(nameSubComponentRegexp.String() + `(?:(?:[._]|__)` + nameSubComponentRegexp.String() + `)*`) |
|
| 10 |
+ // separatorRegexp defines the separators allowed to be embedded in name |
|
| 11 |
+ // components. This allow one period, one or two underscore and multiple |
|
| 12 |
+ // dashes. |
|
| 13 |
+ separatorRegexp = match(`(?:[._]|__|[-]*)`) |
|
| 15 | 14 |
|
| 16 |
- nameRegexp = regexp.MustCompile(`(?:` + nameComponentRegexp.String() + `/)*` + nameComponentRegexp.String()) |
|
| 15 |
+ // nameComponentRegexp restricts registry path component names to start |
|
| 16 |
+ // with at least one letter or number, with following parts able to be |
|
| 17 |
+ // separated by one period, one or two underscore and multiple dashes. |
|
| 18 |
+ nameComponentRegexp = expression( |
|
| 19 |
+ alphaNumericRegexp, |
|
| 20 |
+ optional(repeated(separatorRegexp, alphaNumericRegexp))) |
|
| 17 | 21 |
|
| 18 |
- hostnameComponentRegexp = regexp.MustCompile(`(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])`) |
|
| 22 |
+ // hostnameComponentRegexp restricts the registry hostname component of a |
|
| 23 |
+ // repository name to start with a component as defined by hostnameRegexp |
|
| 24 |
+ // and followed by an optional port. |
|
| 25 |
+ hostnameComponentRegexp = match(`(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])`) |
|
| 19 | 26 |
|
| 20 |
- // hostnameComponentRegexp restricts the registry hostname component of a repository name to |
|
| 21 |
- // start with a component as defined by hostnameRegexp and followed by an optional port. |
|
| 22 |
- hostnameRegexp = regexp.MustCompile(`(?:` + hostnameComponentRegexp.String() + `\.)*` + hostnameComponentRegexp.String() + `(?::[0-9]+)?`) |
|
| 27 |
+ // hostnameRegexp defines the structure of potential hostname components |
|
| 28 |
+ // that may be part of image names. This is purposely a subset of what is |
|
| 29 |
+ // allowed by DNS to ensure backwards compatibility with Docker image |
|
| 30 |
+ // names. |
|
| 31 |
+ hostnameRegexp = expression( |
|
| 32 |
+ hostnameComponentRegexp, |
|
| 33 |
+ optional(repeated(literal(`.`), hostnameComponentRegexp)), |
|
| 34 |
+ optional(literal(`:`), match(`[0-9]+`))) |
|
| 23 | 35 |
|
| 24 | 36 |
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go. |
| 25 |
- TagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
|
|
| 37 |
+ TagRegexp = match(`[\w][\w.-]{0,127}`)
|
|
| 26 | 38 |
|
| 27 | 39 |
// anchoredTagRegexp matches valid tag names, anchored at the start and |
| 28 | 40 |
// end of the matched string. |
| 29 |
- anchoredTagRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`) |
|
| 41 |
+ anchoredTagRegexp = anchored(TagRegexp) |
|
| 30 | 42 |
|
| 31 | 43 |
// DigestRegexp matches valid digests. |
| 32 |
- DigestRegexp = regexp.MustCompile(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
|
|
| 44 |
+ DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
|
|
| 33 | 45 |
|
| 34 | 46 |
// anchoredDigestRegexp matches valid digests, anchored at the start and |
| 35 | 47 |
// end of the matched string. |
| 36 |
- anchoredDigestRegexp = regexp.MustCompile(`^` + DigestRegexp.String() + `$`) |
|
| 48 |
+ anchoredDigestRegexp = anchored(DigestRegexp) |
|
| 37 | 49 |
|
| 38 | 50 |
// NameRegexp is the format for the name component of references. The |
| 39 | 51 |
// regexp has capturing groups for the hostname and name part omitting |
| 40 | 52 |
// the seperating forward slash from either. |
| 41 |
- NameRegexp = regexp.MustCompile(`(?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String()) |
|
| 53 |
+ NameRegexp = expression( |
|
| 54 |
+ optional(hostnameRegexp, literal(`/`)), |
|
| 55 |
+ nameComponentRegexp, |
|
| 56 |
+ optional(repeated(literal(`/`), nameComponentRegexp))) |
|
| 42 | 57 |
|
| 43 |
- // ReferenceRegexp is the full supported format of a reference. The |
|
| 44 |
- // regexp has capturing groups for name, tag, and digest components. |
|
| 45 |
- ReferenceRegexp = regexp.MustCompile(`^((?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String() + `)(?:[:](` + TagRegexp.String() + `))?(?:[@](` + DigestRegexp.String() + `))?$`) |
|
| 58 |
+ // anchoredNameRegexp is used to parse a name value, capturing the |
|
| 59 |
+ // hostname and trailing components. |
|
| 60 |
+ anchoredNameRegexp = anchored( |
|
| 61 |
+ optional(capture(hostnameRegexp), literal(`/`)), |
|
| 62 |
+ capture(nameComponentRegexp, |
|
| 63 |
+ optional(repeated(literal(`/`), nameComponentRegexp)))) |
|
| 46 | 64 |
|
| 47 |
- // anchoredNameRegexp is used to parse a name value, capturing hostname |
|
| 48 |
- anchoredNameRegexp = regexp.MustCompile(`^(?:(` + hostnameRegexp.String() + `)/)?(` + nameRegexp.String() + `)$`) |
|
| 65 |
+ // ReferenceRegexp is the full supported format of a reference. The regexp |
|
| 66 |
+ // is anchored and has capturing groups for name, tag, and digest |
|
| 67 |
+ // components. |
|
| 68 |
+ ReferenceRegexp = anchored(capture(NameRegexp), |
|
| 69 |
+ optional(literal(":"), capture(TagRegexp)),
|
|
| 70 |
+ optional(literal("@"), capture(DigestRegexp)))
|
|
| 49 | 71 |
) |
| 72 |
+ |
|
| 73 |
+// match compiles the string to a regular expression. |
|
| 74 |
+var match = regexp.MustCompile |
|
| 75 |
+ |
|
| 76 |
+// literal compiles s into a literal regular expression, escaping any regexp |
|
| 77 |
+// reserved characters. |
|
| 78 |
+func literal(s string) *regexp.Regexp {
|
|
| 79 |
+ re := match(regexp.QuoteMeta(s)) |
|
| 80 |
+ |
|
| 81 |
+ if _, complete := re.LiteralPrefix(); !complete {
|
|
| 82 |
+ panic("must be a literal")
|
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ return re |
|
| 86 |
+} |
|
| 87 |
+ |
|
| 88 |
+// expression defines a full expression, where each regular expression must |
|
| 89 |
+// follow the previous. |
|
| 90 |
+func expression(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 91 |
+ var s string |
|
| 92 |
+ for _, re := range res {
|
|
| 93 |
+ s += re.String() |
|
| 94 |
+ } |
|
| 95 |
+ |
|
| 96 |
+ return match(s) |
|
| 97 |
+} |
|
| 98 |
+ |
|
| 99 |
+// optional wraps the expression in a non-capturing group and makes the |
|
| 100 |
+// production optional. |
|
| 101 |
+func optional(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 102 |
+ return match(group(expression(res...)).String() + `?`) |
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+// repeated wraps the regexp in a non-capturing group to get one or more |
|
| 106 |
+// matches. |
|
| 107 |
+func repeated(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 108 |
+ return match(group(expression(res...)).String() + `+`) |
|
| 109 |
+} |
|
| 110 |
+ |
|
| 111 |
+// group wraps the regexp in a non-capturing group. |
|
| 112 |
+func group(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 113 |
+ return match(`(?:` + expression(res...).String() + `)`) |
|
| 114 |
+} |
|
| 115 |
+ |
|
| 116 |
+// capture wraps the expression in a capturing group. |
|
| 117 |
+func capture(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 118 |
+ return match(`(` + expression(res...).String() + `)`) |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+// anchored anchors the regular expression by adding start and end delimiters. |
|
| 122 |
+func anchored(res ...*regexp.Regexp) *regexp.Regexp {
|
|
| 123 |
+ return match(`^` + expression(res...).String() + `$`) |
|
| 124 |
+} |
| ... | ... |
@@ -2,8 +2,6 @@ package distribution |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"github.com/docker/distribution/context" |
| 5 |
- "github.com/docker/distribution/digest" |
|
| 6 |
- "github.com/docker/distribution/manifest/schema1" |
|
| 7 | 5 |
) |
| 8 | 6 |
|
| 9 | 7 |
// Scope defines the set of items that match a namespace. |
| ... | ... |
@@ -44,7 +42,9 @@ type Namespace interface {
|
| 44 | 44 |
} |
| 45 | 45 |
|
| 46 | 46 |
// ManifestServiceOption is a function argument for Manifest Service methods |
| 47 |
-type ManifestServiceOption func(ManifestService) error |
|
| 47 |
+type ManifestServiceOption interface {
|
|
| 48 |
+ Apply(ManifestService) error |
|
| 49 |
+} |
|
| 48 | 50 |
|
| 49 | 51 |
// Repository is a named collection of manifests and layers. |
| 50 | 52 |
type Repository interface {
|
| ... | ... |
@@ -62,59 +62,10 @@ type Repository interface {
|
| 62 | 62 |
// be a BlobService for use with clients. This will allow such |
| 63 | 63 |
// implementations to avoid implementing ServeBlob. |
| 64 | 64 |
|
| 65 |
- // Signatures returns a reference to this repository's signatures service. |
|
| 66 |
- Signatures() SignatureService |
|
| 65 |
+ // Tags returns a reference to this repositories tag service |
|
| 66 |
+ Tags(ctx context.Context) TagService |
|
| 67 | 67 |
} |
| 68 | 68 |
|
| 69 | 69 |
// TODO(stevvooe): Must add close methods to all these. May want to change the |
| 70 | 70 |
// way instances are created to better reflect internal dependency |
| 71 | 71 |
// relationships. |
| 72 |
- |
|
| 73 |
-// ManifestService provides operations on image manifests. |
|
| 74 |
-type ManifestService interface {
|
|
| 75 |
- // Exists returns true if the manifest exists. |
|
| 76 |
- Exists(dgst digest.Digest) (bool, error) |
|
| 77 |
- |
|
| 78 |
- // Get retrieves the identified by the digest, if it exists. |
|
| 79 |
- Get(dgst digest.Digest) (*schema1.SignedManifest, error) |
|
| 80 |
- |
|
| 81 |
- // Delete removes the manifest, if it exists. |
|
| 82 |
- Delete(dgst digest.Digest) error |
|
| 83 |
- |
|
| 84 |
- // Put creates or updates the manifest. |
|
| 85 |
- Put(manifest *schema1.SignedManifest) error |
|
| 86 |
- |
|
| 87 |
- // TODO(stevvooe): The methods after this message should be moved to a |
|
| 88 |
- // discrete TagService, per active proposals. |
|
| 89 |
- |
|
| 90 |
- // Tags lists the tags under the named repository. |
|
| 91 |
- Tags() ([]string, error) |
|
| 92 |
- |
|
| 93 |
- // ExistsByTag returns true if the manifest exists. |
|
| 94 |
- ExistsByTag(tag string) (bool, error) |
|
| 95 |
- |
|
| 96 |
- // GetByTag retrieves the named manifest, if it exists. |
|
| 97 |
- GetByTag(tag string, options ...ManifestServiceOption) (*schema1.SignedManifest, error) |
|
| 98 |
- |
|
| 99 |
- // TODO(stevvooe): There are several changes that need to be done to this |
|
| 100 |
- // interface: |
|
| 101 |
- // |
|
| 102 |
- // 1. Allow explicit tagging with Tag(digest digest.Digest, tag string) |
|
| 103 |
- // 2. Support reading tags with a re-entrant reader to avoid large |
|
| 104 |
- // allocations in the registry. |
|
| 105 |
- // 3. Long-term: Provide All() method that lets one scroll through all of |
|
| 106 |
- // the manifest entries. |
|
| 107 |
- // 4. Long-term: break out concept of signing from manifests. This is |
|
| 108 |
- // really a part of the distribution sprint. |
|
| 109 |
- // 5. Long-term: Manifest should be an interface. This code shouldn't |
|
| 110 |
- // really be concerned with the storage format. |
|
| 111 |
-} |
|
| 112 |
- |
|
| 113 |
-// SignatureService provides operations on signatures. |
|
| 114 |
-type SignatureService interface {
|
|
| 115 |
- // Get retrieves all of the signature blobs for the specified digest. |
|
| 116 |
- Get(dgst digest.Digest) ([][]byte, error) |
|
| 117 |
- |
|
| 118 |
- // Put stores the signature for the provided digest. |
|
| 119 |
- Put(dgst digest.Digest, signatures ...[]byte) error |
|
| 120 |
-} |
| ... | ... |
@@ -25,7 +25,8 @@ func (ec ErrorCode) ErrorCode() ErrorCode {
|
| 25 | 25 |
|
| 26 | 26 |
// Error returns the ID/Value |
| 27 | 27 |
func (ec ErrorCode) Error() string {
|
| 28 |
- return ec.Descriptor().Value |
|
| 28 |
+ // NOTE(stevvooe): Cannot use message here since it may have unpopulated args. |
|
| 29 |
+ return strings.ToLower(strings.Replace(ec.String(), "_", " ", -1)) |
|
| 29 | 30 |
} |
| 30 | 31 |
|
| 31 | 32 |
// Descriptor returns the descriptor for the error code. |
| ... | ... |
@@ -104,9 +105,7 @@ func (e Error) ErrorCode() ErrorCode {
|
| 104 | 104 |
|
| 105 | 105 |
// Error returns a human readable representation of the error. |
| 106 | 106 |
func (e Error) Error() string {
|
| 107 |
- return fmt.Sprintf("%s: %s",
|
|
| 108 |
- strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)), |
|
| 109 |
- e.Message) |
|
| 107 |
+ return fmt.Sprintf("%s: %s", e.Code.Error(), e.Message)
|
|
| 110 | 108 |
} |
| 111 | 109 |
|
| 112 | 110 |
// WithDetail will return a new Error, based on the current one, but with |
| ... | ... |
@@ -495,7 +495,7 @@ var routeDescriptors = []RouteDescriptor{
|
| 495 | 495 |
Methods: []MethodDescriptor{
|
| 496 | 496 |
{
|
| 497 | 497 |
Method: "GET", |
| 498 |
- Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest.", |
|
| 498 |
+ Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data.", |
|
| 499 | 499 |
Requests: []RequestDescriptor{
|
| 500 | 500 |
{
|
| 501 | 501 |
Headers: []ParameterDescriptor{
|
| ... | ... |
@@ -204,7 +204,9 @@ func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) {
|
| 204 | 204 |
routeURL.Path = routeURL.Path[1:] |
| 205 | 205 |
} |
| 206 | 206 |
|
| 207 |
- return cr.root.ResolveReference(routeURL), nil |
|
| 207 |
+ url := cr.root.ResolveReference(routeURL) |
|
| 208 |
+ url.Scheme = cr.root.Scheme |
|
| 209 |
+ return url, nil |
|
| 208 | 210 |
} |
| 209 | 211 |
|
| 210 | 212 |
// appendValuesURL appends the parameters to the url. |
| ... | ... |
@@ -240,7 +240,8 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon |
| 240 | 240 |
defer resp.Body.Close() |
| 241 | 241 |
|
| 242 | 242 |
if !client.SuccessStatus(resp.StatusCode) {
|
| 243 |
- return nil, fmt.Errorf("token auth attempt for registry: %s request failed with status: %d %s", req.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
| 243 |
+ err := client.HandleErrorResponse(resp) |
|
| 244 |
+ return nil, err |
|
| 244 | 245 |
} |
| 245 | 246 |
|
| 246 | 247 |
decoder := json.NewDecoder(resp.Body) |
| ... | ... |
@@ -33,7 +33,7 @@ func (hbu *httpBlobUpload) handleErrorResponse(resp *http.Response) error {
|
| 33 | 33 |
if resp.StatusCode == http.StatusNotFound {
|
| 34 | 34 |
return distribution.ErrBlobUploadUnknown |
| 35 | 35 |
} |
| 36 |
- return handleErrorResponse(resp) |
|
| 36 |
+ return HandleErrorResponse(resp) |
|
| 37 | 37 |
} |
| 38 | 38 |
|
| 39 | 39 |
func (hbu *httpBlobUpload) ReadFrom(r io.Reader) (n int64, err error) {
|
| ... | ... |
@@ -47,7 +47,11 @@ func parseHTTPErrorResponse(r io.Reader) error {
|
| 47 | 47 |
return errors |
| 48 | 48 |
} |
| 49 | 49 |
|
| 50 |
-func handleErrorResponse(resp *http.Response) error {
|
|
| 50 |
+// HandleErrorResponse returns error parsed from HTTP response for an |
|
| 51 |
+// unsuccessful HTTP response code (in the range 400 - 499 inclusive). An |
|
| 52 |
+// UnexpectedHTTPStatusError returned for response code outside of expected |
|
| 53 |
+// range. |
|
| 54 |
+func HandleErrorResponse(resp *http.Response) error {
|
|
| 51 | 55 |
if resp.StatusCode == 401 {
|
| 52 | 56 |
err := parseHTTPErrorResponse(resp.Body) |
| 53 | 57 |
if uErr, ok := err.(*UnexpectedHTTPResponseError); ok {
|
| ... | ... |
@@ -3,6 +3,7 @@ package client |
| 3 | 3 |
import ( |
| 4 | 4 |
"bytes" |
| 5 | 5 |
"encoding/json" |
| 6 |
+ "errors" |
|
| 6 | 7 |
"fmt" |
| 7 | 8 |
"io" |
| 8 | 9 |
"io/ioutil" |
| ... | ... |
@@ -14,7 +15,6 @@ import ( |
| 14 | 14 |
"github.com/docker/distribution" |
| 15 | 15 |
"github.com/docker/distribution/context" |
| 16 | 16 |
"github.com/docker/distribution/digest" |
| 17 |
- "github.com/docker/distribution/manifest/schema1" |
|
| 18 | 17 |
"github.com/docker/distribution/reference" |
| 19 | 18 |
"github.com/docker/distribution/registry/api/v2" |
| 20 | 19 |
"github.com/docker/distribution/registry/client/transport" |
| ... | ... |
@@ -91,7 +91,7 @@ func (r *registry) Repositories(ctx context.Context, entries []string, last stri |
| 91 | 91 |
returnErr = io.EOF |
| 92 | 92 |
} |
| 93 | 93 |
} else {
|
| 94 |
- return 0, handleErrorResponse(resp) |
|
| 94 |
+ return 0, HandleErrorResponse(resp) |
|
| 95 | 95 |
} |
| 96 | 96 |
|
| 97 | 97 |
return numFilled, returnErr |
| ... | ... |
@@ -156,74 +156,151 @@ func (r *repository) Manifests(ctx context.Context, options ...distribution.Mani |
| 156 | 156 |
}, nil |
| 157 | 157 |
} |
| 158 | 158 |
|
| 159 |
-func (r *repository) Signatures() distribution.SignatureService {
|
|
| 160 |
- ms, _ := r.Manifests(r.context) |
|
| 161 |
- return &signatures{
|
|
| 162 |
- manifests: ms, |
|
| 159 |
+func (r *repository) Tags(ctx context.Context) distribution.TagService {
|
|
| 160 |
+ return &tags{
|
|
| 161 |
+ client: r.client, |
|
| 162 |
+ ub: r.ub, |
|
| 163 |
+ context: r.context, |
|
| 164 |
+ name: r.Name(), |
|
| 163 | 165 |
} |
| 164 | 166 |
} |
| 165 | 167 |
|
| 166 |
-type signatures struct {
|
|
| 167 |
- manifests distribution.ManifestService |
|
| 168 |
-} |
|
| 169 |
- |
|
| 170 |
-func (s *signatures) Get(dgst digest.Digest) ([][]byte, error) {
|
|
| 171 |
- m, err := s.manifests.Get(dgst) |
|
| 172 |
- if err != nil {
|
|
| 173 |
- return nil, err |
|
| 174 |
- } |
|
| 175 |
- return m.Signatures() |
|
| 168 |
+// tags implements remote tagging operations. |
|
| 169 |
+type tags struct {
|
|
| 170 |
+ client *http.Client |
|
| 171 |
+ ub *v2.URLBuilder |
|
| 172 |
+ context context.Context |
|
| 173 |
+ name string |
|
| 176 | 174 |
} |
| 177 | 175 |
|
| 178 |
-func (s *signatures) Put(dgst digest.Digest, signatures ...[]byte) error {
|
|
| 179 |
- panic("not implemented")
|
|
| 180 |
-} |
|
| 176 |
+// All returns all tags |
|
| 177 |
+func (t *tags) All(ctx context.Context) ([]string, error) {
|
|
| 178 |
+ var tags []string |
|
| 181 | 179 |
|
| 182 |
-type manifests struct {
|
|
| 183 |
- name string |
|
| 184 |
- ub *v2.URLBuilder |
|
| 185 |
- client *http.Client |
|
| 186 |
- etags map[string]string |
|
| 187 |
-} |
|
| 188 |
- |
|
| 189 |
-func (ms *manifests) Tags() ([]string, error) {
|
|
| 190 |
- u, err := ms.ub.BuildTagsURL(ms.name) |
|
| 180 |
+ u, err := t.ub.BuildTagsURL(t.name) |
|
| 191 | 181 |
if err != nil {
|
| 192 |
- return nil, err |
|
| 182 |
+ return tags, err |
|
| 193 | 183 |
} |
| 194 | 184 |
|
| 195 |
- resp, err := ms.client.Get(u) |
|
| 185 |
+ resp, err := t.client.Get(u) |
|
| 196 | 186 |
if err != nil {
|
| 197 |
- return nil, err |
|
| 187 |
+ return tags, err |
|
| 198 | 188 |
} |
| 199 | 189 |
defer resp.Body.Close() |
| 200 | 190 |
|
| 201 | 191 |
if SuccessStatus(resp.StatusCode) {
|
| 202 | 192 |
b, err := ioutil.ReadAll(resp.Body) |
| 203 | 193 |
if err != nil {
|
| 204 |
- return nil, err |
|
| 194 |
+ return tags, err |
|
| 205 | 195 |
} |
| 206 | 196 |
|
| 207 | 197 |
tagsResponse := struct {
|
| 208 | 198 |
Tags []string `json:"tags"` |
| 209 | 199 |
}{}
|
| 210 | 200 |
if err := json.Unmarshal(b, &tagsResponse); err != nil {
|
| 211 |
- return nil, err |
|
| 201 |
+ return tags, err |
|
| 212 | 202 |
} |
| 203 |
+ tags = tagsResponse.Tags |
|
| 204 |
+ return tags, nil |
|
| 205 |
+ } |
|
| 206 |
+ return tags, HandleErrorResponse(resp) |
|
| 207 |
+} |
|
| 213 | 208 |
|
| 214 |
- return tagsResponse.Tags, nil |
|
| 209 |
+func descriptorFromResponse(response *http.Response) (distribution.Descriptor, error) {
|
|
| 210 |
+ desc := distribution.Descriptor{}
|
|
| 211 |
+ headers := response.Header |
|
| 212 |
+ |
|
| 213 |
+ ctHeader := headers.Get("Content-Type")
|
|
| 214 |
+ if ctHeader == "" {
|
|
| 215 |
+ return distribution.Descriptor{}, errors.New("missing or empty Content-Type header")
|
|
| 216 |
+ } |
|
| 217 |
+ desc.MediaType = ctHeader |
|
| 218 |
+ |
|
| 219 |
+ digestHeader := headers.Get("Docker-Content-Digest")
|
|
| 220 |
+ if digestHeader == "" {
|
|
| 221 |
+ bytes, err := ioutil.ReadAll(response.Body) |
|
| 222 |
+ if err != nil {
|
|
| 223 |
+ return distribution.Descriptor{}, err
|
|
| 224 |
+ } |
|
| 225 |
+ _, desc, err := distribution.UnmarshalManifest(ctHeader, bytes) |
|
| 226 |
+ if err != nil {
|
|
| 227 |
+ return distribution.Descriptor{}, err
|
|
| 228 |
+ } |
|
| 229 |
+ return desc, nil |
|
| 230 |
+ } |
|
| 231 |
+ |
|
| 232 |
+ dgst, err := digest.ParseDigest(digestHeader) |
|
| 233 |
+ if err != nil {
|
|
| 234 |
+ return distribution.Descriptor{}, err
|
|
| 235 |
+ } |
|
| 236 |
+ desc.Digest = dgst |
|
| 237 |
+ |
|
| 238 |
+ lengthHeader := headers.Get("Content-Length")
|
|
| 239 |
+ if lengthHeader == "" {
|
|
| 240 |
+ return distribution.Descriptor{}, errors.New("missing or empty Content-Length header")
|
|
| 241 |
+ } |
|
| 242 |
+ length, err := strconv.ParseInt(lengthHeader, 10, 64) |
|
| 243 |
+ if err != nil {
|
|
| 244 |
+ return distribution.Descriptor{}, err
|
|
| 245 |
+ } |
|
| 246 |
+ desc.Size = length |
|
| 247 |
+ |
|
| 248 |
+ return desc, nil |
|
| 249 |
+ |
|
| 250 |
+} |
|
| 251 |
+ |
|
| 252 |
+// Get issues a HEAD request for a Manifest against its named endpoint in order |
|
| 253 |
+// to construct a descriptor for the tag. If the registry doesn't support HEADing |
|
| 254 |
+// a manifest, fallback to GET. |
|
| 255 |
+func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) {
|
|
| 256 |
+ u, err := t.ub.BuildManifestURL(t.name, tag) |
|
| 257 |
+ if err != nil {
|
|
| 258 |
+ return distribution.Descriptor{}, err
|
|
| 259 |
+ } |
|
| 260 |
+ var attempts int |
|
| 261 |
+ resp, err := t.client.Head(u) |
|
| 262 |
+ |
|
| 263 |
+check: |
|
| 264 |
+ if err != nil {
|
|
| 265 |
+ return distribution.Descriptor{}, err
|
|
| 266 |
+ } |
|
| 267 |
+ |
|
| 268 |
+ switch {
|
|
| 269 |
+ case resp.StatusCode >= 200 && resp.StatusCode < 400: |
|
| 270 |
+ return descriptorFromResponse(resp) |
|
| 271 |
+ case resp.StatusCode == http.StatusMethodNotAllowed: |
|
| 272 |
+ resp, err = t.client.Get(u) |
|
| 273 |
+ attempts++ |
|
| 274 |
+ if attempts > 1 {
|
|
| 275 |
+ return distribution.Descriptor{}, err
|
|
| 276 |
+ } |
|
| 277 |
+ goto check |
|
| 278 |
+ default: |
|
| 279 |
+ return distribution.Descriptor{}, HandleErrorResponse(resp)
|
|
| 215 | 280 |
} |
| 216 |
- return nil, handleErrorResponse(resp) |
|
| 217 | 281 |
} |
| 218 | 282 |
|
| 219 |
-func (ms *manifests) Exists(dgst digest.Digest) (bool, error) {
|
|
| 220 |
- // Call by Tag endpoint since the API uses the same |
|
| 221 |
- // URL endpoint for tags and digests. |
|
| 222 |
- return ms.ExistsByTag(dgst.String()) |
|
| 283 |
+func (t *tags) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) {
|
|
| 284 |
+ panic("not implemented")
|
|
| 285 |
+} |
|
| 286 |
+ |
|
| 287 |
+func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error {
|
|
| 288 |
+ panic("not implemented")
|
|
| 223 | 289 |
} |
| 224 | 290 |
|
| 225 |
-func (ms *manifests) ExistsByTag(tag string) (bool, error) {
|
|
| 226 |
- u, err := ms.ub.BuildManifestURL(ms.name, tag) |
|
| 291 |
+func (t *tags) Untag(ctx context.Context, tag string) error {
|
|
| 292 |
+ panic("not implemented")
|
|
| 293 |
+} |
|
| 294 |
+ |
|
| 295 |
+type manifests struct {
|
|
| 296 |
+ name string |
|
| 297 |
+ ub *v2.URLBuilder |
|
| 298 |
+ client *http.Client |
|
| 299 |
+ etags map[string]string |
|
| 300 |
+} |
|
| 301 |
+ |
|
| 302 |
+func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
|
|
| 303 |
+ u, err := ms.ub.BuildManifestURL(ms.name, dgst.String()) |
|
| 227 | 304 |
if err != nil {
|
| 228 | 305 |
return false, err |
| 229 | 306 |
} |
| ... | ... |
@@ -238,49 +315,66 @@ func (ms *manifests) ExistsByTag(tag string) (bool, error) {
|
| 238 | 238 |
} else if resp.StatusCode == http.StatusNotFound {
|
| 239 | 239 |
return false, nil |
| 240 | 240 |
} |
| 241 |
- return false, handleErrorResponse(resp) |
|
| 242 |
-} |
|
| 243 |
- |
|
| 244 |
-func (ms *manifests) Get(dgst digest.Digest) (*schema1.SignedManifest, error) {
|
|
| 245 |
- // Call by Tag endpoint since the API uses the same |
|
| 246 |
- // URL endpoint for tags and digests. |
|
| 247 |
- return ms.GetByTag(dgst.String()) |
|
| 241 |
+ return false, HandleErrorResponse(resp) |
|
| 248 | 242 |
} |
| 249 | 243 |
|
| 250 |
-// AddEtagToTag allows a client to supply an eTag to GetByTag which will be |
|
| 244 |
+// AddEtagToTag allows a client to supply an eTag to Get which will be |
|
| 251 | 245 |
// used for a conditional HTTP request. If the eTag matches, a nil manifest |
| 252 |
-// and nil error will be returned. etag is automatically quoted when added to |
|
| 253 |
-// this map. |
|
| 246 |
+// and ErrManifestNotModified error will be returned. etag is automatically |
|
| 247 |
+// quoted when added to this map. |
|
| 254 | 248 |
func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption {
|
| 255 |
- return func(ms distribution.ManifestService) error {
|
|
| 256 |
- if ms, ok := ms.(*manifests); ok {
|
|
| 257 |
- ms.etags[tag] = fmt.Sprintf(`"%s"`, etag) |
|
| 258 |
- return nil |
|
| 259 |
- } |
|
| 260 |
- return fmt.Errorf("etag options is a client-only option")
|
|
| 249 |
+ return etagOption{tag, etag}
|
|
| 250 |
+} |
|
| 251 |
+ |
|
| 252 |
+type etagOption struct{ tag, etag string }
|
|
| 253 |
+ |
|
| 254 |
+func (o etagOption) Apply(ms distribution.ManifestService) error {
|
|
| 255 |
+ if ms, ok := ms.(*manifests); ok {
|
|
| 256 |
+ ms.etags[o.tag] = fmt.Sprintf(`"%s"`, o.etag) |
|
| 257 |
+ return nil |
|
| 261 | 258 |
} |
| 259 |
+ return fmt.Errorf("etag options is a client-only option")
|
|
| 262 | 260 |
} |
| 263 | 261 |
|
| 264 |
-func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*schema1.SignedManifest, error) {
|
|
| 262 |
+func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) {
|
|
| 263 |
+ |
|
| 264 |
+ var tag string |
|
| 265 | 265 |
for _, option := range options {
|
| 266 |
- err := option(ms) |
|
| 267 |
- if err != nil {
|
|
| 268 |
- return nil, err |
|
| 266 |
+ if opt, ok := option.(withTagOption); ok {
|
|
| 267 |
+ tag = opt.tag |
|
| 268 |
+ } else {
|
|
| 269 |
+ err := option.Apply(ms) |
|
| 270 |
+ if err != nil {
|
|
| 271 |
+ return nil, err |
|
| 272 |
+ } |
|
| 269 | 273 |
} |
| 270 | 274 |
} |
| 271 | 275 |
|
| 272 |
- u, err := ms.ub.BuildManifestURL(ms.name, tag) |
|
| 276 |
+ var ref string |
|
| 277 |
+ if tag != "" {
|
|
| 278 |
+ ref = tag |
|
| 279 |
+ } else {
|
|
| 280 |
+ ref = dgst.String() |
|
| 281 |
+ } |
|
| 282 |
+ |
|
| 283 |
+ u, err := ms.ub.BuildManifestURL(ms.name, ref) |
|
| 273 | 284 |
if err != nil {
|
| 274 | 285 |
return nil, err |
| 275 | 286 |
} |
| 287 |
+ |
|
| 276 | 288 |
req, err := http.NewRequest("GET", u, nil)
|
| 277 | 289 |
if err != nil {
|
| 278 | 290 |
return nil, err |
| 279 | 291 |
} |
| 280 | 292 |
|
| 281 |
- if _, ok := ms.etags[tag]; ok {
|
|
| 282 |
- req.Header.Set("If-None-Match", ms.etags[tag])
|
|
| 293 |
+ for _, t := range distribution.ManifestMediaTypes() {
|
|
| 294 |
+ req.Header.Add("Accept", t)
|
|
| 295 |
+ } |
|
| 296 |
+ |
|
| 297 |
+ if _, ok := ms.etags[ref]; ok {
|
|
| 298 |
+ req.Header.Set("If-None-Match", ms.etags[ref])
|
|
| 283 | 299 |
} |
| 300 |
+ |
|
| 284 | 301 |
resp, err := ms.client.Do(req) |
| 285 | 302 |
if err != nil {
|
| 286 | 303 |
return nil, err |
| ... | ... |
@@ -289,44 +383,89 @@ func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServic |
| 289 | 289 |
if resp.StatusCode == http.StatusNotModified {
|
| 290 | 290 |
return nil, distribution.ErrManifestNotModified |
| 291 | 291 |
} else if SuccessStatus(resp.StatusCode) {
|
| 292 |
- var sm schema1.SignedManifest |
|
| 293 |
- decoder := json.NewDecoder(resp.Body) |
|
| 292 |
+ mt := resp.Header.Get("Content-Type")
|
|
| 293 |
+ body, err := ioutil.ReadAll(resp.Body) |
|
| 294 | 294 |
|
| 295 |
- if err := decoder.Decode(&sm); err != nil {
|
|
| 295 |
+ if err != nil {
|
|
| 296 |
+ return nil, err |
|
| 297 |
+ } |
|
| 298 |
+ m, _, err := distribution.UnmarshalManifest(mt, body) |
|
| 299 |
+ if err != nil {
|
|
| 296 | 300 |
return nil, err |
| 297 | 301 |
} |
| 298 |
- return &sm, nil |
|
| 302 |
+ return m, nil |
|
| 303 |
+ } |
|
| 304 |
+ return nil, HandleErrorResponse(resp) |
|
| 305 |
+} |
|
| 306 |
+ |
|
| 307 |
+// WithTag allows a tag to be passed into Put which enables the client |
|
| 308 |
+// to build a correct URL. |
|
| 309 |
+func WithTag(tag string) distribution.ManifestServiceOption {
|
|
| 310 |
+ return withTagOption{tag}
|
|
| 311 |
+} |
|
| 312 |
+ |
|
| 313 |
+type withTagOption struct{ tag string }
|
|
| 314 |
+ |
|
| 315 |
+func (o withTagOption) Apply(m distribution.ManifestService) error {
|
|
| 316 |
+ if _, ok := m.(*manifests); ok {
|
|
| 317 |
+ return nil |
|
| 299 | 318 |
} |
| 300 |
- return nil, handleErrorResponse(resp) |
|
| 319 |
+ return fmt.Errorf("withTagOption is a client-only option")
|
|
| 301 | 320 |
} |
| 302 | 321 |
|
| 303 |
-func (ms *manifests) Put(m *schema1.SignedManifest) error {
|
|
| 304 |
- manifestURL, err := ms.ub.BuildManifestURL(ms.name, m.Tag) |
|
| 322 |
+// Put puts a manifest. A tag can be specified using an options parameter which uses some shared state to hold the |
|
| 323 |
+// tag name in order to build the correct upload URL. This state is written and read under a lock. |
|
| 324 |
+func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) {
|
|
| 325 |
+ var tag string |
|
| 326 |
+ |
|
| 327 |
+ for _, option := range options {
|
|
| 328 |
+ if opt, ok := option.(withTagOption); ok {
|
|
| 329 |
+ tag = opt.tag |
|
| 330 |
+ } else {
|
|
| 331 |
+ err := option.Apply(ms) |
|
| 332 |
+ if err != nil {
|
|
| 333 |
+ return "", err |
|
| 334 |
+ } |
|
| 335 |
+ } |
|
| 336 |
+ } |
|
| 337 |
+ |
|
| 338 |
+ manifestURL, err := ms.ub.BuildManifestURL(ms.name, tag) |
|
| 305 | 339 |
if err != nil {
|
| 306 |
- return err |
|
| 340 |
+ return "", err |
|
| 307 | 341 |
} |
| 308 | 342 |
|
| 309 |
- // todo(richardscothern): do something with options here when they become applicable |
|
| 343 |
+ mediaType, p, err := m.Payload() |
|
| 344 |
+ if err != nil {
|
|
| 345 |
+ return "", err |
|
| 346 |
+ } |
|
| 310 | 347 |
|
| 311 |
- putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(m.Raw))
|
|
| 348 |
+ putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p))
|
|
| 312 | 349 |
if err != nil {
|
| 313 |
- return err |
|
| 350 |
+ return "", err |
|
| 314 | 351 |
} |
| 315 | 352 |
|
| 353 |
+ putRequest.Header.Set("Content-Type", mediaType)
|
|
| 354 |
+ |
|
| 316 | 355 |
resp, err := ms.client.Do(putRequest) |
| 317 | 356 |
if err != nil {
|
| 318 |
- return err |
|
| 357 |
+ return "", err |
|
| 319 | 358 |
} |
| 320 | 359 |
defer resp.Body.Close() |
| 321 | 360 |
|
| 322 | 361 |
if SuccessStatus(resp.StatusCode) {
|
| 323 |
- // TODO(dmcgowan): make use of digest header |
|
| 324 |
- return nil |
|
| 362 |
+ dgstHeader := resp.Header.Get("Docker-Content-Digest")
|
|
| 363 |
+ dgst, err := digest.ParseDigest(dgstHeader) |
|
| 364 |
+ if err != nil {
|
|
| 365 |
+ return "", err |
|
| 366 |
+ } |
|
| 367 |
+ |
|
| 368 |
+ return dgst, nil |
|
| 325 | 369 |
} |
| 326 |
- return handleErrorResponse(resp) |
|
| 370 |
+ |
|
| 371 |
+ return "", HandleErrorResponse(resp) |
|
| 327 | 372 |
} |
| 328 | 373 |
|
| 329 |
-func (ms *manifests) Delete(dgst digest.Digest) error {
|
|
| 374 |
+func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error {
|
|
| 330 | 375 |
u, err := ms.ub.BuildManifestURL(ms.name, dgst.String()) |
| 331 | 376 |
if err != nil {
|
| 332 | 377 |
return err |
| ... | ... |
@@ -345,9 +484,14 @@ func (ms *manifests) Delete(dgst digest.Digest) error {
|
| 345 | 345 |
if SuccessStatus(resp.StatusCode) {
|
| 346 | 346 |
return nil |
| 347 | 347 |
} |
| 348 |
- return handleErrorResponse(resp) |
|
| 348 |
+ return HandleErrorResponse(resp) |
|
| 349 | 349 |
} |
| 350 | 350 |
|
| 351 |
+// todo(richardscothern): Restore interface and implementation with merge of #1050 |
|
| 352 |
+/*func (ms *manifests) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) {
|
|
| 353 |
+ panic("not supported")
|
|
| 354 |
+}*/ |
|
| 355 |
+ |
|
| 351 | 356 |
type blobs struct {
|
| 352 | 357 |
name string |
| 353 | 358 |
ub *v2.URLBuilder |
| ... | ... |
@@ -377,11 +521,7 @@ func (bs *blobs) Stat(ctx context.Context, dgst digest.Digest) (distribution.Des |
| 377 | 377 |
} |
| 378 | 378 |
|
| 379 | 379 |
func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
|
| 380 |
- desc, err := bs.Stat(ctx, dgst) |
|
| 381 |
- if err != nil {
|
|
| 382 |
- return nil, err |
|
| 383 |
- } |
|
| 384 |
- reader, err := bs.Open(ctx, desc.Digest) |
|
| 380 |
+ reader, err := bs.Open(ctx, dgst) |
|
| 385 | 381 |
if err != nil {
|
| 386 | 382 |
return nil, err |
| 387 | 383 |
} |
| ... | ... |
@@ -401,7 +541,7 @@ func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.Rea |
| 401 | 401 |
if resp.StatusCode == http.StatusNotFound {
|
| 402 | 402 |
return distribution.ErrBlobUnknown |
| 403 | 403 |
} |
| 404 |
- return handleErrorResponse(resp) |
|
| 404 |
+ return HandleErrorResponse(resp) |
|
| 405 | 405 |
}), nil |
| 406 | 406 |
} |
| 407 | 407 |
|
| ... | ... |
@@ -457,7 +597,7 @@ func (bs *blobs) Create(ctx context.Context) (distribution.BlobWriter, error) {
|
| 457 | 457 |
location: location, |
| 458 | 458 |
}, nil |
| 459 | 459 |
} |
| 460 |
- return nil, handleErrorResponse(resp) |
|
| 460 |
+ return nil, HandleErrorResponse(resp) |
|
| 461 | 461 |
} |
| 462 | 462 |
|
| 463 | 463 |
func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
|
| ... | ... |
@@ -488,6 +628,10 @@ func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distributi |
| 488 | 488 |
|
| 489 | 489 |
if SuccessStatus(resp.StatusCode) {
|
| 490 | 490 |
lengthHeader := resp.Header.Get("Content-Length")
|
| 491 |
+ if lengthHeader == "" {
|
|
| 492 |
+ return distribution.Descriptor{}, fmt.Errorf("missing content-length header for request: %s", u)
|
|
| 493 |
+ } |
|
| 494 |
+ |
|
| 491 | 495 |
length, err := strconv.ParseInt(lengthHeader, 10, 64) |
| 492 | 496 |
if err != nil {
|
| 493 | 497 |
return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err)
|
| ... | ... |
@@ -501,7 +645,7 @@ func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distributi |
| 501 | 501 |
} else if resp.StatusCode == http.StatusNotFound {
|
| 502 | 502 |
return distribution.Descriptor{}, distribution.ErrBlobUnknown
|
| 503 | 503 |
} |
| 504 |
- return distribution.Descriptor{}, handleErrorResponse(resp)
|
|
| 504 |
+ return distribution.Descriptor{}, HandleErrorResponse(resp)
|
|
| 505 | 505 |
} |
| 506 | 506 |
|
| 507 | 507 |
func buildCatalogValues(maxEntries int, last string) url.Values {
|
| ... | ... |
@@ -538,7 +682,7 @@ func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error {
|
| 538 | 538 |
if SuccessStatus(resp.StatusCode) {
|
| 539 | 539 |
return nil |
| 540 | 540 |
} |
| 541 |
- return handleErrorResponse(resp) |
|
| 541 |
+ return HandleErrorResponse(resp) |
|
| 542 | 542 |
} |
| 543 | 543 |
|
| 544 | 544 |
func (bs *blobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
|
| 545 | 545 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,27 @@ |
| 0 |
+package distribution |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/docker/distribution/context" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+// TagService provides access to information about tagged objects. |
|
| 7 |
+type TagService interface {
|
|
| 8 |
+ // Get retrieves the descriptor identified by the tag. Some |
|
| 9 |
+ // implementations may differentiate between "trusted" tags and |
|
| 10 |
+ // "untrusted" tags. If a tag is "untrusted", the mapping will be returned |
|
| 11 |
+ // as an ErrTagUntrusted error, with the target descriptor. |
|
| 12 |
+ Get(ctx context.Context, tag string) (Descriptor, error) |
|
| 13 |
+ |
|
| 14 |
+ // Tag associates the tag with the provided descriptor, updating the |
|
| 15 |
+ // current association, if needed. |
|
| 16 |
+ Tag(ctx context.Context, tag string, desc Descriptor) error |
|
| 17 |
+ |
|
| 18 |
+ // Untag removes the given tag association |
|
| 19 |
+ Untag(ctx context.Context, tag string) error |
|
| 20 |
+ |
|
| 21 |
+ // All returns the set of tags managed by this tag service |
|
| 22 |
+ All(ctx context.Context) ([]string, error) |
|
| 23 |
+ |
|
| 24 |
+ // Lookup returns the set of tags referencing the given digest. |
|
| 25 |
+ Lookup(ctx context.Context, digest Descriptor) ([]string, error) |
|
| 26 |
+} |