summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:48:08 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:48:08 +0000
commitb65b89d538e8c6adad31b84584fe2c53ba8ebc09 (patch)
tree6fe7ff2b7c36ddf98d24c8a854ca6299103658d1
parentInitial commit. (diff)
downloadgo-containerregistry-upstream.tar.xz
go-containerregistry-upstream.zip
Adding upstream version 0.14.0+ds1.upstream/0.14.0+ds1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.codecov.yaml2
-rw-r--r--.gitattributes7
-rw-r--r--.github/ISSUE_TEMPLATE/crane_bug_report.md25
-rw-r--r--.github/ISSUE_TEMPLATE/ggcr_bug_report.md25
-rw-r--r--.github/ISSUE_TEMPLATE/question.md9
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/analyze.yaml25
-rw-r--r--.github/workflows/boilerplate.yaml33
-rw-r--r--.github/workflows/build.yaml26
-rw-r--r--.github/workflows/bump-deps.yaml35
-rw-r--r--.github/workflows/donotsubmit.yaml15
-rw-r--r--.github/workflows/e2e.yaml94
-rw-r--r--.github/workflows/ecr-auth.yaml93
-rw-r--r--.github/workflows/ghcr-auth.yaml47
-rw-r--r--.github/workflows/presubmit.yaml34
-rw-r--r--.github/workflows/release.yml78
-rw-r--r--.github/workflows/stale.yaml30
-rw-r--r--.github/workflows/style.yaml55
-rw-r--r--.github/workflows/test.yaml28
-rw-r--r--.gitignore12
-rw-r--r--.golangci.yaml40
-rw-r--r--.goreleaser.yml119
-rw-r--r--.ko/debug/.ko.yaml1
-rw-r--r--.wokeignore1
-rw-r--r--CONTRIBUTING.md36
-rw-r--r--LICENSE202
-rw-r--r--README.md150
-rw-r--r--SECURITY.md4
-rw-r--r--cloudbuild.yaml61
-rw-r--r--cmd/crane/README.md122
-rw-r--r--cmd/crane/cmd/append.go122
-rw-r--r--cmd/crane/cmd/auth.go205
-rw-r--r--cmd/crane/cmd/blob.go48
-rw-r--r--cmd/crane/cmd/catalog.go54
-rw-r--r--cmd/crane/cmd/config.go39
-rw-r--r--cmd/crane/cmd/copy.go34
-rw-r--r--cmd/crane/cmd/delete.go33
-rw-r--r--cmd/crane/cmd/digest.go91
-rw-r--r--cmd/crane/cmd/export.go89
-rw-r--r--cmd/crane/cmd/flatten.go254
-rw-r--r--cmd/crane/cmd/index.go291
-rw-r--r--cmd/crane/cmd/list.go62
-rw-r--r--cmd/crane/cmd/manifest.go40
-rw-r--r--cmd/crane/cmd/mutate.go207
-rw-r--r--cmd/crane/cmd/optimize.go42
-rw-r--r--cmd/crane/cmd/pull.go138
-rw-r--r--cmd/crane/cmd/push.go126
-rw-r--r--cmd/crane/cmd/rebase.go210
-rw-r--r--cmd/crane/cmd/root.go148
-rw-r--r--cmd/crane/cmd/serve.go84
-rw-r--r--cmd/crane/cmd/tag.go44
-rw-r--r--cmd/crane/cmd/util.go86
-rw-r--r--cmd/crane/cmd/validate.go73
-rw-r--r--cmd/crane/cmd/version.go56
-rw-r--r--cmd/crane/depcheck_test.go32
-rw-r--r--cmd/crane/doc/crane.md42
-rw-r--r--cmd/crane/doc/crane_append.md43
-rw-r--r--cmd/crane/doc/crane_auth.md29
-rw-r--r--cmd/crane/doc/crane_auth_get.md38
-rw-r--r--cmd/crane/doc/crane_auth_login.md37
-rw-r--r--cmd/crane/doc/crane_blob.md33
-rw-r--r--cmd/crane/doc/crane_catalog.md34
-rw-r--r--cmd/crane/doc/crane_config.md27
-rw-r--r--cmd/crane/doc/crane_copy.md27
-rw-r--r--cmd/crane/doc/crane_delete.md27
-rw-r--r--cmd/crane/doc/crane_digest.md29
-rw-r--r--cmd/crane/doc/crane_export.md40
-rw-r--r--cmd/crane/doc/crane_flatten.md28
-rw-r--r--cmd/crane/doc/crane_index.md29
-rw-r--r--cmd/crane/doc/crane_index_append.md47
-rw-r--r--cmd/crane/doc/crane_index_filter.md41
-rw-r--r--cmd/crane/doc/crane_ls.md29
-rw-r--r--cmd/crane/doc/crane_manifest.md27
-rw-r--r--cmd/crane/doc/crane_mutate.md37
-rw-r--r--cmd/crane/doc/crane_pull.md30
-rw-r--r--cmd/crane/doc/crane_push.md33
-rw-r--r--cmd/crane/doc/crane_rebase.md32
-rw-r--r--cmd/crane/doc/crane_registry.md24
-rw-r--r--cmd/crane/doc/crane_registry_serve.md35
-rw-r--r--cmd/crane/doc/crane_tag.md46
-rw-r--r--cmd/crane/doc/crane_validate.md30
-rw-r--r--cmd/crane/doc/crane_version.md34
-rw-r--r--cmd/crane/help/README.md5
-rw-r--r--cmd/crane/help/main.go45
-rw-r--r--cmd/crane/main.go38
-rw-r--r--cmd/crane/rebase.md125
-rw-r--r--cmd/crane/rebase.pngbin0 -> 49992 bytes
-rwxr-xr-xcmd/crane/rebase_test.sh62
-rw-r--r--cmd/crane/recipes.md105
-rw-r--r--cmd/gcrane/README.md65
-rw-r--r--cmd/gcrane/cmd/copy.go47
-rw-r--r--cmd/gcrane/cmd/gc.go76
-rw-r--r--cmd/gcrane/cmd/list.go121
-rw-r--r--cmd/gcrane/depcheck_test.go32
-rw-r--r--cmd/gcrane/main.go72
-rw-r--r--cmd/ko/README.md3
-rw-r--r--cmd/krane/README.md15
-rw-r--r--cmd/krane/go.mod67
-rw-r--r--cmd/krane/go.sum199
-rw-r--r--cmd/krane/main.go67
-rw-r--r--cmd/registry/main.go44
-rwxr-xr-xcmd/registry/test.sh57
-rw-r--r--go.mod49
-rw-r--r--go.sum153
-rw-r--r--hack/boilerplate/boilerplate.go.txt13
-rwxr-xr-xhack/bump-deps.sh47
-rwxr-xr-xhack/presubmit.sh58
-rwxr-xr-xhack/update-codegen.sh52
-rwxr-xr-xhack/update-deps.sh31
-rwxr-xr-xhack/update-dots.sh30
-rw-r--r--images/containerd.dot.svg2074
-rw-r--r--images/containers.dot.svg5365
-rw-r--r--images/crane.pngbin0 -> 539880 bytes
-rw-r--r--images/credhelper-basic.svg1
-rw-r--r--images/credhelper-oauth.svg1
-rw-r--r--images/docker.dot.svg2155
-rw-r--r--images/dot/containerd.dot316
-rw-r--r--images/dot/containers.dot831
-rw-r--r--images/dot/docker.dot327
-rw-r--r--images/dot/ggcr.dot130
-rw-r--r--images/dot/image-anatomy.dot26
-rw-r--r--images/dot/index-anatomy-strange.dot24
-rw-r--r--images/dot/index-anatomy.dot18
-rw-r--r--images/dot/mutate.dot59
-rw-r--r--images/dot/remote.dot66
-rw-r--r--images/dot/stream.dot47
-rw-r--r--images/dot/tarball.dot43
-rw-r--r--images/dot/upload.dot67
-rw-r--r--images/gcrane.pngbin0 -> 561713 bytes
-rw-r--r--images/ggcr.dot.svg874
-rw-r--r--images/image-anatomy.dot.svg99
-rw-r--r--images/index-anatomy-strange.dot.svg125
-rw-r--r--images/index-anatomy.dot.svg85
-rw-r--r--images/mutate.dot.svg250
-rw-r--r--images/ociimage.gv97
-rw-r--r--images/ociimage.jpegbin0 -> 114782 bytes
-rw-r--r--images/remote.dot.svg180
-rw-r--r--images/stream.dot.svg217
-rw-r--r--images/tarball.dot.svg126
-rw-r--r--images/upload.dot.svg359
-rw-r--r--internal/and/and_closer.go48
-rw-r--r--internal/and/and_closer_test.go85
-rw-r--r--internal/cmd/edit.go485
-rw-r--r--internal/cmd/edit_test.go174
-rw-r--r--internal/compare/doc.go16
-rw-r--r--internal/compare/image.go111
-rw-r--r--internal/compare/image_test.go66
-rw-r--r--internal/compare/index.go83
-rw-r--r--internal/compare/index_test.go51
-rw-r--r--internal/compare/layer.go80
-rw-r--r--internal/compare/layer_test.go48
-rw-r--r--internal/compression/compression.go97
-rw-r--r--internal/compression/compression_test.go78
-rw-r--r--internal/depcheck/depcheck.go186
-rw-r--r--internal/editor/editor.go64
-rw-r--r--internal/estargz/estargz.go54
-rw-r--r--internal/estargz/estargz_test.go108
-rw-r--r--internal/gzip/zip.go118
-rw-r--r--internal/gzip/zip_test.go98
-rw-r--r--internal/httptest/httptest.go104
-rw-r--r--internal/legacy/copy.go57
-rw-r--r--internal/legacy/copy_test.go97
-rw-r--r--internal/redact/redact.go89
-rw-r--r--internal/retry/retry.go94
-rw-r--r--internal/retry/retry_test.go100
-rw-r--r--internal/retry/wait/kubernetes_apimachinery_wait.go123
-rw-r--r--internal/verify/verify.go122
-rw-r--r--internal/verify/verify_test.go147
-rw-r--r--internal/windows/windows.go114
-rw-r--r--internal/windows/windows_test.go81
-rw-r--r--internal/zstd/zstd.go116
-rw-r--r--internal/zstd/zstd_test.go96
-rw-r--r--pkg/authn/README.md322
-rw-r--r--pkg/authn/anon.go26
-rw-r--r--pkg/authn/anon_test.go31
-rw-r--r--pkg/authn/auth.go30
-rw-r--r--pkg/authn/authn.go115
-rw-r--r--pkg/authn/authn_test.go148
-rw-r--r--pkg/authn/basic.go29
-rw-r--r--pkg/authn/basic_test.go33
-rw-r--r--pkg/authn/bearer.go27
-rw-r--r--pkg/authn/bearer_test.go31
-rw-r--r--pkg/authn/doc.go17
-rw-r--r--pkg/authn/github/keychain.go59
-rw-r--r--pkg/authn/github/keychain_test.go112
-rw-r--r--pkg/authn/k8schain/README.md49
-rw-r--r--pkg/authn/k8schain/doc.go18
-rw-r--r--pkg/authn/k8schain/go.mod96
-rw-r--r--pkg/authn/k8schain/go.sum364
-rw-r--r--pkg/authn/k8schain/k8schain.go105
-rw-r--r--pkg/authn/k8schain/tests/explicit/main.go52
-rw-r--r--pkg/authn/k8schain/tests/explicit/test.yaml59
-rw-r--r--pkg/authn/k8schain/tests/implicit/main.go52
-rw-r--r--pkg/authn/k8schain/tests/implicit/test.yaml47
-rw-r--r--pkg/authn/k8schain/tests/noauth/main.go47
-rw-r--r--pkg/authn/k8schain/tests/noauth/test.yaml44
-rw-r--r--pkg/authn/k8schain/tests/serviceaccount/main.go54
-rw-r--r--pkg/authn/k8schain/tests/serviceaccount/test.yaml67
-rw-r--r--pkg/authn/keychain.go180
-rw-r--r--pkg/authn/keychain_test.go392
-rw-r--r--pkg/authn/kubernetes/go.mod59
-rw-r--r--pkg/authn/kubernetes/go.sum276
-rw-r--r--pkg/authn/kubernetes/keychain.go331
-rw-r--r--pkg/authn/kubernetes/keychain_test.go586
-rw-r--r--pkg/authn/multikeychain.go41
-rw-r--r--pkg/authn/multikeychain_test.go98
-rw-r--r--pkg/compression/compression.go26
-rw-r--r--pkg/crane/append.go114
-rw-r--r--pkg/crane/append_test.go73
-rw-r--r--pkg/crane/catalog.go35
-rw-r--r--pkg/crane/config.go24
-rw-r--r--pkg/crane/copy.go88
-rw-r--r--pkg/crane/crane_test.go574
-rw-r--r--pkg/crane/delete.go33
-rw-r--r--pkg/crane/digest.go52
-rw-r--r--pkg/crane/digest_test.go61
-rw-r--r--pkg/crane/doc.go16
-rw-r--r--pkg/crane/example_test.go31
-rw-r--r--pkg/crane/export.go47
-rw-r--r--pkg/crane/export_test.go41
-rw-r--r--pkg/crane/filemap.go72
-rw-r--r--pkg/crane/filemap_test.go187
-rw-r--r--pkg/crane/get.go56
-rw-r--r--pkg/crane/list.go33
-rw-r--r--pkg/crane/manifest.go32
-rw-r--r--pkg/crane/optimize.go237
-rw-r--r--pkg/crane/optimize_test.go179
-rw-r--r--pkg/crane/options.go149
-rw-r--r--pkg/crane/options_test.go58
-rw-r--r--pkg/crane/pull.go142
-rw-r--r--pkg/crane/push.go65
-rw-r--r--pkg/crane/tag.go39
-rwxr-xr-xpkg/crane/testdata/content.tarbin0 -> 10240 bytes
-rw-r--r--pkg/gcrane/copy.go347
-rw-r--r--pkg/gcrane/copy_test.go428
-rw-r--r--pkg/gcrane/doc.go16
-rw-r--r--pkg/gcrane/options.go122
-rw-r--r--pkg/gcrane/options_test.go58
-rw-r--r--pkg/legacy/config.go33
-rw-r--r--pkg/legacy/doc.go18
-rw-r--r--pkg/legacy/tarball/README.md6
-rw-r--r--pkg/legacy/tarball/doc.go18
-rw-r--r--pkg/legacy/tarball/write.go374
-rw-r--r--pkg/legacy/tarball/write_test.go615
-rw-r--r--pkg/logs/logs.go39
-rw-r--r--pkg/name/README.md3
-rw-r--r--pkg/name/check.go43
-rw-r--r--pkg/name/digest.go94
-rw-r--r--pkg/name/digest_test.go152
-rw-r--r--pkg/name/doc.go42
-rw-r--r--pkg/name/errors.go48
-rw-r--r--pkg/name/errors_test.go37
-rw-r--r--pkg/name/internal/must_test.go27
-rwxr-xr-xpkg/name/internal/must_test.sh29
-rw-r--r--pkg/name/options.go83
-rw-r--r--pkg/name/ref.go75
-rw-r--r--pkg/name/ref_test.go157
-rw-r--r--pkg/name/registry.go136
-rw-r--r--pkg/name/registry_test.go252
-rw-r--r--pkg/name/repository.go121
-rw-r--r--pkg/name/repository_test.go145
-rw-r--r--pkg/name/tag.go108
-rw-r--r--pkg/name/tag_test.go162
-rw-r--r--pkg/registry/README.md14
-rw-r--r--pkg/registry/blobs.go483
-rw-r--r--pkg/registry/compatibility_test.go63
-rw-r--r--pkg/registry/depcheck_test.go38
-rw-r--r--pkg/registry/error.go79
-rw-r--r--pkg/registry/manifest.go430
-rw-r--r--pkg/registry/registry.go117
-rw-r--r--pkg/registry/registry_test.go609
-rw-r--r--pkg/registry/tls.go29
-rw-r--r--pkg/registry/tls_test.go49
-rw-r--r--pkg/v1/cache/cache.go194
-rw-r--r--pkg/v1/cache/cache_test.go154
-rw-r--r--pkg/v1/cache/example_test.go46
-rw-r--r--pkg/v1/cache/fs.go151
-rw-r--r--pkg/v1/cache/fs_test.go213
-rw-r--r--pkg/v1/cache/ro.go27
-rw-r--r--pkg/v1/cache/ro_test.go79
-rw-r--r--pkg/v1/config.go151
-rw-r--r--pkg/v1/config_test.go38
-rw-r--r--pkg/v1/daemon/README.md11
-rw-r--r--pkg/v1/daemon/doc.go17
-rw-r--r--pkg/v1/daemon/image.go203
-rw-r--r--pkg/v1/daemon/image_test.go159
-rw-r--r--pkg/v1/daemon/options.go103
-rw-r--r--pkg/v1/daemon/write.go60
-rw-r--r--pkg/v1/daemon/write_test.go159
-rw-r--r--pkg/v1/doc.go18
-rw-r--r--pkg/v1/empty/README.md8
-rw-r--r--pkg/v1/empty/doc.go16
-rw-r--r--pkg/v1/empty/image.go52
-rw-r--r--pkg/v1/empty/image_test.go48
-rw-r--r--pkg/v1/empty/index.go64
-rw-r--r--pkg/v1/empty/index_test.go40
-rw-r--r--pkg/v1/fake/image.go826
-rw-r--r--pkg/v1/fake/index.go546
-rw-r--r--pkg/v1/google/README.md7
-rw-r--r--pkg/v1/google/auth.go179
-rw-r--r--pkg/v1/google/auth_test.go270
-rw-r--r--pkg/v1/google/doc.go16
-rw-r--r--pkg/v1/google/keychain.go92
-rw-r--r--pkg/v1/google/list.go331
-rw-r--r--pkg/v1/google/list_test.go339
-rw-r--r--pkg/v1/google/options.go73
-rw-r--r--pkg/v1/google/testdata/README.md4
-rw-r--r--pkg/v1/google/testdata/key.json35
-rw-r--r--pkg/v1/hash.go123
-rw-r--r--pkg/v1/hash_test.go115
-rw-r--r--pkg/v1/image.go59
-rw-r--r--pkg/v1/index.go43
-rw-r--r--pkg/v1/layer.go42
-rw-r--r--pkg/v1/layout/README.md5
-rw-r--r--pkg/v1/layout/blob.go37
-rw-r--r--pkg/v1/layout/doc.go19
-rw-r--r--pkg/v1/layout/image.go139
-rw-r--r--pkg/v1/layout/image_test.go181
-rw-r--r--pkg/v1/layout/index.go161
-rw-r--r--pkg/v1/layout/index_test.go81
-rw-r--r--pkg/v1/layout/layoutpath.go25
-rw-r--r--pkg/v1/layout/options.go71
-rw-r--r--pkg/v1/layout/read.go32
-rw-r--r--pkg/v1/layout/read_test.go42
-rw-r--r--pkg/v1/layout/testdata/README.md5
-rw-r--r--pkg/v1/layout/testdata/test_index/blobs/sha256/05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b513
-rw-r--r--pkg/v1/layout/testdata/test_index/blobs/sha256/2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb13
-rw-r--r--pkg/v1/layout/testdata/test_index/blobs/sha256/321460fa87fd42433950b42d04b7aff249f4ed960d43404a9f699886906cc9d3bin0 -> 165 bytes
-rw-r--r--pkg/v1/layout/testdata/test_index/blobs/sha256/32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b0038507201
-rw-r--r--pkg/v1/layout/testdata/test_index/blobs/sha256/6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e1
-rw-r--r--pkg/v1/layout/testdata/test_index/blobs/sha256/930705ce23e3b6ed4c08746b6fe880089c864fbaf62482702ae3fdd66b8c7fe91
-rw-r--r--pkg/v1/layout/testdata/test_index/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2bbin0 -> 167 bytes
-rw-r--r--pkg/v1/layout/testdata/test_index/blobs/sha256/eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b2786501
-rw-r--r--pkg/v1/layout/testdata/test_index/index.json37
-rw-r--r--pkg/v1/layout/testdata/test_index/oci-layout3
-rw-r--r--pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e15
-rw-r--r--pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e4423561
-rw-r--r--pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2bbin0 -> 167 bytes
-rw-r--r--pkg/v1/layout/testdata/test_index_media_type/index.json10
-rw-r--r--pkg/v1/layout/testdata/test_index_media_type/oci-layout3
-rw-r--r--pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/381d958b555884ba59574ab5c066e9f6116b5aec3567675aa13bec63331f08101
-rw-r--r--pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0bin0 -> 114 bytes
-rw-r--r--pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/98ceaf93e482fe91b9bfd6bba07137c098e49ee2d55e69f09fb6c951e75e0e461
-rw-r--r--pkg/v1/layout/testdata/test_index_one_image/index.json1
-rw-r--r--pkg/v1/layout/testdata/test_index_one_image/oci-layout1
-rw-r--r--pkg/v1/layout/write.go481
-rw-r--r--pkg/v1/layout/write_test.go672
-rw-r--r--pkg/v1/manifest.go71
-rw-r--r--pkg/v1/manifest_test.go76
-rw-r--r--pkg/v1/match/match.go92
-rw-r--r--pkg/v1/match/match_test.go131
-rw-r--r--pkg/v1/mutate/README.md56
-rw-r--r--pkg/v1/mutate/doc.go16
-rw-r--r--pkg/v1/mutate/image.go287
-rw-r--r--pkg/v1/mutate/index.go204
-rw-r--r--pkg/v1/mutate/index_test.go235
-rw-r--r--pkg/v1/mutate/mutate.go553
-rw-r--r--pkg/v1/mutate/mutate_test.go770
-rw-r--r--pkg/v1/mutate/rebase.go144
-rw-r--r--pkg/v1/mutate/rebase_test.go179
-rw-r--r--pkg/v1/mutate/testdata/README.md10
-rw-r--r--pkg/v1/mutate/testdata/bar1
-rw-r--r--pkg/v1/mutate/testdata/foo1
-rwxr-xr-xpkg/v1/mutate/testdata/overwritten_file.tarbin0 -> 51200 bytes
-rwxr-xr-xpkg/v1/mutate/testdata/source_image.tarbin0 -> 20480 bytes
-rwxr-xr-xpkg/v1/mutate/testdata/source_image_with_empty_layer_history.tarbin0 -> 20480 bytes
-rw-r--r--pkg/v1/mutate/testdata/whiteout/bar.txt1
-rw-r--r--pkg/v1/mutate/testdata/whiteout/foo.txt1
-rwxr-xr-xpkg/v1/mutate/testdata/whiteout_image.tarbin0 -> 51200 bytes
-rw-r--r--pkg/v1/mutate/whiteout_test.go43
-rw-r--r--pkg/v1/partial/README.md82
-rw-r--r--pkg/v1/partial/compressed.go188
-rw-r--r--pkg/v1/partial/compressed_test.go193
-rw-r--r--pkg/v1/partial/configlayer_test.go139
-rw-r--r--pkg/v1/partial/doc.go17
-rw-r--r--pkg/v1/partial/image.go28
-rw-r--r--pkg/v1/partial/index.go85
-rw-r--r--pkg/v1/partial/index_test.go119
-rw-r--r--pkg/v1/partial/uncompressed.go223
-rw-r--r--pkg/v1/partial/uncompressed_test.go233
-rw-r--r--pkg/v1/partial/with.go436
-rw-r--r--pkg/v1/partial/with_test.go246
-rw-r--r--pkg/v1/platform.go149
-rw-r--r--pkg/v1/platform_test.go235
-rw-r--r--pkg/v1/progress.go25
-rw-r--r--pkg/v1/random/doc.go16
-rw-r--r--pkg/v1/random/image.go116
-rw-r--r--pkg/v1/random/image_test.go129
-rw-r--r--pkg/v1/random/index.go111
-rw-r--r--pkg/v1/random/index_test.go64
-rw-r--r--pkg/v1/remote/README.md117
-rw-r--r--pkg/v1/remote/catalog.go154
-rw-r--r--pkg/v1/remote/catalog_test.go183
-rw-r--r--pkg/v1/remote/check.go72
-rw-r--r--pkg/v1/remote/check_e2e_test.go46
-rw-r--r--pkg/v1/remote/check_test.go76
-rw-r--r--pkg/v1/remote/delete.go61
-rw-r--r--pkg/v1/remote/delete_test.go89
-rw-r--r--pkg/v1/remote/descriptor.go511
-rw-r--r--pkg/v1/remote/descriptor_test.go259
-rw-r--r--pkg/v1/remote/doc.go17
-rw-r--r--pkg/v1/remote/error_roundtrip_test.go127
-rw-r--r--pkg/v1/remote/image.go256
-rw-r--r--pkg/v1/remote/image_test.go743
-rw-r--r--pkg/v1/remote/index.go319
-rw-r--r--pkg/v1/remote/index_test.go504
-rw-r--r--pkg/v1/remote/layer.go94
-rw-r--r--pkg/v1/remote/layer_test.go148
-rw-r--r--pkg/v1/remote/list.go141
-rw-r--r--pkg/v1/remote/list_test.go159
-rw-r--r--pkg/v1/remote/mount.go108
-rw-r--r--pkg/v1/remote/mount_test.go55
-rw-r--r--pkg/v1/remote/multi_write.go302
-rw-r--r--pkg/v1/remote/multi_write_test.go351
-rw-r--r--pkg/v1/remote/options.go317
-rw-r--r--pkg/v1/remote/progress.go69
-rw-r--r--pkg/v1/remote/progress_test.go463
-rw-r--r--pkg/v1/remote/referrers.go35
-rw-r--r--pkg/v1/remote/referrers_test.go183
-rw-r--r--pkg/v1/remote/transport/README.md129
-rw-r--r--pkg/v1/remote/transport/basic.go62
-rw-r--r--pkg/v1/remote/transport/basic_test.go138
-rw-r--r--pkg/v1/remote/transport/bearer.go320
-rw-r--r--pkg/v1/remote/transport/bearer_test.go561
-rw-r--r--pkg/v1/remote/transport/doc.go18
-rw-r--r--pkg/v1/remote/transport/error.go173
-rw-r--r--pkg/v1/remote/transport/error_test.go236
-rw-r--r--pkg/v1/remote/transport/logger.go91
-rw-r--r--pkg/v1/remote/transport/logger_test.go93
-rw-r--r--pkg/v1/remote/transport/ping.go227
-rw-r--r--pkg/v1/remote/transport/ping_test.go260
-rw-r--r--pkg/v1/remote/transport/retry.go111
-rw-r--r--pkg/v1/remote/transport/retry_test.go177
-rw-r--r--pkg/v1/remote/transport/schemer.go44
-rw-r--r--pkg/v1/remote/transport/scope.go24
-rw-r--r--pkg/v1/remote/transport/transport.go116
-rw-r--r--pkg/v1/remote/transport/transport_test.go282
-rw-r--r--pkg/v1/remote/transport/useragent.go94
-rw-r--r--pkg/v1/remote/write.go1003
-rw-r--r--pkg/v1/remote/write_test.go1643
-rw-r--r--pkg/v1/static/layer.go68
-rw-r--r--pkg/v1/static/static_test.go83
-rw-r--r--pkg/v1/stream/README.md68
-rw-r--r--pkg/v1/stream/layer.go273
-rw-r--r--pkg/v1/stream/layer_test.go298
-rw-r--r--pkg/v1/tarball/README.md280
-rw-r--r--pkg/v1/tarball/doc.go17
-rw-r--r--pkg/v1/tarball/image.go429
-rw-r--r--pkg/v1/tarball/image_test.go139
-rw-r--r--pkg/v1/tarball/layer.go349
-rw-r--r--pkg/v1/tarball/layer_test.go381
-rw-r--r--pkg/v1/tarball/progress_test.go57
-rw-r--r--pkg/v1/tarball/testdata/bar1
-rw-r--r--pkg/v1/tarball/testdata/bat/bat1
-rw-r--r--pkg/v1/tarball/testdata/baz1
-rwxr-xr-xpkg/v1/tarball/testdata/content.tarbin0 -> 10240 bytes
-rw-r--r--pkg/v1/tarball/testdata/foo1
-rw-r--r--pkg/v1/tarball/testdata/no_manifest.tarbin0 -> 20480 bytes
-rw-r--r--pkg/v1/tarball/testdata/null_manifest.tarbin0 -> 2048 bytes
-rwxr-xr-xpkg/v1/tarball/testdata/test_bundle.tarbin0 -> 40960 bytes
-rwxr-xr-xpkg/v1/tarball/testdata/test_image_1.tarbin0 -> 20480 bytes
-rwxr-xr-xpkg/v1/tarball/testdata/test_image_2.tarbin0 -> 20480 bytes
-rw-r--r--pkg/v1/tarball/testdata/test_link.tarbin0 -> 29696 bytes
-rw-r--r--pkg/v1/tarball/testdata/test_load_manifest.tarbin0 -> 20480 bytes
-rw-r--r--pkg/v1/tarball/write.go457
-rw-r--r--pkg/v1/tarball/write_test.go502
-rw-r--r--pkg/v1/types/types.go82
-rw-r--r--pkg/v1/types/types_test.go112
-rw-r--r--pkg/v1/validate/doc.go16
-rw-r--r--pkg/v1/validate/image.go288
-rw-r--r--pkg/v1/validate/index.go175
-rw-r--r--pkg/v1/validate/layer.go191
-rw-r--r--pkg/v1/validate/options.go37
-rw-r--r--pkg/v1/zz_deepcopy_generated.go339
474 files changed, 65397 insertions, 0 deletions
diff --git a/.codecov.yaml b/.codecov.yaml
new file mode 100644
index 0000000..68c99ae
--- /dev/null
+++ b/.codecov.yaml
@@ -0,0 +1,2 @@
+ignore:
+ - "**/zz_*_generated.go" # Ignore generated files.
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..e4fb951
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+# This file is documented at https://git-scm.com/docs/gitattributes.
+# Linguist-specific attributes are documented at
+# https://github.com/github/linguist.
+
+**/zz_deepcopy_generated.go linguist-generated=true
+cmd/crane/doc/crane*.md linguist-generated=true
+go.sum linguist-generated=true
diff --git a/.github/ISSUE_TEMPLATE/crane_bug_report.md b/.github/ISSUE_TEMPLATE/crane_bug_report.md
new file mode 100644
index 0000000..fb14c38
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/crane_bug_report.md
@@ -0,0 +1,25 @@
+---
+name: crane bug report
+about: Create a report to help us improve the crane or gcrane CLIs
+title: 'crane:'
+labels: bug
+assignees: ''
+
+---
+
+### Describe the bug
+
+A clear and concise description of what the bug is.
+
+### To Reproduce
+
+### Expected behavior
+
+A clear and concise description of what you expected to happen.
+
+### Additional context
+
+Add any other context about the problem here.
+
+- Output of `crane version`
+- Registry used (e.g., GCR, ECR, Quay)
diff --git a/.github/ISSUE_TEMPLATE/ggcr_bug_report.md b/.github/ISSUE_TEMPLATE/ggcr_bug_report.md
new file mode 100644
index 0000000..790d97a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/ggcr_bug_report.md
@@ -0,0 +1,25 @@
+---
+name: Go library bug report
+about: Create a report to help us improve the Go library
+title: 'ggcr:'
+labels: bug
+assignees: ''
+
+---
+
+### Describe the bug
+
+A clear and concise description of what the bug is.
+
+### To Reproduce
+
+### Expected behavior
+
+A clear and concise description of what you expected to happen.
+
+### Additional context
+
+Add any other context about the problem here.
+
+- Version of the module
+- Registry used (e.g., GCR, ECR, Quay)
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 0000000..ff4f551
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,9 @@
+---
+name: Question
+about: Ask a question about the project
+title: 'question:'
+labels: question
+assignees: ''
+
+---
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..e2347a8
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+- package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/analyze.yaml b/.github/workflows/analyze.yaml
new file mode 100644
index 0000000..5982e60
--- /dev/null
+++ b/.github/workflows/analyze.yaml
@@ -0,0 +1,25 @@
+name: Analyze
+
+on:
+ workflow_dispatch:
+ push:
+ branches: ['main']
+ pull_request:
+ branches: ['main']
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ # We must fetch at least the immediate parents so that if this is
+ # a pull request then we can checkout the head.
+ fetch-depth: 2
+
+ - uses: github/codeql-action/init@v2
+ with:
+ languages: go
+ - uses: github/codeql-action/autobuild@v2
+ - uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/boilerplate.yaml b/.github/workflows/boilerplate.yaml
new file mode 100644
index 0000000..3782e51
--- /dev/null
+++ b/.github/workflows/boilerplate.yaml
@@ -0,0 +1,33 @@
+name: Boilerplate
+
+on:
+ pull_request:
+ branches: ['main']
+
+jobs:
+
+ check:
+ name: Boilerplate Check
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false # Keep running if one leg fails.
+ matrix:
+ extension:
+ - go
+ - sh
+
+ # Map between extension and human-readable name.
+ include:
+ - extension: go
+ language: Go
+ - extension: sh
+ language: Bash
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v3
+
+ - uses: chainguard-dev/actions/boilerplate@5e21cb47971231c078a677dfe89a348371cb880c # main
+ with:
+ extension: ${{ matrix.extension }}
+ language: ${{ matrix.language }}
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..b3ba675
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,26 @@
+name: Build
+
+on:
+ pull_request:
+ branches: ['main']
+
+jobs:
+
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ go-version: [1.19, '1.20']
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: ${{ matrix.go-version }}
+ check-latest: true
+
+ - run: |
+ go build ./...
+ go test -run=^$ ./...
diff --git a/.github/workflows/bump-deps.yaml b/.github/workflows/bump-deps.yaml
new file mode 100644
index 0000000..4712489
--- /dev/null
+++ b/.github/workflows/bump-deps.yaml
@@ -0,0 +1,35 @@
+name: Bump Deps
+
+on:
+ schedule:
+ - cron: '0 6 * * 2' # weekly at 6AM Tuesday
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ bump-deps:
+ name: Bump Deps
+
+ # Don't bother bumping deps on forks.
+ if: ${{ github.repository == 'google/go-containerregistry' }}
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ check-latest: true
+
+ - run: ./hack/bump-deps.sh
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v4
+ with:
+ title: "Bump dependencies using hack/bump-deps.sh"
+ commit-message: "Bump dependencies using hack/bump-deps.sh"
+ labels: dependencies
+ assignees: imjasonh
+ delete-branch: true
diff --git a/.github/workflows/donotsubmit.yaml b/.github/workflows/donotsubmit.yaml
new file mode 100644
index 0000000..92d454b
--- /dev/null
+++ b/.github/workflows/donotsubmit.yaml
@@ -0,0 +1,15 @@
+name: Do Not Submit
+
+on:
+ pull_request:
+ branches: ['main']
+
+jobs:
+
+ donotsubmit:
+ name: Do Not Submit
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: chainguard-dev/actions/donotsubmit@5e21cb47971231c078a677dfe89a348371cb880c # main
diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
new file mode 100644
index 0000000..0991bf7
--- /dev/null
+++ b/.github/workflows/e2e.yaml
@@ -0,0 +1,94 @@
+name: Basic e2e test
+
+on:
+ pull_request:
+ branches: ['main']
+
+jobs:
+ e2e:
+ strategy:
+ fail-fast: false
+ matrix:
+ platform:
+ - ubuntu-latest
+ - windows-latest
+ name: e2e ${{ matrix.platform }}
+ runs-on: ${{ matrix.platform }}
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ check-latest: true
+
+ - name: crane append to an image, set the entrypoint, run it locally, roundtrip it
+ shell: bash
+ run: |
+ set -euxo pipefail
+
+ # Setup local registry
+ go run ./cmd/registry &
+
+ base=alpine
+ platform=linux/amd64
+ if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then
+ base=mcr.microsoft.com/windows/nanoserver:ltsc2022
+ platform=windows/amd64
+ fi
+
+ CGO_ENABLED=0 go build -o app/crane ./cmd/crane
+ tar cvf crane.tar app
+
+ # This prevents Bash for Windows from mangling path names.
+ # It shouldn't be necessary in general unless you're using Bash for
+ # Windows.
+ export MSYS_NO_PATHCONV=1
+
+ img=$(./app/crane mutate \
+ --entrypoint=/app/crane,version \
+ $(./app/crane append \
+ --platform ${platform} \
+ --base ${base} \
+ --new_tag localhost:1338/append-test \
+ --new_layer crane.tar))
+
+ # Run the image with and without args.
+ docker run $img
+ docker run $img --help
+
+ # Make sure we can roundtrip it through pull/push
+ layout=$(mktemp -d)
+ dst=localhost:1338/roundtrip-test
+
+ ./app/crane pull --format=oci $img $layout
+ ./app/crane push --image-refs=foo.images $layout $dst
+ diff <(./app/crane manifest $img) <(./app/crane manifest $(cat foo.images))
+
+ # Make sure we can roundtrip an index (distroless).
+ distroless=$(mktemp -d)
+ remote="gcr.io/distroless/static"
+ local="localhost:1338/distroless:static"
+
+ ./app/crane pull --format=oci $remote $distroless
+ ./app/crane push $distroless $local
+ diff <(./app/crane manifest $remote) <(./app/crane manifest $local)
+
+ # And that it works for a single platform (pulling from what we just pushed).
+ distroless=$(mktemp -d)
+ remote="$local"
+ local="localhost:1338/distroless/platform:static"
+
+ ./app/crane pull --platform=linux/arm64 --format=oci $remote $distroless
+ ./app/crane push $distroless $local
+ diff <(./app/crane manifest --platform linux/arm64 $remote) <(./app/crane manifest $local)
+
+ - name: crane pull image, and export it from stdin to filesystem tar to stdout
+ shell: bash
+ run: |
+ set -euxo pipefail
+
+ ./app/crane pull ubuntu ubuntu.tar
+ ./app/crane export - - < ubuntu.tar > filesystem.tar
+ ls -la *.tar
+
diff --git a/.github/workflows/ecr-auth.yaml b/.github/workflows/ecr-auth.yaml
new file mode 100644
index 0000000..47cfe29
--- /dev/null
+++ b/.github/workflows/ecr-auth.yaml
@@ -0,0 +1,93 @@
+name: ECR Authentication test
+
+on:
+ pull_request_target:
+ branches: [ 'main' ]
+
+permissions:
+ # This lets us clone the repo
+ contents: read
+ # This lets us mint identity tokens.
+ id-token: write
+
+jobs:
+ krane:
+ runs-on: ubuntu-latest
+ env:
+ AWS_ACCOUNT: 479305788615
+ AWS_REGION: us-east-2
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ check-latest: true
+
+ - name: Install krane
+ working-directory: ./cmd/krane
+ run: go install .
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v2.0.0
+ with:
+ role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/federated-ecr-readonly
+ aws-region: ${{ env.AWS_REGION }}
+
+ - name: Test krane + ECR
+ run: |
+ # List the tags
+ krane ls ${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/go-containerregistry-test
+
+ - name: Test krane auth get + ECR
+ shell: bash
+ run: |
+ CRED1=$(krane auth get ${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com)
+ CRED2=$(krane auth get ${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com)
+ if [[ "$CRED1" == "" ]] ; then
+ exit 1
+ fi
+ if [[ "$CRED1" == "$CRED2" ]] ; then
+ echo "credentials are cached by infrastructure"
+ fi
+
+ crane-ecr-login:
+ runs-on: ubuntu-latest
+ env:
+ AWS_ACCOUNT: 479305788615
+ AWS_REGION: us-east-2
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ check-latest: true
+
+ - name: Install crane
+ working-directory: ./cmd/crane
+ run: go install .
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v2.0.0
+ with:
+ role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/federated-ecr-readonly
+ aws-region: ${{ env.AWS_REGION }}
+
+ - run: |
+ wget https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com/0.5.0/linux-amd64/docker-credential-ecr-login
+ chmod +x ./docker-credential-ecr-login
+ mv docker-credential-ecr-login /usr/local/bin
+
+ cat > $HOME/.docker/config.json <<EOF
+ {
+ "credHelpers": {
+ "${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com": "ecr-login"
+ }
+ }
+ EOF
+
+ - name: Test crane + ECR
+ run: |
+ # List the tags
+ crane ls ${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/go-containerregistry-test
diff --git a/.github/workflows/ghcr-auth.yaml b/.github/workflows/ghcr-auth.yaml
new file mode 100644
index 0000000..a511827
--- /dev/null
+++ b/.github/workflows/ghcr-auth.yaml
@@ -0,0 +1,47 @@
+name: GHCR Authentication test
+
+on:
+ pull_request_target:
+ branches: ['main']
+ push:
+ branches: ['main']
+
+permissions:
+ contents: read
+ packages: read
+
+jobs:
+ krane:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ check-latest: true
+
+ - name: Install krane
+ working-directory: ./cmd/krane
+ run: go install .
+
+ - name: Test krane + GHCR
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ # List the tags
+ krane ls ghcr.io/${{ github.repository }}/testimage
+
+ - name: Test krane auth get + GHCR
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ shell: bash
+ run: |
+ CRED1=$(krane auth get ghcr.io)
+ CRED2=$(krane auth get ghcr.io)
+ if [[ "$CRED1" == "" ]] ; then
+ exit 1
+ fi
+ if [[ "$CRED1" == "$CRED2" ]] ; then
+ echo "credentials are cached by infrastructure"
+ fi
+
diff --git a/.github/workflows/presubmit.yaml b/.github/workflows/presubmit.yaml
new file mode 100644
index 0000000..7771a7f
--- /dev/null
+++ b/.github/workflows/presubmit.yaml
@@ -0,0 +1,34 @@
+# Copyright 2021 Google LLC All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: Presubmit
+
+on:
+ push:
+ branches: ['main']
+ pull_request:
+ branches: ['main']
+
+jobs:
+ presubmit:
+ name: Presubmit
+ runs-on: 'ubuntu-latest'
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ check-latest: true
+ - run: ./hack/presubmit.sh
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..abdaad3
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,78 @@
+name: goreleaser
+
+on:
+ push:
+ tags: ['*']
+
+jobs:
+ goreleaser:
+ runs-on: ubuntu-latest
+ outputs:
+ hashes: ${{ steps.hash.outputs.hashes }}
+ steps:
+ - uses: actions/checkout@v3
+ - name: Unshallow
+ run: git fetch --prune --unshallow
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.18
+ check-latest: true
+ - uses: goreleaser/goreleaser-action@v4.2.0
+ id: run-goreleaser
+ with:
+ version: latest
+ args: release --rm-dist
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Generate subject
+ id: hash
+ env:
+ ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
+ run: |
+ set -euo pipefail
+
+ checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
+ echo "::set-output name=hashes::$(cat $checksum_file | base64 -w0)"
+
+ provenance:
+ needs: [goreleaser]
+ permissions:
+ actions: read # To read the workflow path.
+ id-token: write # To sign the provenance.
+ contents: write # To add assets to a release.
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.5.0
+ with:
+ base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
+ upload-assets: true # upload to a new release
+
+ verification:
+ needs: [goreleaser, provenance]
+ runs-on: ubuntu-latest
+ permissions: read-all
+ steps:
+ # Note: this will be replaced with the GHA in the future.
+ # See https://github.com/slsa-framework/slsa-verifier/issues/95
+ - name: Install SLSA verifier
+ uses: slsa-framework/slsa-verifier/actions/installer@v2.0.1
+ - name: Download assets
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ set -euo pipefail
+ gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.tar.gz"
+ gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "multiple.intoto.jsonl"
+ - name: Verify assets
+ env:
+ CHECKSUMS: ${{ needs.goreleaser.outputs.hashes }}
+ PROVENANCE: "${{ needs.provenance.outputs.attestation-name }}"
+ run: |
+ set -euo pipefail
+ checksums=$(echo "$CHECKSUMS" | base64 -d)
+ while read -r line; do
+ fn=$(echo $line | cut -d ' ' -f2)
+ echo "Verifying $fn"
+ ./slsa-verifier-linux-amd64 -artifact-path "$fn" \
+ -provenance "$PROVENANCE" \
+ -source "github.com/$GITHUB_REPOSITORY" \
+ -tag "$GITHUB_REF_NAME"
+ done <<<"$checksums"
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
new file mode 100644
index 0000000..38d76ab
--- /dev/null
+++ b/.github/workflows/stale.yaml
@@ -0,0 +1,30 @@
+name: 'Close stale'
+
+on:
+ schedule:
+ - cron: '0 1 * * *'
+
+jobs:
+ stale:
+ runs-on: 'ubuntu-latest'
+ steps:
+ - uses: 'actions/stale@v7'
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+
+ stale-issue-message: |-
+ This issue is stale because it has been open for 90 days with no
+ activity. It will automatically close after 30 more days of
+ inactivity. Keep fresh with the 'lifecycle/frozen' label.
+ stale-issue-label: 'lifecycle/stale'
+ exempt-issue-labels: 'lifecycle/frozen'
+
+ stale-pr-message: |-
+ This Pull Request is stale because it has been open for 90 days with
+ no activity. It will automatically close after 30 more days of
+ inactivity. Keep fresh with the 'lifecycle/frozen' label.
+ stale-pr-label: 'lifecycle/stale'
+ exempt-pr-labels: 'lifecycle/frozen'
+
+ days-before-stale: 90
+ days-before-close: 30
diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml
new file mode 100644
index 0000000..baf54c9
--- /dev/null
+++ b/.github/workflows/style.yaml
@@ -0,0 +1,55 @@
+name: Code Style
+
+on:
+ pull_request:
+ branches: ['main']
+
+jobs:
+
+ goimports:
+ name: check goimports
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ check-latest: true
+ - uses: actions/checkout@v3
+ - uses: chainguard-dev/actions/goimports@5e21cb47971231c078a677dfe89a348371cb880c # main
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ check-latest: true
+
+ - uses: golangci/golangci-lint-action@v3.4.0
+ with:
+ version: v1.51.2
+
+ - uses: reviewdog/action-misspell@v1
+ if: ${{ always() }}
+ with:
+ github_token: ${{ secrets.github_token }}
+ fail_on_error: true
+ locale: "US"
+ exclude: ./vendor/*
+
+ - uses: chainguard-dev/actions/trailing-space@5e21cb47971231c078a677dfe89a348371cb880c # main
+ if: ${{ always() }}
+
+ - uses: chainguard-dev/actions/eof-newline@5e21cb47971231c078a677dfe89a348371cb880c # main
+ if: ${{ always() }}
+
+ - uses: get-woke/woke-action-reviewdog@v0
+ if: ${{ always() }}
+ with:
+ github-token: ${{ secrets.github_token }}
+ reporter: github-pr-check
+ level: error
+ fail-on-error: true
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..8f31fd2
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,28 @@
+name: Test
+
+on:
+ push:
+ branches: ['main']
+ pull_request:
+ branches: ['main']
+
+jobs:
+
+ test:
+ strategy:
+ matrix:
+ go-version: [1.19, '1.20']
+
+ name: Unit Tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: ${{ matrix.go-version }}
+ check-latest: true
+
+ - run: go test -coverprofile=coverage.txt -covermode=atomic -race ./...
+
+ - uses: codecov/codecov-action@v3.1.1
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6d410b5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+**/*~
+.project
+bazel*
+.idea
+*.iml
+
+cmd/crane/crane
+cmd/gcrane/gcrane
+cmd/krane/krane
+
+/.pc/
+/_build/
diff --git a/.golangci.yaml b/.golangci.yaml
new file mode 100644
index 0000000..1dee826
--- /dev/null
+++ b/.golangci.yaml
@@ -0,0 +1,40 @@
+run:
+ timeout: 5m
+
+ skip-dirs:
+ - internal
+ - pkg/registry
+
+issues:
+ exclude-rules:
+ - path: test # Excludes /test, *_test.go etc.
+ linters:
+ - gosec
+
+linters:
+ enable:
+ - asciicheck
+ - depguard
+ - errorlint
+ - gofmt
+ - gosec
+ - goimports
+ - importas
+ - prealloc
+ - revive
+ - misspell
+ - stylecheck
+ - tparallel
+ - unconvert
+ - unparam
+ - unused
+ - whitespace
+
+ disable:
+ - errcheck
+
+linters-settings:
+ depguard:
+ include-go-root: true
+ packages-with-error-message:
+ - crypto/sha256: "use crypto.SHA256 instead"
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..f078121
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,119 @@
+# This is an example goreleaser.yaml file with some sane defaults.
+# Make sure to check the documentation at http://goreleaser.com
+# before:
+# hooks:
+# # You may remove this if you don't use go modules.
+# - go mod download
+# # you may remove this if you don't need go generate
+# - go generate ./...
+builds:
+- id: crane
+ env:
+ - CGO_ENABLED=0
+ main: ./cmd/crane/main.go
+ binary: crane
+ flags:
+ - -trimpath
+ ldflags:
+ - -s
+ - -w
+ - -X github.com/google/go-containerregistry/cmd/crane/cmd.Version={{.Version}}
+ - -X github.com/google/go-containerregistry/pkg/v1/remote/transport.Version={{.Version}}
+ goarch:
+ - amd64
+ - arm
+ - arm64
+ - 386
+ - s390x
+ goos:
+ - linux
+ - darwin
+ - windows
+ ignore:
+ - goos: windows
+ goarch: 386
+
+- id: gcrane
+ env:
+ - CGO_ENABLED=0
+ main: ./cmd/gcrane/main.go
+ binary: gcrane
+ flags:
+ - -trimpath
+ ldflags:
+ - -s
+ - -w
+ - -X github.com/google/go-containerregistry/cmd/crane/cmd.Version={{.Version}}
+ - -X github.com/google/go-containerregistry/pkg/v1/remote/transport.Version={{.Version}}
+ goarch:
+ - amd64
+ - arm
+ - arm64
+ - 386
+ - s390x
+ goos:
+ - linux
+ - darwin
+ - windows
+ ignore:
+ - goos: windows
+ goarch: 386
+
+- id: krane
+ env:
+ - CGO_ENABLED=0
+ main: ./main.go
+ dir: ./cmd/krane
+ binary: krane
+ flags:
+ - -trimpath
+ ldflags:
+ - -s
+ - -w
+ - -X github.com/google/go-containerregistry/cmd/crane/cmd.Version={{.Version}}
+ - -X github.com/google/go-containerregistry/pkg/v1/remote/transport.Version={{.Version}}
+ goarch:
+ - amd64
+ - arm
+ - arm64
+ - 386
+ - s390x
+ goos:
+ - linux
+ - darwin
+ - windows
+ ignore:
+ - goos: windows
+ goarch: 386
+source:
+ enabled: true
+archives:
+- replacements:
+ darwin: Darwin
+ linux: Linux
+ windows: Windows
+ 386: i386
+ amd64: x86_64
+ name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
+checksum:
+ name_template: 'checksums.txt'
+snapshot:
+ name_template: "{{ .Tag }}-next"
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - '^docs:'
+ - '^test:'
+release:
+ footer: |
+ ### Container Images
+
+ https://gcr.io/go-containerregistry/crane:{{.Tag}}
+ https://gcr.io/go-containerregistry/gcrane:{{.Tag}}
+
+ For example:
+ ```
+ docker pull gcr.io/go-containerregistry/crane:{{.Tag}}
+ docker pull gcr.io/go-containerregistry/gcrane:{{.Tag}}
+ ```
diff --git a/.ko/debug/.ko.yaml b/.ko/debug/.ko.yaml
new file mode 100644
index 0000000..ded0f59
--- /dev/null
+++ b/.ko/debug/.ko.yaml
@@ -0,0 +1 @@
+defaultBaseImage: gcr.io/distroless/base:debug
diff --git a/.wokeignore b/.wokeignore
new file mode 100644
index 0000000..05b7efe
--- /dev/null
+++ b/.wokeignore
@@ -0,0 +1 @@
+vendor/**
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..29e762c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,36 @@
+# How to Contribute to go-containerregistry
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Testing
+
+Ensure the following passes:
+```
+./hack/presubmit.sh
+```
+and commit any resultant changes to `go.mod` and `go.sum`. To update any docs
+after client changes, run:
+
+```
+./hack/update-codegen.sh
+```
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c77c03e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,150 @@
+# go-containerregistry
+
+[![GitHub Actions Build Status](https://github.com/google/go-containerregistry/workflows/Build/badge.svg)](https://github.com/google/go-containerregistry/actions?query=workflow%3ABuild)
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry?status.svg)](https://godoc.org/github.com/google/go-containerregistry)
+[![Code Coverage](https://codecov.io/gh/google/go-containerregistry/branch/main/graph/badge.svg)](https://codecov.io/gh/google/go-containerregistry)
+
+## Introduction
+
+This is a golang library for working with container registries.
+It's largely based on the [Python library of the same name](https://github.com/google/containerregistry).
+
+The following diagram shows the main types that this library handles.
+![OCI image representation](images/ociimage.jpeg)
+
+## Philosophy
+
+The overarching design philosophy of this library is to define interfaces that present an immutable
+view of resources (e.g. [`Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1#Image),
+[`Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1#Layer),
+[`ImageIndex`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1#ImageIndex)),
+which can be backed by a variety of medium (e.g. [registry](./pkg/v1/remote/README.md),
+[tarball](./pkg/v1/tarball/README.md), [daemon](./pkg/v1/daemon/README.md), ...).
+
+To complement these immutable views, we support functional mutations that produce new immutable views
+of the resulting resource (e.g. [mutate](./pkg/v1/mutate/README.md)). The end goal is to provide a
+set of versatile primitives that can compose to do extraordinarily powerful things efficiently and easily.
+
+Both the resource views and mutations may be lazy, eager, memoizing, etc, and most are optimized
+for common paths based on the tooling we have seen in the wild (e.g. writing new images from disk
+to the registry as a compressed tarball).
+
+
+### Experiments
+
+Over time, we will add new functionality under experimental environment variables listed here.
+
+| Env Var | Value(s) | What is does |
+|---------|----------|--------------|
+| `GGCR_EXPERIMENT_ESTARGZ` | `"1"` | When enabled this experiment will direct `tarball.LayerFromOpener` to emit [estargz](https://github.com/opencontainers/image-spec/issues/815) compatible layers, which enable them to be lazily loaded by an appropriately configured containerd. |
+
+
+### `v1.Image`
+
+#### Sources
+
+* [`remote.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Image)
+* [`tarball.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball#Image)
+* [`daemon.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon#Image)
+* [`layout.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout#Path.Image)
+* [`random.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/random#Image)
+
+#### Sinks
+
+* [`remote.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Write)
+* [`tarball.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball#Write)
+* [`daemon.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon#Write)
+* [`legacy/tarball.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball#Write)
+* [`layout.AppendImage`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout#Path.AppendImage)
+
+### `v1.ImageIndex`
+
+#### Sources
+
+* [`remote.Index`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Index)
+* [`random.Index`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/random#Index)
+* [`layout.ImageIndexFromPath`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout#ImageIndexFromPath)
+
+#### Sinks
+
+* [`remote.WriteIndex`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#WriteIndex)
+* [`layout.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout#Write)
+
+### `v1.Layer`
+
+#### Sources
+
+* [`remote.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Layer)
+* [`tarball.LayerFromFile`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball#LayerFromFile)
+* [`random.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/random#Layer)
+* [`stream.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream#Layer)
+
+#### Sinks
+
+* [`remote.WriteLayer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#WriteLayer)
+
+## Overview
+
+### `mutate`
+
+The simplest use for these libraries is to read from one source and write to another.
+
+For example,
+
+ * `crane pull` is `remote.Image -> tarball.Write`,
+ * `crane push` is `tarball.Image -> remote.Write`,
+ * `crane cp` is `remote.Image -> remote.Write`.
+
+However, often you actually want to _change something_ about an image.
+This is the purpose of the [`mutate`](pkg/v1/mutate) package, which exposes
+some commonly useful things to change about an image.
+
+### `partial`
+
+If you're trying to use this library with a different source or sink than it already supports,
+it can be somewhat cumbersome. The `Image` and `Layer` interfaces are pretty wide, with a lot
+of redundant information. This is somewhat by design, because we want to expose this information
+as efficiently as possible where we can, but again it is a pain to implement yourself.
+
+The purpose of the [`partial`](pkg/v1/partial) package is to make implementing a `v1.Image`
+much easier, by filling in all the derived accessors for you if you implement a minimal
+subset of `v1.Image`.
+
+### `transport`
+
+You might think our abstractions are bad and you just want to authenticate
+and send requests to a registry.
+
+This is the purpose of the [`transport`](pkg/v1/remote/transport) and [`authn`](pkg/authn) packages.
+
+## Tools
+
+This repo hosts some tools built on top of the library.
+
+### `crane`
+
+[`crane`](cmd/crane/README.md) is a tool for interacting with remote images
+and registries.
+
+### `gcrane`
+
+[`gcrane`](cmd/gcrane/README.md) is a GCR-specific variant of `crane` that has
+richer output for the `ls` subcommand and some basic garbage collection support.
+
+### `krane`
+
+[`krane`](cmd/krane/README.md) is a drop-in replacement for `crane` that supports
+common Kubernetes-based workload identity mechanisms using [`k8schain`](#k8schain)
+as a fallback to traditional authentication mechanisms.
+
+### `k8schain`
+
+[`k8schain`](pkg/authn/k8schain/README.md) implements the authentication
+semantics used by kubelets in a way that is easily consumable by this library.
+
+`k8schain` is not a standalone tool, but it is linked here for visibility.
+
+### Emeritus: [`ko`](https://github.com/google/ko)
+
+This tool was originally developed in this repo but has since been moved to its
+own repo.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..ce1f393
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,4 @@
+To report a security issue, please use http://g.co/vulnz. We use
+http://g.co/vulnz for our intake, and do coordination and disclosure here on
+GitHub (including using GitHub Security Advisory). The Google Security Team will
+respond within 5 working days of your report on g.co/vulnz.
diff --git a/cloudbuild.yaml b/cloudbuild.yaml
new file mode 100644
index 0000000..b9f2ad7
--- /dev/null
+++ b/cloudbuild.yaml
@@ -0,0 +1,61 @@
+timeout: 3600s # 60 minutes
+
+steps:
+- name: golang
+ entrypoint: sh
+ args:
+ - -c
+ - |
+ set -eux
+
+ export GOROOT=/usr/local/go
+ export KO_DOCKER_REPO="gcr.io/$PROJECT_ID"
+ export GOFLAGS="-ldflags=-X=github.com/google/go-containerregistry/cmd/crane/cmd.Version=$COMMIT_SHA"
+
+ # Put contents of /workspace on GOPATH.
+ shadow=$$GOPATH/src/github.com/google/go-containerregistry
+ link_dir=$$(dirname "$$shadow")
+ mkdir -p $$link_dir
+ ln -s $$PWD $$shadow || stat $$shadow
+
+ # Install ko from release.
+ curl -L -o ko.tar.gz https://github.com/google/ko/releases/download/v0.8.2/ko_0.8.2_Linux_i386.tar.gz
+ tar xvfz ko.tar.gz
+ chmod +x ko
+ alias ko=$${PWD}/ko
+
+ # Use the ko binary to build the crane-ish builder images.
+ ko publish --platform=all -B github.com/google/go-containerregistry/cmd/crane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME"
+ ko publish --platform=all -B github.com/google/go-containerregistry/cmd/gcrane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME"
+ # ./cmd/krane is a separate module, so switch directories.
+ cd ./cmd/krane
+ ko publish --platform=all -B github.com/google/go-containerregistry/cmd/krane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME"
+ cd ../../
+
+ # Use the ko binary to build the crane-ish builder *debug* images.
+ export KO_CONFIG_PATH=$(pwd)/.ko/debug/
+ ko publish --platform=all -B github.com/google/go-containerregistry/cmd/crane -t "debug"
+ ko publish --platform=all -B github.com/google/go-containerregistry/cmd/gcrane -t "debug"
+ # ./cmd/krane is a separate module, so switch directories.
+ cd ./cmd/krane
+ ko publish --platform=all -B github.com/google/go-containerregistry/cmd/krane -t "debug"
+ cd ../../
+
+ # Tag-specific debug images are pushed to gcr.io/go-containerregistry/TOOL/debug:...
+ KO_DOCKER_REPO=gcr.io/$PROJECT_ID/crane/debug ko publish --platform=all --bare github.com/google/go-containerregistry/cmd/crane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME"
+ KO_DOCKER_REPO=gcr.io/$PROJECT_ID/gcrane/debug ko publish --platform=all --bare github.com/google/go-containerregistry/cmd/gcrane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME"
+ # ./cmd/krane is a separate module, so switch directories.
+ cd ./cmd/krane
+ KO_DOCKER_REPO=gcr.io/$PROJECT_ID/krane/debug ko publish --platform=all --bare github.com/google/go-containerregistry/cmd/krane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME"
+ cd ../../
+
+# Use the crane builder to get the digest for crane-ish.
+- name: gcr.io/$PROJECT_ID/crane
+ args: ['digest', 'gcr.io/$PROJECT_ID/crane']
+
+- name: gcr.io/$PROJECT_ID/crane
+ args: ['digest', 'gcr.io/$PROJECT_ID/gcrane']
+
+- name: gcr.io/$PROJECT_ID/crane
+ args: ['digest', 'gcr.io/$PROJECT_ID/krane']
+
diff --git a/cmd/crane/README.md b/cmd/crane/README.md
new file mode 100644
index 0000000..a05dbeb
--- /dev/null
+++ b/cmd/crane/README.md
@@ -0,0 +1,122 @@
+# `crane`
+
+[`crane`](doc/crane.md) is a tool for interacting with remote images
+and registries.
+
+<img src="../../images/crane.png" width="40%">
+
+A collection of useful things you can do with `crane` is [here](recipes.md).
+
+## Installation
+
+### Install from Releases
+
+1. Get the [latest release](https://github.com/google/go-containerregistry/releases/latest) version.
+
+ ```sh
+ $ VERSION=$(curl -s "https://api.github.com/repos/google/go-containerregistry/releases/latest" | jq -r '.tag_name')
+ ```
+
+ or set a specific version:
+
+ ```sh
+ $ VERSION=vX.Y.Z # Version number with a leading v
+ ```
+
+1. Download the release.
+
+ ```sh
+ $ OS=Linux # or Darwin, Windows
+ $ ARCH=x86_64 # or arm64, x86_64, armv6, i386, s390x
+ $ curl -sL "https://github.com/google/go-containerregistry/releases/download/${VERSION}/go-containerregistry_${OS}_${ARCH}.tar.gz" > go-containerregistry.tar.gz
+ ```
+
+1. Verify the signature. We generate [SLSA 3 provenance](https://slsa.dev) using
+ the OpenSSF's [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator).
+ To verify our release, install the verification tool from [slsa-framework/slsa-verifier#installation](https://github.com/slsa-framework/slsa-verifier#installation)
+ and verify as follows:
+
+ ```sh
+ $ curl -sL https://github.com/google/go-containerregistry/releases/download/${VERSION}/multiple.intoto.jsonl > provenance.intoto.jsonl
+ $ # NOTE: You may be using a different architecture.
+ $ slsa-verifier-linux-amd64 verify-artifact go-containerregistry.tar.gz --provenance-path provenance.intoto.jsonl --source-uri github.com/google/go-containerregistry --source-tag "${VERSION}"
+ PASSED: Verified SLSA provenance
+ ```
+
+1. Unpack it in the PATH.
+
+ ```sh
+ $ tar -zxvf go-containerregistry.tar.gz -C /usr/local/bin/ crane
+ ```
+
+### Install manually
+
+Install manually:
+
+```sh
+go install github.com/google/go-containerregistry/cmd/crane@latest
+```
+
+### Install via brew
+
+If you're macOS user and using [Homebrew](https://brew.sh/), you can install via brew command:
+
+```sh
+$ brew install crane
+```
+
+### Install on Arch Linux
+
+If you're an Arch Linux user you can install via pacman command:
+
+```sh
+$ pacman -S crane
+```
+
+### Setup on GitHub Actions
+
+You can use the [`setup-crane`](https://github.com/imjasonh/setup-crane) action
+to install `crane` and setup auth to [GitHub Container
+Registry](https://github.com/features/packages) in a GitHub Action workflow:
+
+```
+steps:
+- uses: imjasonh/setup-crane@v0.1
+```
+
+## Images
+
+You can also use crane as docker image
+
+```sh
+$ docker run --rm gcr.io/go-containerregistry/crane ls ubuntu
+10.04
+12.04.5
+12.04
+12.10
+```
+
+And it's also available with a shell, at the `:debug` tag:
+
+```sh
+docker run --rm -it --entrypoint "/busybox/sh" gcr.io/go-containerregistry/crane:debug
+```
+
+Tagged debug images are available at `gcr.io/go-containerregistry/crane/debug:[tag]`.
+
+### Using with GitLab
+
+```yaml
+# Tags an existing Docker image which was tagged with the short commit hash with the tag 'latest'
+docker-tag-latest:
+ stage: latest
+ only:
+ refs:
+ - main
+ image:
+ name: gcr.io/go-containerregistry/crane:debug
+ entrypoint: [""]
+ script:
+ - crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ - crane tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA latest
+```
diff --git a/cmd/crane/cmd/append.go b/cmd/crane/cmd/append.go
new file mode 100644
index 0000000..9eb2acc
--- /dev/null
+++ b/cmd/crane/cmd/append.go
@@ -0,0 +1,122 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdAppend creates a new cobra.Command for the append subcommand.
+func NewCmdAppend(options *[]crane.Option) *cobra.Command {
+ var baseRef, newTag, outFile string
+ var newLayers []string
+ var annotate, ociEmptyBase bool
+
+ appendCmd := &cobra.Command{
+ Use: "append",
+ Short: "Append contents of a tarball to a remote image",
+ Long: `This sub-command pushes an image based on an (optional)
+base image, with appended layers containing the contents of the
+provided tarballs.
+
+If the base image is a Windows base image (i.e., its config.OS is "windows"),
+the contents of the tarballs will be modified to be suitable for a Windows
+container image.`,
+ Args: cobra.NoArgs,
+ RunE: func(_ *cobra.Command, args []string) error {
+ var base v1.Image
+ var err error
+
+ if baseRef == "" {
+ logs.Warn.Printf("base unspecified, using empty image")
+ base = empty.Image
+ if ociEmptyBase {
+ base = mutate.MediaType(base, types.OCIManifestSchema1)
+ base = mutate.ConfigMediaType(base, types.OCIConfigJSON)
+ }
+ } else {
+ base, err = crane.Pull(baseRef, *options...)
+ if err != nil {
+ return fmt.Errorf("pulling %s: %w", baseRef, err)
+ }
+ }
+
+ img, err := crane.Append(base, newLayers...)
+ if err != nil {
+ return fmt.Errorf("appending %v: %w", newLayers, err)
+ }
+
+ if baseRef != "" && annotate {
+ ref, err := name.ParseReference(baseRef)
+ if err != nil {
+ return fmt.Errorf("parsing ref %q: %w", baseRef, err)
+ }
+
+ baseDigest, err := base.Digest()
+ if err != nil {
+ return err
+ }
+ anns := map[string]string{
+ specsv1.AnnotationBaseImageDigest: baseDigest.String(),
+ }
+ if _, ok := ref.(name.Tag); ok {
+ anns[specsv1.AnnotationBaseImageName] = ref.Name()
+ }
+ img = mutate.Annotations(img, anns).(v1.Image)
+ }
+
+ if outFile != "" {
+ if err := crane.Save(img, newTag, outFile); err != nil {
+ return fmt.Errorf("writing output %q: %w", outFile, err)
+ }
+ } else {
+ if err := crane.Push(img, newTag, *options...); err != nil {
+ return fmt.Errorf("pushing image %s: %w", newTag, err)
+ }
+ ref, err := name.ParseReference(newTag)
+ if err != nil {
+ return fmt.Errorf("parsing reference %s: %w", newTag, err)
+ }
+ d, err := img.Digest()
+ if err != nil {
+ return fmt.Errorf("digest: %w", err)
+ }
+ fmt.Println(ref.Context().Digest(d.String()))
+ }
+ return nil
+ },
+ }
+ appendCmd.Flags().StringVarP(&baseRef, "base", "b", "", "Name of base image to append to")
+ appendCmd.Flags().StringVarP(&newTag, "new_tag", "t", "", "Tag to apply to resulting image")
+ appendCmd.Flags().StringSliceVarP(&newLayers, "new_layer", "f", []string{}, "Path to tarball to append to image")
+ appendCmd.Flags().StringVarP(&outFile, "output", "o", "", "Path to new tarball of resulting image")
+ appendCmd.Flags().BoolVar(&annotate, "set-base-image-annotations", false, "If true, annotate the resulting image as being based on the base image")
+ appendCmd.Flags().BoolVar(&ociEmptyBase, "oci-empty-base", false, "If true, empty base image will have OCI media types instead of Docker")
+
+ appendCmd.MarkFlagsMutuallyExclusive("oci-empty-base", "base")
+ appendCmd.MarkFlagRequired("new_tag")
+ appendCmd.MarkFlagRequired("new_layer")
+ return appendCmd
+}
diff --git a/cmd/crane/cmd/auth.go b/cmd/crane/cmd/auth.go
new file mode 100644
index 0000000..1964ade
--- /dev/null
+++ b/cmd/crane/cmd/auth.go
@@ -0,0 +1,205 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "strings"
+
+ "github.com/docker/cli/cli/config"
+ "github.com/docker/cli/cli/config/types"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdAuth creates a new cobra.Command for the auth subcommand.
+func NewCmdAuth(options []crane.Option, argv ...string) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "auth",
+ Short: "Log in or access credentials",
+ Args: cobra.NoArgs,
+ RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Usage() },
+ }
+ cmd.AddCommand(NewCmdAuthGet(options, argv...), NewCmdAuthLogin(argv...))
+ return cmd
+}
+
+type credentials struct {
+ Username string
+ Secret string
+}
+
+// https://github.com/docker/cli/blob/2291f610ae73533e6e0749d4ef1e360149b1e46b/cli/config/credentials/native_store.go#L100-L109
+func toCreds(config *authn.AuthConfig) credentials {
+ creds := credentials{
+ Username: config.Username,
+ Secret: config.Password,
+ }
+
+ if config.IdentityToken != "" {
+ creds.Username = "<token>"
+ creds.Secret = config.IdentityToken
+ }
+ return creds
+}
+
+// NewCmdAuthGet creates a new `crane auth get` command.
+func NewCmdAuthGet(options []crane.Option, argv ...string) *cobra.Command {
+ if len(argv) == 0 {
+ argv = []string{os.Args[0]}
+ }
+
+ baseCmd := strings.Join(argv, " ")
+ eg := fmt.Sprintf(` # Read configured credentials for reg.example.com
+ $ echo "reg.example.com" | %s get
+ {"username":"AzureDiamond","password":"hunter2"}
+ # or
+ $ %s get reg.example.com
+ {"username":"AzureDiamond","password":"hunter2"}`, baseCmd, baseCmd)
+
+ return &cobra.Command{
+ Use: "get [REGISTRY_ADDR]",
+ Short: "Implements a credential helper",
+ Example: eg,
+ Args: cobra.MaximumNArgs(1),
+ RunE: func(_ *cobra.Command, args []string) error {
+ registryAddr := ""
+ if len(args) == 1 {
+ registryAddr = args[0]
+ } else {
+ b, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return err
+ }
+ registryAddr = strings.TrimSpace(string(b))
+ }
+
+ reg, err := name.NewRegistry(registryAddr)
+ if err != nil {
+ return err
+ }
+ authorizer, err := crane.GetOptions(options...).Keychain.Resolve(reg)
+ if err != nil {
+ return err
+ }
+
+ // If we don't find any credentials, there's a magic error to return:
+ //
+ // https://github.com/docker/docker-credential-helpers/blob/f78081d1f7fef6ad74ad6b79368de6348386e591/credentials/error.go#L4-L6
+ // https://github.com/docker/docker-credential-helpers/blob/f78081d1f7fef6ad74ad6b79368de6348386e591/credentials/credentials.go#L61-L63
+ if authorizer == authn.Anonymous {
+ fmt.Fprint(os.Stdout, "credentials not found in native keychain\n")
+ os.Exit(1)
+ }
+
+ auth, err := authorizer.Authorization()
+ if err != nil {
+ return err
+ }
+
+ // Convert back to a form that credential helpers can parse so that this
+ // can act as a meta credential helper.
+ creds := toCreds(auth)
+ return json.NewEncoder(os.Stdout).Encode(creds)
+ },
+ }
+}
+
+// NewCmdAuthLogin creates a new `crane auth login` command.
+func NewCmdAuthLogin(argv ...string) *cobra.Command {
+ var opts loginOptions
+
+ if len(argv) == 0 {
+ argv = []string{os.Args[0]}
+ }
+
+ eg := fmt.Sprintf(` # Log in to reg.example.com
+ %s login reg.example.com -u AzureDiamond -p hunter2`, strings.Join(argv, " "))
+
+ cmd := &cobra.Command{
+ Use: "login [OPTIONS] [SERVER]",
+ Short: "Log in to a registry",
+ Example: eg,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ reg, err := name.NewRegistry(args[0])
+ if err != nil {
+ return err
+ }
+
+ opts.serverAddress = reg.Name()
+
+ return login(opts)
+ },
+ }
+
+ flags := cmd.Flags()
+
+ flags.StringVarP(&opts.user, "username", "u", "", "Username")
+ flags.StringVarP(&opts.password, "password", "p", "", "Password")
+ flags.BoolVarP(&opts.passwordStdin, "password-stdin", "", false, "Take the password from stdin")
+
+ return cmd
+}
+
+type loginOptions struct {
+ serverAddress string
+ user string
+ password string
+ passwordStdin bool
+}
+
+func login(opts loginOptions) error {
+ if opts.passwordStdin {
+ contents, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return err
+ }
+
+ opts.password = strings.TrimSuffix(string(contents), "\n")
+ opts.password = strings.TrimSuffix(opts.password, "\r")
+ }
+ if opts.user == "" && opts.password == "" {
+ return errors.New("username and password required")
+ }
+ cf, err := config.Load(os.Getenv("DOCKER_CONFIG"))
+ if err != nil {
+ return err
+ }
+ creds := cf.GetCredentialsStore(opts.serverAddress)
+ if opts.serverAddress == name.DefaultRegistry {
+ opts.serverAddress = authn.DefaultAuthKey
+ }
+ if err := creds.Store(types.AuthConfig{
+ ServerAddress: opts.serverAddress,
+ Username: opts.user,
+ Password: opts.password,
+ }); err != nil {
+ return err
+ }
+
+ if err := cf.Save(); err != nil {
+ return err
+ }
+ log.Printf("logged in via %s", cf.Filename)
+ return nil
+}
diff --git a/cmd/crane/cmd/blob.go b/cmd/crane/cmd/blob.go
new file mode 100644
index 0000000..fef405f
--- /dev/null
+++ b/cmd/crane/cmd/blob.go
@@ -0,0 +1,48 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdBlob creates a new cobra.Command for the blob subcommand.
+func NewCmdBlob(options *[]crane.Option) *cobra.Command {
+ return &cobra.Command{
+ Use: "blob BLOB",
+ Short: "Read a blob from the registry",
+ Example: "crane blob ubuntu@sha256:4c1d20cdee96111c8acf1858b62655a37ce81ae48648993542b7ac363ac5c0e5 > blob.tar.gz",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ src := args[0]
+ layer, err := crane.PullLayer(src, *options...)
+ if err != nil {
+ return fmt.Errorf("pulling layer %s: %w", src, err)
+ }
+ blob, err := layer.Compressed()
+ if err != nil {
+ return fmt.Errorf("fetching blob %s: %w", src, err)
+ }
+ if _, err := io.Copy(cmd.OutOrStdout(), blob); err != nil {
+ return fmt.Errorf("copying blob %s: %w", src, err)
+ }
+ return nil
+ },
+ }
+}
diff --git a/cmd/crane/cmd/catalog.go b/cmd/crane/cmd/catalog.go
new file mode 100644
index 0000000..0bfa0a9
--- /dev/null
+++ b/cmd/crane/cmd/catalog.go
@@ -0,0 +1,54 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdCatalog creates a new cobra.Command for the repos subcommand.
+func NewCmdCatalog(options *[]crane.Option, argv ...string) *cobra.Command {
+ if len(argv) == 0 {
+ argv = []string{os.Args[0]}
+ }
+
+ baseCmd := strings.Join(argv, " ")
+ eg := fmt.Sprintf(` # list the repos for reg.example.com
+ $ %s catalog reg.example.com`, baseCmd)
+
+ return &cobra.Command{
+ Use: "catalog [REGISTRY]",
+ Short: "List the repos in a registry",
+ Example: eg,
+ Args: cobra.ExactArgs(1),
+ RunE: func(_ *cobra.Command, args []string) error {
+ reg := args[0]
+ repos, err := crane.Catalog(reg, *options...)
+ if err != nil {
+ return fmt.Errorf("reading repos for %s: %w", reg, err)
+ }
+
+ for _, repo := range repos {
+ fmt.Println(repo)
+ }
+ return nil
+ },
+ }
+}
diff --git a/cmd/crane/cmd/config.go b/cmd/crane/cmd/config.go
new file mode 100644
index 0000000..ed2a3fb
--- /dev/null
+++ b/cmd/crane/cmd/config.go
@@ -0,0 +1,39 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdConfig creates a new cobra.Command for the config subcommand.
+func NewCmdConfig(options *[]crane.Option) *cobra.Command {
+ return &cobra.Command{
+ Use: "config IMAGE",
+ Short: "Get the config of an image",
+ Args: cobra.ExactArgs(1),
+ RunE: func(_ *cobra.Command, args []string) error {
+ cfg, err := crane.Config(args[0], *options...)
+ if err != nil {
+ return fmt.Errorf("fetching config: %w", err)
+ }
+ fmt.Print(string(cfg))
+ return nil
+ },
+ }
+}
diff --git a/cmd/crane/cmd/copy.go b/cmd/crane/cmd/copy.go
new file mode 100644
index 0000000..81f2e70
--- /dev/null
+++ b/cmd/crane/cmd/copy.go
@@ -0,0 +1,34 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdCopy creates a new cobra.Command for the copy subcommand.
+func NewCmdCopy(options *[]crane.Option) *cobra.Command {
+ return &cobra.Command{
+ Use: "copy SRC DST",
+ Aliases: []string{"cp"},
+ Short: "Efficiently copy a remote image from src to dst while retaining the digest value",
+ Args: cobra.ExactArgs(2),
+ RunE: func(_ *cobra.Command, args []string) error {
+ src, dst := args[0], args[1]
+ return crane.Copy(src, dst, *options...)
+ },
+ }
+}
diff --git a/cmd/crane/cmd/delete.go b/cmd/crane/cmd/delete.go
new file mode 100644
index 0000000..18966ea
--- /dev/null
+++ b/cmd/crane/cmd/delete.go
@@ -0,0 +1,33 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdDelete creates a new cobra.Command for the delete subcommand.
+func NewCmdDelete(options *[]crane.Option) *cobra.Command {
+ return &cobra.Command{
+ Use: "delete IMAGE",
+ Short: "Delete an image reference from its registry",
+ Args: cobra.ExactArgs(1),
+ RunE: func(_ *cobra.Command, args []string) error {
+ ref := args[0]
+ return crane.Delete(ref, *options...)
+ },
+ }
+}
diff --git a/cmd/crane/cmd/digest.go b/cmd/crane/cmd/digest.go
new file mode 100644
index 0000000..2060bbc
--- /dev/null
+++ b/cmd/crane/cmd/digest.go
@@ -0,0 +1,91 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdDigest creates a new cobra.Command for the digest subcommand.
+func NewCmdDigest(options *[]crane.Option) *cobra.Command {
+ var tarball string
+ var fullRef bool
+ cmd := &cobra.Command{
+ Use: "digest IMAGE",
+ Short: "Get the digest of an image",
+ Args: cobra.MaximumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if tarball == "" && len(args) == 0 {
+ if err := cmd.Help(); err != nil {
+ return err
+ }
+ return errors.New("image reference required without --tarball")
+ }
+ if fullRef && tarball != "" {
+ return errors.New("cannot specify --full-ref with --tarball")
+ }
+
+ digest, err := getDigest(tarball, args, options)
+ if err != nil {
+ return err
+ }
+ if fullRef {
+ ref, err := name.ParseReference(args[0])
+ if err != nil {
+ return err
+ }
+ fmt.Println(ref.Context().Digest(digest))
+ } else {
+ fmt.Println(digest)
+ }
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&tarball, "tarball", "", "(Optional) path to tarball containing the image")
+ cmd.Flags().BoolVar(&fullRef, "full-ref", false, "(Optional) if true, print the full image reference by digest")
+
+ return cmd
+}
+
+func getDigest(tarball string, args []string, options *[]crane.Option) (string, error) {
+ if tarball != "" {
+ return getTarballDigest(tarball, args, options)
+ }
+
+ return crane.Digest(args[0], *options...)
+}
+
+func getTarballDigest(tarball string, args []string, options *[]crane.Option) (string, error) {
+ tag := ""
+ if len(args) > 0 {
+ tag = args[0]
+ }
+
+ img, err := crane.LoadTag(tarball, tag, *options...)
+ if err != nil {
+ return "", fmt.Errorf("loading image from %q: %w", tarball, err)
+ }
+ digest, err := img.Digest()
+ if err != nil {
+ return "", fmt.Errorf("computing digest: %w", err)
+ }
+ return digest.String(), nil
+}
diff --git a/cmd/crane/cmd/export.go b/cmd/crane/cmd/export.go
new file mode 100644
index 0000000..70b58c1
--- /dev/null
+++ b/cmd/crane/cmd/export.go
@@ -0,0 +1,89 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "os"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdExport creates a new cobra.Command for the export subcommand.
+func NewCmdExport(options *[]crane.Option) *cobra.Command {
+ return &cobra.Command{
+ Use: "export IMAGE|- TARBALL|-",
+ Short: "Export filesystem of a container image as a tarball",
+ Example: ` # Write tarball to stdout
+ crane export ubuntu -
+
+ # Write tarball to file
+ crane export ubuntu ubuntu.tar
+
+ # Read image from stdin
+ crane export - ubuntu.tar`,
+ Args: cobra.RangeArgs(1, 2),
+ RunE: func(_ *cobra.Command, args []string) error {
+ src, dst := args[0], "-"
+ if len(args) > 1 {
+ dst = args[1]
+ }
+
+ f, err := openFile(dst)
+ if err != nil {
+ return fmt.Errorf("failed to open %s: %w", dst, err)
+ }
+ defer f.Close()
+
+ var img v1.Image
+ if src == "-" {
+ tmpfile, err := os.CreateTemp("", "crane")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer os.Remove(tmpfile.Name())
+
+ if _, err := io.Copy(tmpfile, os.Stdin); err != nil {
+ log.Fatal(err)
+ }
+ tmpfile.Close()
+
+ img, err = tarball.ImageFromPath(tmpfile.Name(), nil)
+ if err != nil {
+ return fmt.Errorf("reading tarball from stdin: %w", err)
+ }
+ } else {
+ img, err = crane.Pull(src, *options...)
+ if err != nil {
+ return fmt.Errorf("pulling %s: %w", src, err)
+ }
+ }
+
+ return crane.Export(img, f)
+ },
+ }
+}
+
+func openFile(s string) (*os.File, error) {
+ if s == "-" {
+ return os.Stdout, nil
+ }
+ return os.Create(s)
+}
diff --git a/cmd/crane/cmd/flatten.go b/cmd/crane/cmd/flatten.go
new file mode 100644
index 0000000..76c13f1
--- /dev/null
+++ b/cmd/crane/cmd/flatten.go
@@ -0,0 +1,254 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "compress/gzip"
+ "encoding/json"
+ "fmt"
+ "log"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdFlatten creates a new cobra.Command for the flatten subcommand.
+func NewCmdFlatten(options *[]crane.Option) *cobra.Command {
+ var dst string
+
+ flattenCmd := &cobra.Command{
+ Use: "flatten",
+ Short: "Flatten an image's layers into a single layer",
+ Args: cobra.ExactArgs(1),
+ Run: func(cmd *cobra.Command, args []string) {
+ // We need direct access to the underlying remote options because crane
+ // doesn't expose great facilities for working with an index (yet).
+ o := crane.GetOptions(*options...)
+
+ // Pull image and get config.
+ src := args[0]
+
+ // If the new ref isn't provided, write over the original image.
+ // If that ref was provided by digest (e.g., output from
+ // another crane command), then strip that and push the
+ // mutated image by digest instead.
+ if dst == "" {
+ dst = src
+ }
+
+ ref, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ log.Fatalf("parsing %s: %v", src, err)
+ }
+ newRef, err := name.ParseReference(dst, o.Name...)
+ if err != nil {
+ log.Fatalf("parsing %s: %v", dst, err)
+ }
+ repo := newRef.Context()
+
+ flat, err := flatten(ref, repo, cmd.Parent().Use, o)
+ if err != nil {
+ log.Fatalf("flattening %s: %v", ref, err)
+ }
+
+ digest, err := flat.Digest()
+ if err != nil {
+ log.Fatalf("digesting new image: %v", err)
+ }
+
+ if _, ok := ref.(name.Digest); ok {
+ newRef = repo.Digest(digest.String())
+ }
+
+ if err := push(flat, newRef, o); err != nil {
+ log.Fatalf("pushing %s: %v", newRef, err)
+ }
+ fmt.Println(repo.Digest(digest.String()))
+ },
+ }
+ flattenCmd.Flags().StringVarP(&dst, "tag", "t", "", "New tag to apply to flattened image. If not provided, push by digest to the original image repository.")
+ return flattenCmd
+}
+
+func flatten(ref name.Reference, repo name.Repository, use string, o crane.Options) (partial.Describable, error) {
+ desc, err := remote.Get(ref, o.Remote...)
+ if err != nil {
+ return nil, fmt.Errorf("pulling %s: %w", ref, err)
+ }
+
+ if desc.MediaType.IsIndex() {
+ idx, err := desc.ImageIndex()
+ if err != nil {
+ return nil, err
+ }
+ return flattenIndex(idx, repo, use, o)
+ } else if desc.MediaType.IsImage() {
+ img, err := desc.Image()
+ if err != nil {
+ return nil, err
+ }
+ return flattenImage(img, repo, use, o)
+ }
+
+ return nil, fmt.Errorf("can't flatten %s", desc.MediaType)
+}
+
+func push(flat partial.Describable, ref name.Reference, o crane.Options) error {
+ if idx, ok := flat.(v1.ImageIndex); ok {
+ return remote.WriteIndex(ref, idx, o.Remote...)
+ } else if img, ok := flat.(v1.Image); ok {
+ return remote.Write(ref, img, o.Remote...)
+ }
+
+ return fmt.Errorf("can't push %T", flat)
+}
+
+type remoteIndex interface {
+ Manifests() ([]partial.Describable, error)
+}
+
+func flattenIndex(old v1.ImageIndex, repo name.Repository, use string, o crane.Options) (partial.Describable, error) {
+ ri, ok := old.(remoteIndex)
+ if !ok {
+ return nil, fmt.Errorf("unexpected index")
+ }
+
+ m, err := old.IndexManifest()
+ if err != nil {
+ return nil, err
+ }
+
+ manifests, err := ri.Manifests()
+ if err != nil {
+ return nil, err
+ }
+
+ adds := []mutate.IndexAddendum{}
+
+ for _, m := range manifests {
+ // Keep the old descriptor (annotations and whatnot).
+ desc, err := partial.Descriptor(m)
+ if err != nil {
+ return nil, err
+ }
+
+ flattened, err := flattenChild(m, repo, use, o)
+ if err != nil {
+ return nil, err
+ }
+ desc.Size, err = flattened.Size()
+ if err != nil {
+ return nil, err
+ }
+ desc.Digest, err = flattened.Digest()
+ if err != nil {
+ return nil, err
+ }
+ adds = append(adds, mutate.IndexAddendum{
+ Add: flattened,
+ Descriptor: *desc,
+ })
+ }
+
+ idx := mutate.AppendManifests(empty.Index, adds...)
+
+ // Retain any annotations from the original index.
+ if len(m.Annotations) != 0 {
+ idx = mutate.Annotations(idx, m.Annotations).(v1.ImageIndex)
+ }
+
+ // This is stupid, but some registries get mad if you try to push OCI media types that reference docker media types.
+ mt, err := old.MediaType()
+ if err != nil {
+ return nil, err
+ }
+ idx = mutate.IndexMediaType(idx, mt)
+
+ return idx, nil
+}
+
+func flattenChild(old partial.Describable, repo name.Repository, use string, o crane.Options) (partial.Describable, error) {
+ if idx, ok := old.(v1.ImageIndex); ok {
+ return flattenIndex(idx, repo, use, o)
+ } else if img, ok := old.(v1.Image); ok {
+ return flattenImage(img, repo, use, o)
+ }
+
+ logs.Warn.Printf("can't flatten %T, skipping", old)
+ return old, nil
+}
+
+func flattenImage(old v1.Image, repo name.Repository, use string, o crane.Options) (partial.Describable, error) {
+ digest, err := old.Digest()
+ if err != nil {
+ return nil, fmt.Errorf("getting old digest: %w", err)
+ }
+ m, err := old.Manifest()
+ if err != nil {
+ return nil, fmt.Errorf("reading manifest: %w", err)
+ }
+
+ cf, err := old.ConfigFile()
+ if err != nil {
+ return nil, fmt.Errorf("getting config: %w", err)
+ }
+ cf = cf.DeepCopy()
+
+ oldHistory, err := json.Marshal(cf.History)
+ if err != nil {
+ return nil, fmt.Errorf("marshal history")
+ }
+
+ // Clear layer-specific config file information.
+ cf.RootFS.DiffIDs = []v1.Hash{}
+ cf.History = []v1.History{}
+
+ img, err := mutate.ConfigFile(empty.Image, cf)
+ if err != nil {
+ return nil, fmt.Errorf("mutating config: %w", err)
+ }
+
+ // TODO: Make compression configurable?
+ layer := stream.NewLayer(mutate.Extract(old), stream.WithCompressionLevel(gzip.BestCompression))
+ if err := remote.WriteLayer(repo, layer, o.Remote...); err != nil {
+ return nil, fmt.Errorf("uploading layer: %w", err)
+ }
+
+ img, err = mutate.Append(img, mutate.Addendum{
+ Layer: layer,
+ History: v1.History{
+ CreatedBy: fmt.Sprintf("%s flatten %s", use, digest),
+ Comment: string(oldHistory),
+ },
+ })
+ if err != nil {
+ return nil, fmt.Errorf("appending layers: %w", err)
+ }
+
+ // Retain any annotations from the original image.
+ if len(m.Annotations) != 0 {
+ img = mutate.Annotations(img, m.Annotations).(v1.Image)
+ }
+
+ return img, nil
+}
diff --git a/cmd/crane/cmd/index.go b/cmd/crane/cmd/index.go
new file mode 100644
index 0000000..8d4b425
--- /dev/null
+++ b/cmd/crane/cmd/index.go
@@ -0,0 +1,291 @@
+// Copyright 2023 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/match"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdIndex creates a new cobra.Command for the index subcommand.
+func NewCmdIndex(options *[]crane.Option) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "index",
+ Short: "Modify an image index.",
+ Args: cobra.ExactArgs(1),
+ Run: func(cmd *cobra.Command, _ []string) {
+ cmd.Usage()
+ },
+ }
+ cmd.AddCommand(NewCmdIndexFilter(options), NewCmdIndexAppend(options))
+ return cmd
+}
+
+// NewCmdIndexFilter creates a new cobra.Command for the index filter subcommand.
+func NewCmdIndexFilter(options *[]crane.Option) *cobra.Command {
+ var newTag string
+ platforms := &platformsValue{}
+
+ cmd := &cobra.Command{
+ Use: "filter",
+ Short: "Modifies a remote index by filtering based on platform.",
+ Example: ` # Filter out weird platforms from ubuntu, copy result to example.com/ubuntu
+ crane index filter ubuntu --platform linux/amd64 --platform linux/arm64 -t example.com/ubuntu
+
+ # Filter out any non-linux platforms, push to example.com/hello-world
+ crane index filter hello-world --platform linux -t example.com/hello-world
+
+ # Same as above, but in-place
+ crane index filter example.com/hello-world:some-tag --platform linux`,
+ Args: cobra.ExactArgs(1),
+ RunE: func(_ *cobra.Command, args []string) error {
+ o := crane.GetOptions(*options...)
+ baseRef := args[0]
+
+ ref, err := name.ParseReference(baseRef)
+ if err != nil {
+ return err
+ }
+ base, err := remote.Index(ref, o.Remote...)
+ if err != nil {
+ return fmt.Errorf("pulling %s: %w", baseRef, err)
+ }
+
+ idx := filterIndex(base, platforms.platforms)
+
+ digest, err := idx.Digest()
+ if err != nil {
+ return err
+ }
+
+ if newTag != "" {
+ ref, err = name.ParseReference(newTag)
+ if err != nil {
+ return fmt.Errorf("parsing reference %s: %w", newTag, err)
+ }
+ } else {
+ if _, ok := ref.(name.Digest); ok {
+ ref = ref.Context().Digest(digest.String())
+ }
+ }
+
+ if err := remote.WriteIndex(ref, idx, o.Remote...); err != nil {
+ return fmt.Errorf("pushing image %s: %w", newTag, err)
+ }
+ fmt.Println(ref.Context().Digest(digest.String()))
+ return nil
+ },
+ }
+ cmd.Flags().StringVarP(&newTag, "tag", "t", "", "Tag to apply to resulting image")
+
+ // Consider reusing the persistent flag for this, it's separate so we can have multiple values.
+ cmd.Flags().Var(platforms, "platform", "Specifies the platform(s) to keep from base in the form os/arch[/variant][:osversion][,<platform>] (e.g. linux/amd64).")
+
+ return cmd
+}
+
+// NewCmdIndexAppend creates a new cobra.Command for the index append subcommand.
+func NewCmdIndexAppend(options *[]crane.Option) *cobra.Command {
+ var baseRef, newTag string
+ var newManifests []string
+ var dockerEmptyBase, flatten bool
+
+ cmd := &cobra.Command{
+ Use: "append",
+ Short: "Append manifests to a remote index.",
+ Long: `This sub-command pushes an index based on an (optional) base index, with appended manifests.
+
+The platform for appended manifests is inferred from the config file or omitted if that is infeasible.`,
+ Example: ` # Append a windows hello-world image to ubuntu, push to example.com/hello-world:weird
+ crane index append ubuntu -m hello-world@sha256:87b9ca29151260634b95efb84d43b05335dc3ed36cc132e2b920dd1955342d20 -t example.com/hello-world:weird
+
+ # Create an index from scratch for etcd.
+ crane index append -m registry.k8s.io/etcd-amd64:3.4.9 -m registry.k8s.io/etcd-arm64:3.4.9 -t example.com/etcd`,
+ Args: cobra.MaximumNArgs(1),
+ RunE: func(_ *cobra.Command, args []string) error {
+ if len(args) == 1 {
+ baseRef = args[0]
+ }
+ o := crane.GetOptions(*options...)
+
+ var (
+ base v1.ImageIndex
+ err error
+ ref name.Reference
+ )
+
+ if baseRef == "" {
+ if newTag == "" {
+ return errors.New("at least one of --base or --tag must be specified")
+ }
+
+ logs.Warn.Printf("base unspecified, using empty index")
+ base = empty.Index
+ if dockerEmptyBase {
+ base = mutate.IndexMediaType(base, types.DockerManifestList)
+ }
+ } else {
+ ref, err = name.ParseReference(baseRef)
+ if err != nil {
+ return err
+ }
+ base, err = remote.Index(ref, o.Remote...)
+ if err != nil {
+ return fmt.Errorf("pulling %s: %w", baseRef, err)
+ }
+ }
+
+ adds := make([]mutate.IndexAddendum, 0, len(newManifests))
+
+ for _, m := range newManifests {
+ ref, err := name.ParseReference(m)
+ if err != nil {
+ return err
+ }
+ desc, err := remote.Get(ref, o.Remote...)
+ if err != nil {
+ return err
+ }
+ if desc.MediaType.IsImage() {
+ img, err := desc.Image()
+ if err != nil {
+ return err
+ }
+
+ cf, err := img.ConfigFile()
+ if err != nil {
+ return err
+ }
+ newDesc, err := partial.Descriptor(img)
+ if err != nil {
+ return err
+ }
+ newDesc.Platform = cf.Platform()
+ adds = append(adds, mutate.IndexAddendum{
+ Add: img,
+ Descriptor: *newDesc,
+ })
+ } else if desc.MediaType.IsIndex() {
+ idx, err := desc.ImageIndex()
+ if err != nil {
+ return err
+ }
+ if flatten {
+ im, err := idx.IndexManifest()
+ if err != nil {
+ return err
+ }
+ for _, child := range im.Manifests {
+ switch {
+ case child.MediaType.IsImage():
+ img, err := idx.Image(child.Digest)
+ if err != nil {
+ return err
+ }
+ adds = append(adds, mutate.IndexAddendum{
+ Add: img,
+ Descriptor: child,
+ })
+ case child.MediaType.IsIndex():
+ idx, err := idx.ImageIndex(child.Digest)
+ if err != nil {
+ return err
+ }
+ adds = append(adds, mutate.IndexAddendum{
+ Add: idx,
+ Descriptor: child,
+ })
+ default:
+ return fmt.Errorf("unexpected child %q with media type %q", child.Digest, child.MediaType)
+ }
+ }
+ } else {
+ adds = append(adds, mutate.IndexAddendum{
+ Add: idx,
+ })
+ }
+ } else {
+ return fmt.Errorf("saw unexpected MediaType %q for %q", desc.MediaType, m)
+ }
+ }
+
+ idx := mutate.AppendManifests(base, adds...)
+ digest, err := idx.Digest()
+ if err != nil {
+ return err
+ }
+
+ if newTag != "" {
+ ref, err = name.ParseReference(newTag)
+ if err != nil {
+ return fmt.Errorf("parsing reference %s: %w", newTag, err)
+ }
+ } else {
+ if _, ok := ref.(name.Digest); ok {
+ ref = ref.Context().Digest(digest.String())
+ }
+ }
+
+ if err := remote.WriteIndex(ref, idx, o.Remote...); err != nil {
+ return fmt.Errorf("pushing image %s: %w", newTag, err)
+ }
+ fmt.Println(ref.Context().Digest(digest.String()))
+ return nil
+ },
+ }
+ cmd.Flags().StringVarP(&newTag, "tag", "t", "", "Tag to apply to resulting image")
+ cmd.Flags().StringSliceVarP(&newManifests, "manifest", "m", []string{}, "References to manifests to append to the base index")
+ cmd.Flags().BoolVar(&dockerEmptyBase, "docker-empty-base", false, "If true, empty base index will have Docker media types instead of OCI")
+ cmd.Flags().BoolVar(&flatten, "flatten", true, "If true, appending an index will append each of its children rather than the index itself")
+
+ return cmd
+}
+
+func filterIndex(idx v1.ImageIndex, platforms []v1.Platform) v1.ImageIndex {
+ matcher := not(satisfiesPlatforms(platforms))
+ return mutate.RemoveManifests(idx, matcher)
+}
+
+func satisfiesPlatforms(platforms []v1.Platform) match.Matcher {
+ return func(desc v1.Descriptor) bool {
+ if desc.Platform == nil {
+ return false
+ }
+ for _, p := range platforms {
+ if desc.Platform.Satisfies(p) {
+ return true
+ }
+ }
+ return false
+ }
+}
+
+func not(in match.Matcher) match.Matcher {
+ return func(desc v1.Descriptor) bool {
+ return !in(desc)
+ }
+}
diff --git a/cmd/crane/cmd/list.go b/cmd/crane/cmd/list.go
new file mode 100644
index 0000000..3902ccd
--- /dev/null
+++ b/cmd/crane/cmd/list.go
@@ -0,0 +1,62 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdList creates a new cobra.Command for the ls subcommand.
+func NewCmdList(options *[]crane.Option) *cobra.Command {
+ var fullRef, omitDigestTags bool
+ cmd := &cobra.Command{
+ Use: "ls REPO",
+ Short: "List the tags in a repo",
+ Args: cobra.ExactArgs(1),
+ RunE: func(_ *cobra.Command, args []string) error {
+ repo := args[0]
+ tags, err := crane.ListTags(repo, *options...)
+ if err != nil {
+ return fmt.Errorf("reading tags for %s: %w", repo, err)
+ }
+
+ r, err := name.NewRepository(repo)
+ if err != nil {
+ return err
+ }
+
+ for _, tag := range tags {
+ if omitDigestTags && strings.HasPrefix(tag, "sha256-") {
+ continue
+ }
+
+ if fullRef {
+ fmt.Println(r.Tag(tag))
+ } else {
+ fmt.Println(tag)
+ }
+ }
+ return nil
+ },
+ }
+ cmd.Flags().BoolVar(&fullRef, "full-ref", false, "(Optional) if true, print the full image reference")
+ cmd.Flags().BoolVar(&omitDigestTags, "omit-digest-tags", false, "(Optional), if true, omit digest tags (e.g., ':sha256-...')")
+ return cmd
+}
diff --git a/cmd/crane/cmd/manifest.go b/cmd/crane/cmd/manifest.go
new file mode 100644
index 0000000..d9ef7fd
--- /dev/null
+++ b/cmd/crane/cmd/manifest.go
@@ -0,0 +1,40 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdManifest creates a new cobra.Command for the manifest subcommand.
+func NewCmdManifest(options *[]crane.Option) *cobra.Command {
+ return &cobra.Command{
+ Use: "manifest IMAGE",
+ Short: "Get the manifest of an image",
+ Args: cobra.ExactArgs(1),
+ RunE: func(_ *cobra.Command, args []string) error {
+ src := args[0]
+ manifest, err := crane.Manifest(src, *options...)
+ if err != nil {
+ return fmt.Errorf("fetching manifest %s: %w", src, err)
+ }
+ fmt.Print(string(manifest))
+ return nil
+ },
+ }
+}
diff --git a/cmd/crane/cmd/mutate.go b/cmd/crane/cmd/mutate.go
new file mode 100644
index 0000000..a99def0
--- /dev/null
+++ b/cmd/crane/cmd/mutate.go
@@ -0,0 +1,207 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdMutate creates a new cobra.Command for the mutate subcommand.
+func NewCmdMutate(options *[]crane.Option) *cobra.Command {
+ var labels map[string]string
+ var annotations map[string]string
+ var entrypoint, cmd []string
+ var envVars map[string]string
+ var newLayers []string
+ var outFile string
+ var newRef string
+ var newRepo string
+ var user string
+
+ mutateCmd := &cobra.Command{
+ Use: "mutate",
+ Short: "Modify image labels and annotations. The container must be pushed to a registry, and the manifest is updated there.",
+ Args: cobra.ExactArgs(1),
+ RunE: func(_ *cobra.Command, args []string) error {
+ // Pull image and get config.
+ ref := args[0]
+
+ if len(annotations) != 0 {
+ desc, err := crane.Head(ref, *options...)
+ if err != nil {
+ return err
+ }
+ if desc.MediaType.IsIndex() {
+ return errors.New("mutating annotations on an index is not yet supported")
+ }
+ }
+
+ if newRepo != "" && newRef != "" {
+ return errors.New("repository can't be set when a tag is specified")
+ }
+
+ img, err := crane.Pull(ref, *options...)
+ if err != nil {
+ return fmt.Errorf("pulling %s: %w", ref, err)
+ }
+ if len(newLayers) != 0 {
+ img, err = crane.Append(img, newLayers...)
+ if err != nil {
+ return fmt.Errorf("appending %v: %w", newLayers, err)
+ }
+ }
+ cfg, err := img.ConfigFile()
+ if err != nil {
+ return err
+ }
+ cfg = cfg.DeepCopy()
+
+ // Set labels.
+ if cfg.Config.Labels == nil {
+ cfg.Config.Labels = map[string]string{}
+ }
+
+ if err := validateKeyVals(labels); err != nil {
+ return err
+ }
+
+ for k, v := range labels {
+ cfg.Config.Labels[k] = v
+ }
+
+ if err := validateKeyVals(annotations); err != nil {
+ return err
+ }
+
+ // set envvars if specified
+ if err := setEnvVars(cfg, envVars); err != nil {
+ return err
+ }
+
+ // Set entrypoint.
+ if len(entrypoint) > 0 {
+ cfg.Config.Entrypoint = entrypoint
+ cfg.Config.Cmd = nil // This matches Docker's behavior.
+ }
+
+ // Set cmd.
+ if len(cmd) > 0 {
+ cfg.Config.Cmd = cmd
+ }
+
+ // Set user.
+ if len(user) > 0 {
+ cfg.Config.User = user
+ }
+
+ // Mutate and write image.
+ img, err = mutate.Config(img, cfg.Config)
+ if err != nil {
+ return fmt.Errorf("mutating config: %w", err)
+ }
+
+ img = mutate.Annotations(img, annotations).(v1.Image)
+
+ // If the new ref isn't provided, write over the original image.
+ // If that ref was provided by digest (e.g., output from
+ // another crane command), then strip that and push the
+ // mutated image by digest instead.
+ if newRepo != "" {
+ newRef = newRepo
+ } else if newRef == "" {
+ newRef = ref
+ }
+ digest, err := img.Digest()
+ if err != nil {
+ return fmt.Errorf("digesting new image: %w", err)
+ }
+ if outFile != "" {
+ if err := crane.Save(img, newRef, outFile); err != nil {
+ return fmt.Errorf("writing output %q: %w", outFile, err)
+ }
+ } else {
+ r, err := name.ParseReference(newRef)
+ if err != nil {
+ return fmt.Errorf("parsing %s: %w", newRef, err)
+ }
+ if _, ok := r.(name.Digest); ok || newRepo != "" {
+ newRef = r.Context().Digest(digest.String()).String()
+ }
+ if err := crane.Push(img, newRef, *options...); err != nil {
+ return fmt.Errorf("pushing %s: %w", newRef, err)
+ }
+ fmt.Println(r.Context().Digest(digest.String()))
+ }
+ return nil
+ },
+ }
+ mutateCmd.Flags().StringToStringVarP(&annotations, "annotation", "a", nil, "New annotations to add")
+ mutateCmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "New labels to add")
+ mutateCmd.Flags().StringToStringVarP(&envVars, "env", "e", nil, "New envvar to add")
+ mutateCmd.Flags().StringSliceVar(&entrypoint, "entrypoint", nil, "New entrypoint to set")
+ mutateCmd.Flags().StringSliceVar(&cmd, "cmd", nil, "New cmd to set")
+ mutateCmd.Flags().StringVar(&newRepo, "repo", "", "Repository to push the mutated image to. If provided, push by digest to this repository.")
+ mutateCmd.Flags().StringVarP(&newRef, "tag", "t", "", "New tag reference to apply to mutated image. If not provided, push by digest to the original image repository.")
+ mutateCmd.Flags().StringVarP(&outFile, "output", "o", "", "Path to new tarball of resulting image")
+ mutateCmd.Flags().StringSliceVar(&newLayers, "append", []string{}, "Path to tarball to append to image")
+ mutateCmd.Flags().StringVarP(&user, "user", "u", "", "New user to set")
+ return mutateCmd
+}
+
+// validateKeyVals ensures no values are empty, returns error if they are
+func validateKeyVals(kvPairs map[string]string) error {
+ for label, value := range kvPairs {
+ if value == "" {
+ return fmt.Errorf("parsing label %q, value is empty", label)
+ }
+ }
+ return nil
+}
+
+// setEnvVars override envvars in a config
+func setEnvVars(cfg *v1.ConfigFile, envVars map[string]string) error {
+ newEnv := make([]string, 0, len(cfg.Config.Env))
+ for _, old := range cfg.Config.Env {
+ split := strings.SplitN(old, "=", 2)
+ if len(split) != 2 {
+ return fmt.Errorf("invalid key value pair in config: %s", old)
+ }
+ // keep order so override if specified again
+ oldKey := split[0]
+ if v, ok := envVars[oldKey]; ok {
+ newEnv = append(newEnv, fmt.Sprintf("%s=%s", oldKey, v))
+ delete(envVars, oldKey)
+ } else {
+ newEnv = append(newEnv, old)
+ }
+ }
+ isWindows := cfg.OS == "windows"
+ for k, v := range envVars {
+ if isWindows {
+ k = strings.ToUpper(k)
+ }
+ newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, v))
+ }
+ cfg.Config.Env = newEnv
+ return nil
+}
diff --git a/cmd/crane/cmd/optimize.go b/cmd/crane/cmd/optimize.go
new file mode 100644
index 0000000..89c4706
--- /dev/null
+++ b/cmd/crane/cmd/optimize.go
@@ -0,0 +1,42 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdOptimize creates a new cobra.Command for the optimize subcommand.
+func NewCmdOptimize(options *[]crane.Option) *cobra.Command {
+ var files []string
+
+ cmd := &cobra.Command{
+ Use: "optimize SRC DST",
+ Hidden: true,
+ Aliases: []string{"opt"},
+ Short: "Optimize a remote container image from src to dst",
+ Args: cobra.ExactArgs(2),
+ RunE: func(_ *cobra.Command, args []string) error {
+ src, dst := args[0], args[1]
+ return crane.Optimize(src, dst, files, *options...)
+ },
+ }
+
+ cmd.Flags().StringSliceVar(&files, "prioritize", nil,
+ "The list of files to prioritize in the optimized image.")
+
+ return cmd
+}
diff --git a/cmd/crane/cmd/pull.go b/cmd/crane/cmd/pull.go
new file mode 100644
index 0000000..41c6e95
--- /dev/null
+++ b/cmd/crane/cmd/pull.go
@@ -0,0 +1,138 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/cache"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/layout"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdPull creates a new cobra.Command for the pull subcommand.
+func NewCmdPull(options *[]crane.Option) *cobra.Command {
+ var (
+ cachePath, format string
+ annotateRef bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "pull IMAGE TARBALL",
+ Short: "Pull remote images by reference and store their contents locally",
+ Args: cobra.MinimumNArgs(2),
+ RunE: func(_ *cobra.Command, args []string) error {
+ imageMap := map[string]v1.Image{}
+ indexMap := map[string]v1.ImageIndex{}
+ srcList, path := args[:len(args)-1], args[len(args)-1]
+ o := crane.GetOptions(*options...)
+ for _, src := range srcList {
+ ref, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing reference %q: %w", src, err)
+ }
+
+ rmt, err := remote.Get(ref, o.Remote...)
+ if err != nil {
+ return err
+ }
+
+ // If we're writing an index to a layout and --platform hasn't been set,
+ // pull the entire index, not just a child image.
+ if format == "oci" && rmt.MediaType.IsIndex() && o.Platform == nil {
+ idx, err := rmt.ImageIndex()
+ if err != nil {
+ return err
+ }
+ indexMap[src] = idx
+ continue
+ }
+
+ img, err := rmt.Image()
+ if err != nil {
+ return err
+ }
+ if cachePath != "" {
+ img = cache.Image(img, cache.NewFilesystemCache(cachePath))
+ }
+ imageMap[src] = img
+ }
+
+ switch format {
+ case "tarball":
+ if err := crane.MultiSave(imageMap, path); err != nil {
+ return fmt.Errorf("saving tarball %s: %w", path, err)
+ }
+ case "legacy":
+ if err := crane.MultiSaveLegacy(imageMap, path); err != nil {
+ return fmt.Errorf("saving legacy tarball %s: %w", path, err)
+ }
+ case "oci":
+ // Don't use crane.MultiSaveOCI so we can control annotations.
+ p, err := layout.FromPath(path)
+ if err != nil {
+ p, err = layout.Write(path, empty.Index)
+ if err != nil {
+ return err
+ }
+ }
+ for ref, img := range imageMap {
+ opts := []layout.Option{}
+ if annotateRef {
+ parsed, err := name.ParseReference(ref, o.Name...)
+ if err != nil {
+ return err
+ }
+ opts = append(opts, layout.WithAnnotations(map[string]string{
+ "org.opencontainers.image.ref.name": parsed.Name(),
+ }))
+ }
+ if err = p.AppendImage(img, opts...); err != nil {
+ return err
+ }
+ }
+
+ for ref, idx := range indexMap {
+ opts := []layout.Option{}
+ if annotateRef {
+ parsed, err := name.ParseReference(ref, o.Name...)
+ if err != nil {
+ return err
+ }
+ opts = append(opts, layout.WithAnnotations(map[string]string{
+ "org.opencontainers.image.ref.name": parsed.Name(),
+ }))
+ }
+ if err := p.AppendIndex(idx, opts...); err != nil {
+ return err
+ }
+ }
+ default:
+ return fmt.Errorf("unexpected --format: %q (valid values are: tarball, legacy, and oci)", format)
+ }
+ return nil
+ },
+ }
+ cmd.Flags().StringVarP(&cachePath, "cache_path", "c", "", "Path to cache image layers")
+ cmd.Flags().StringVar(&format, "format", "tarball", fmt.Sprintf("Format in which to save images (%q, %q, or %q)", "tarball", "legacy", "oci"))
+ cmd.Flags().BoolVar(&annotateRef, "annotate-ref", false, "Preserves image reference used to pull as an annotation when used with --format=oci")
+
+ return cmd
+}
diff --git a/cmd/crane/cmd/push.go b/cmd/crane/cmd/push.go
new file mode 100644
index 0000000..887621e
--- /dev/null
+++ b/cmd/crane/cmd/push.go
@@ -0,0 +1,126 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/layout"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdPush creates a new cobra.Command for the push subcommand.
+func NewCmdPush(options *[]crane.Option) *cobra.Command {
+ index := false
+ imageRefs := ""
+ cmd := &cobra.Command{
+ Use: "push PATH IMAGE",
+ Short: "Push local image contents to a remote registry",
+ Long: `If the PATH is a directory, it will be read as an OCI image layout. Otherwise, PATH is assumed to be a docker-style tarball.`,
+ Args: cobra.ExactArgs(2),
+ RunE: func(_ *cobra.Command, args []string) error {
+ path, tag := args[0], args[1]
+
+ img, err := loadImage(path, index)
+ if err != nil {
+ return err
+ }
+
+ o := crane.GetOptions(*options...)
+ ref, err := name.ParseReference(tag, o.Name...)
+ if err != nil {
+ return err
+ }
+ var h v1.Hash
+ switch t := img.(type) {
+ case v1.Image:
+ if err := remote.Write(ref, t, o.Remote...); err != nil {
+ return err
+ }
+ if h, err = t.Digest(); err != nil {
+ return err
+ }
+ case v1.ImageIndex:
+ if err := remote.WriteIndex(ref, t, o.Remote...); err != nil {
+ return err
+ }
+ if h, err = t.Digest(); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("cannot push type (%T) to registry", img)
+ }
+
+ digest := ref.Context().Digest(h.String())
+ if imageRefs != "" {
+ return os.WriteFile(imageRefs, []byte(digest.String()), 0600)
+ }
+ // TODO(mattmoor): think about printing the digest to standard out
+ // to facilitate command composition similar to ko build.
+
+ return nil
+ },
+ }
+ cmd.Flags().BoolVar(&index, "index", false, "push a collection of images as a single index, currently required if PATH contains multiple images")
+ cmd.Flags().StringVar(&imageRefs, "image-refs", "", "path to file where a list of the published image references will be written")
+ return cmd
+}
+
+func loadImage(path string, index bool) (partial.WithRawManifest, error) {
+ stat, err := os.Stat(path)
+ if err != nil {
+ return nil, err
+ }
+
+ if !stat.IsDir() {
+ img, err := crane.Load(path)
+ if err != nil {
+ return nil, fmt.Errorf("loading %s as tarball: %w", path, err)
+ }
+ return img, nil
+ }
+
+ l, err := layout.ImageIndexFromPath(path)
+ if err != nil {
+ return nil, fmt.Errorf("loading %s as OCI layout: %w", path, err)
+ }
+
+ if index {
+ return l, nil
+ }
+
+ m, err := l.IndexManifest()
+ if err != nil {
+ return nil, err
+ }
+ if len(m.Manifests) != 1 {
+ return nil, fmt.Errorf("layout contains %d entries, consider --index", len(m.Manifests))
+ }
+
+ desc := m.Manifests[0]
+ if desc.MediaType.IsImage() {
+ return l.Image(desc.Digest)
+ } else if desc.MediaType.IsIndex() {
+ return l.ImageIndex(desc.Digest)
+ }
+
+ return nil, fmt.Errorf("layout contains non-image (mediaType: %q), consider --index", desc.MediaType)
+}
diff --git a/cmd/crane/cmd/rebase.go b/cmd/crane/cmd/rebase.go
new file mode 100644
index 0000000..43f21b5
--- /dev/null
+++ b/cmd/crane/cmd/rebase.go
@@ -0,0 +1,210 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "log"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdRebase creates a new cobra.Command for the rebase subcommand.
+func NewCmdRebase(options *[]crane.Option) *cobra.Command {
+ var orig, oldBase, newBase, rebased string
+
+ rebaseCmd := &cobra.Command{
+ Use: "rebase",
+ Short: "Rebase an image onto a new base image",
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if orig == "" {
+ orig = args[0]
+ } else if len(args) != 0 || args[0] != "" {
+ return fmt.Errorf("cannot use --original with positional argument")
+ }
+
+ // If the new ref isn't provided, write over the original image.
+ // If that ref was provided by digest (e.g., output from
+ // another crane command), then strip that and push the
+ // mutated image by digest instead.
+ if rebased == "" {
+ rebased = orig
+ }
+
+ // Stupid hack to support insecure flag.
+ nameOpt := []name.Option{}
+ if ok, err := cmd.Parent().PersistentFlags().GetBool("insecure"); err != nil {
+ log.Fatalf("flag problems: %v", err)
+ } else if ok {
+ nameOpt = append(nameOpt, name.Insecure)
+ }
+ r, err := name.ParseReference(rebased, nameOpt...)
+ if err != nil {
+ log.Fatalf("parsing %s: %v", rebased, err)
+ }
+
+ desc, err := crane.Head(orig, *options...)
+ if err != nil {
+ log.Fatalf("checking %s: %v", orig, err)
+ }
+ if !cmd.Parent().PersistentFlags().Changed("platform") && desc.MediaType.IsIndex() {
+ log.Fatalf("rebasing an index is not yet supported")
+ }
+
+ origImg, err := crane.Pull(orig, *options...)
+ if err != nil {
+ return err
+ }
+ origMf, err := origImg.Manifest()
+ if err != nil {
+ return err
+ }
+ anns := origMf.Annotations
+ if newBase == "" && anns != nil {
+ newBase = anns[specsv1.AnnotationBaseImageName]
+ }
+ if newBase == "" {
+ return errors.New("could not determine new base image from annotations")
+ }
+ newBaseRef, err := name.ParseReference(newBase)
+ if err != nil {
+ return err
+ }
+ if oldBase == "" && anns != nil {
+ oldBaseDigest := anns[specsv1.AnnotationBaseImageDigest]
+ oldBase = newBaseRef.Context().Digest(oldBaseDigest).String()
+ }
+ if oldBase == "" {
+ return errors.New("could not determine old base image by digest from annotations")
+ }
+
+ rebasedImg, err := rebaseImage(origImg, oldBase, newBase, *options...)
+ if err != nil {
+ return fmt.Errorf("rebasing image: %w", err)
+ }
+
+ rebasedDigest, err := rebasedImg.Digest()
+ if err != nil {
+ return fmt.Errorf("digesting new image: %w", err)
+ }
+ origDigest, err := origImg.Digest()
+ if err != nil {
+ return err
+ }
+ if rebasedDigest == origDigest {
+ logs.Warn.Println("rebasing was no-op")
+ }
+
+ if _, ok := r.(name.Digest); ok {
+ rebased = r.Context().Digest(rebasedDigest.String()).String()
+ }
+ logs.Progress.Println("pushing rebased image as", rebased)
+ if err := crane.Push(rebasedImg, rebased, *options...); err != nil {
+ log.Fatalf("pushing %s: %v", rebased, err)
+ }
+
+ fmt.Println(r.Context().Digest(rebasedDigest.String()))
+ return nil
+ },
+ }
+ rebaseCmd.Flags().StringVar(&orig, "original", "", "Original image to rebase (DEPRECATED: use positional arg instead)")
+ rebaseCmd.Flags().StringVar(&oldBase, "old_base", "", "Old base image to remove")
+ rebaseCmd.Flags().StringVar(&newBase, "new_base", "", "New base image to insert")
+ rebaseCmd.Flags().StringVar(&rebased, "rebased", "", "Tag to apply to rebased image (DEPRECATED: use --tag)")
+ rebaseCmd.Flags().StringVarP(&rebased, "tag", "t", "", "Tag to apply to rebased image")
+ return rebaseCmd
+}
+
+// rebaseImage parses the references and uses them to perform a rebase on the
+// original image.
+//
+// If oldBase or newBase are "", rebaseImage attempts to derive them using
+// annotations in the original image. If those annotations are not found,
+// rebaseImage returns an error.
+//
+// If rebasing is successful, base image annotations are set on the resulting
+// image to facilitate implicit rebasing next time.
+func rebaseImage(orig v1.Image, oldBase, newBase string, opt ...crane.Option) (v1.Image, error) {
+ m, err := orig.Manifest()
+ if err != nil {
+ return nil, err
+ }
+ if newBase == "" && m.Annotations != nil {
+ newBase = m.Annotations[specsv1.AnnotationBaseImageName]
+ if newBase != "" {
+ logs.Debug.Printf("Detected new base from %q annotation: %s", specsv1.AnnotationBaseImageName, newBase)
+ }
+ }
+ if newBase == "" {
+ return nil, fmt.Errorf("either new base or %q annotation is required", specsv1.AnnotationBaseImageName)
+ }
+ newBaseImg, err := crane.Pull(newBase, opt...)
+ if err != nil {
+ return nil, err
+ }
+
+ if oldBase == "" && m.Annotations != nil {
+ oldBase = m.Annotations[specsv1.AnnotationBaseImageDigest]
+ if oldBase != "" {
+ newBaseRef, err := name.ParseReference(newBase)
+ if err != nil {
+ return nil, err
+ }
+
+ oldBase = newBaseRef.Context().Digest(oldBase).String()
+ logs.Debug.Printf("Detected old base from %q annotation: %s", specsv1.AnnotationBaseImageDigest, oldBase)
+ }
+ }
+ if oldBase == "" {
+ return nil, fmt.Errorf("either old base or %q annotation is required", specsv1.AnnotationBaseImageDigest)
+ }
+
+ oldBaseImg, err := crane.Pull(oldBase, opt...)
+ if err != nil {
+ return nil, err
+ }
+
+ // NB: if newBase is an index, we need to grab the index's digest to
+ // annotate the resulting image, even though we pull the
+ // platform-specific image to rebase.
+ // crane.Digest will pull a platform-specific image, so use crane.Head
+ // here instead.
+ newBaseDesc, err := crane.Head(newBase, opt...)
+ if err != nil {
+ return nil, err
+ }
+ newBaseDigest := newBaseDesc.Digest.String()
+
+ rebased, err := mutate.Rebase(orig, oldBaseImg, newBaseImg)
+ if err != nil {
+ return nil, err
+ }
+
+ // Update base image annotations for the new image manifest.
+ logs.Debug.Printf("Setting annotation %q: %q", specsv1.AnnotationBaseImageDigest, newBaseDigest)
+ logs.Debug.Printf("Setting annotation %q: %q", specsv1.AnnotationBaseImageName, newBase)
+ return mutate.Annotations(rebased, map[string]string{
+ specsv1.AnnotationBaseImageDigest: newBaseDigest,
+ specsv1.AnnotationBaseImageName: newBase,
+ }).(v1.Image), nil
+}
diff --git a/cmd/crane/cmd/root.go b/cmd/crane/cmd/root.go
new file mode 100644
index 0000000..95412cc
--- /dev/null
+++ b/cmd/crane/cmd/root.go
@@ -0,0 +1,148 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ "github.com/docker/cli/cli/config"
+ "github.com/google/go-containerregistry/internal/cmd"
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/spf13/cobra"
+)
+
+const (
+ use = "crane"
+ short = "Crane is a tool for managing container images"
+)
+
+var Root = New(use, short, []crane.Option{})
+
+// New returns a top-level command for crane. This is mostly exposed
+// to share code with gcrane.
+func New(use, short string, options []crane.Option) *cobra.Command {
+ verbose := false
+ insecure := false
+ ndlayers := false
+ platform := &platformValue{}
+
+ root := &cobra.Command{
+ Use: use,
+ Short: short,
+ RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Usage() },
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ PersistentPreRun: func(cmd *cobra.Command, args []string) {
+ options = append(options, crane.WithContext(cmd.Context()))
+ // TODO(jonjohnsonjr): crane.Verbose option?
+ if verbose {
+ logs.Debug.SetOutput(os.Stderr)
+ }
+ if insecure {
+ options = append(options, crane.Insecure)
+ }
+ if ndlayers {
+ options = append(options, crane.WithNondistributable())
+ }
+ if Version != "" {
+ binary := "crane"
+ if len(os.Args[0]) != 0 {
+ binary = filepath.Base(os.Args[0])
+ }
+ options = append(options, crane.WithUserAgent(fmt.Sprintf("%s/%s", binary, Version)))
+ }
+
+ options = append(options, crane.WithPlatform(platform.platform))
+
+ transport := remote.DefaultTransport.(*http.Transport).Clone()
+ transport.TLSClientConfig = &tls.Config{
+ InsecureSkipVerify: insecure, //nolint: gosec
+ }
+
+ var rt http.RoundTripper = transport
+
+ // Add any http headers if they are set in the config file.
+ cf, err := config.Load(os.Getenv("DOCKER_CONFIG"))
+ if err != nil {
+ logs.Debug.Printf("failed to read config file: %v", err)
+ } else if len(cf.HTTPHeaders) != 0 {
+ rt = &headerTransport{
+ inner: rt,
+ httpHeaders: cf.HTTPHeaders,
+ }
+ }
+
+ options = append(options, crane.WithTransport(rt))
+ },
+ }
+
+ root.AddCommand(
+ NewCmdAppend(&options),
+ NewCmdAuth(options, "crane", "auth"),
+ NewCmdBlob(&options),
+ NewCmdCatalog(&options, "crane"),
+ NewCmdConfig(&options),
+ NewCmdCopy(&options),
+ NewCmdDelete(&options),
+ NewCmdDigest(&options),
+ cmd.NewCmdEdit(&options),
+ NewCmdExport(&options),
+ NewCmdFlatten(&options),
+ NewCmdIndex(&options),
+ NewCmdList(&options),
+ NewCmdManifest(&options),
+ NewCmdMutate(&options),
+ NewCmdOptimize(&options),
+ NewCmdPull(&options),
+ NewCmdPush(&options),
+ NewCmdRebase(&options),
+ NewCmdTag(&options),
+ NewCmdValidate(&options),
+ NewCmdVersion(),
+ newCmdRegistry(),
+ )
+
+ root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logs")
+ root.PersistentFlags().BoolVar(&insecure, "insecure", false, "Allow image references to be fetched without TLS")
+ root.PersistentFlags().BoolVar(&ndlayers, "allow-nondistributable-artifacts", false, "Allow pushing non-distributable (foreign) layers")
+ root.PersistentFlags().Var(platform, "platform", "Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64).")
+
+ return root
+}
+
+// headerTransport sets headers on outgoing requests.
+type headerTransport struct {
+ httpHeaders map[string]string
+ inner http.RoundTripper
+}
+
+// RoundTrip implements http.RoundTripper.
+func (ht *headerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
+ for k, v := range ht.httpHeaders {
+ if http.CanonicalHeaderKey(k) == "User-Agent" {
+ // Docker sets this, which is annoying, since we're not docker.
+ // We might want to revisit completely ignoring this.
+ continue
+ }
+ in.Header.Set(k, v)
+ }
+ return ht.inner.RoundTrip(in)
+}
diff --git a/cmd/crane/cmd/serve.go b/cmd/crane/cmd/serve.go
new file mode 100644
index 0000000..5b11153
--- /dev/null
+++ b/cmd/crane/cmd/serve.go
@@ -0,0 +1,84 @@
+// Copyright 2023 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/spf13/cobra"
+
+ "github.com/google/go-containerregistry/pkg/registry"
+)
+
+func newCmdRegistry() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "registry",
+ }
+ cmd.AddCommand(newCmdServe())
+ return cmd
+}
+
+func newCmdServe() *cobra.Command {
+ return &cobra.Command{
+ Use: "serve",
+ Short: "Serve an in-memory registry implementation",
+ Long: `This sub-command serves an in-memory registry implementation on port :8080 (or $PORT)
+
+The command blocks while the server accepts pushes and pulls.
+
+Contents are only stored in memory, and when the process exits, pushed data is lost.`,
+ Args: cobra.NoArgs,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ ctx := cmd.Context()
+
+ port := os.Getenv("PORT")
+ if port == "" {
+ port = "0"
+ }
+ listener, err := net.Listen("tcp", "localhost:"+port)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ porti := listener.Addr().(*net.TCPAddr).Port
+ port = fmt.Sprintf("%d", porti)
+
+ s := &http.Server{
+ ReadHeaderTimeout: 5 * time.Second, // prevent slowloris, quiet linter
+ Handler: registry.New(),
+ }
+ log.Printf("serving on port %s", port)
+
+ errCh := make(chan error)
+ go func() { errCh <- s.Serve(listener) }()
+
+ <-ctx.Done()
+ log.Println("shutting down...")
+ if err := s.Shutdown(ctx); err != nil {
+ return err
+ }
+
+ if err := <-errCh; !errors.Is(err, http.ErrServerClosed) {
+ return err
+ }
+ return nil
+ },
+ }
+}
diff --git a/cmd/crane/cmd/tag.go b/cmd/crane/cmd/tag.go
new file mode 100644
index 0000000..9af803a
--- /dev/null
+++ b/cmd/crane/cmd/tag.go
@@ -0,0 +1,44 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdTag creates a new cobra.Command for the tag subcommand.
+func NewCmdTag(options *[]crane.Option) *cobra.Command {
+ return &cobra.Command{
+ Use: "tag IMG TAG",
+ Short: "Efficiently tag a remote image",
+ Long: `This differs slightly from the "copy" command in a couple subtle ways:
+
+1. You don't have to specify the entire repository for the tag you're adding. For example, these two commands are functionally equivalent:
+` + "```" + `
+crane cp registry.example.com/library/ubuntu:v0 registry.example.com/library/ubuntu:v1
+crane tag registry.example.com/library/ubuntu:v0 v1
+` + "```" + `
+
+2. We can skip layer existence checks because we know the manifest already exists. This makes "tag" slightly faster than "copy".`,
+ Example: `# Add a v1 tag to ubuntu
+crane tag ubuntu v1`,
+ Args: cobra.ExactArgs(2),
+ RunE: func(_ *cobra.Command, args []string) error {
+ img, tag := args[0], args[1]
+ return crane.Tag(img, tag, *options...)
+ },
+ }
+}
diff --git a/cmd/crane/cmd/util.go b/cmd/crane/cmd/util.go
new file mode 100644
index 0000000..f4ff651
--- /dev/null
+++ b/cmd/crane/cmd/util.go
@@ -0,0 +1,86 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "strings"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+type platformsValue struct {
+ platforms []v1.Platform
+}
+
+func (ps *platformsValue) Set(platform string) error {
+ if ps.platforms == nil {
+ ps.platforms = []v1.Platform{}
+ }
+ p, err := parsePlatform(platform)
+ if err != nil {
+ return err
+ }
+ pv := platformValue{p}
+ ps.platforms = append(ps.platforms, *pv.platform)
+ return nil
+}
+
+func (ps *platformsValue) String() string {
+ ss := make([]string, 0, len(ps.platforms))
+ for _, p := range ps.platforms {
+ ss = append(ss, p.String())
+ }
+ return strings.Join(ss, ",")
+}
+
+func (ps *platformsValue) Type() string {
+ return "platform(s)"
+}
+
+type platformValue struct {
+ platform *v1.Platform
+}
+
+func (pv *platformValue) Set(platform string) error {
+ p, err := parsePlatform(platform)
+ if err != nil {
+ return err
+ }
+ pv.platform = p
+ return nil
+}
+
+func (pv *platformValue) String() string {
+ return platformToString(pv.platform)
+}
+
+func (pv *platformValue) Type() string {
+ return "platform"
+}
+
+func platformToString(p *v1.Platform) string {
+ if p == nil {
+ return "all"
+ }
+ return p.String()
+}
+
+func parsePlatform(platform string) (*v1.Platform, error) {
+ if platform == "all" {
+ return nil, nil
+ }
+
+ return v1.ParsePlatform(platform)
+}
diff --git a/cmd/crane/cmd/validate.go b/cmd/crane/cmd/validate.go
new file mode 100644
index 0000000..4a4acbd
--- /dev/null
+++ b/cmd/crane/cmd/validate.go
@@ -0,0 +1,73 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdValidate creates a new cobra.Command for the validate subcommand.
+func NewCmdValidate(options *[]crane.Option) *cobra.Command {
+ var (
+ tarballPath, remoteRef string
+ fast bool
+ )
+
+ validateCmd := &cobra.Command{
+ Use: "validate",
+ Short: "Validate that an image is well-formed",
+ Args: cobra.ExactArgs(0),
+ RunE: func(_ *cobra.Command, args []string) error {
+ for flag, maker := range map[string]func(string, ...crane.Option) (v1.Image, error){
+ tarballPath: makeTarball,
+ remoteRef: crane.Pull,
+ } {
+ if flag == "" {
+ continue
+ }
+ img, err := maker(flag, *options...)
+ if err != nil {
+ return fmt.Errorf("failed to read image %s: %w", flag, err)
+ }
+
+ opt := []validate.Option{}
+ if fast {
+ opt = append(opt, validate.Fast)
+ }
+ if err := validate.Image(img, opt...); err != nil {
+ fmt.Printf("FAIL: %s: %v\n", flag, err)
+ return err
+ }
+ fmt.Printf("PASS: %s\n", flag)
+ }
+ return nil
+ },
+ }
+ validateCmd.Flags().StringVar(&tarballPath, "tarball", "", "Path to tarball to validate")
+ validateCmd.Flags().StringVar(&remoteRef, "remote", "", "Name of remote image to validate")
+ validateCmd.Flags().BoolVar(&fast, "fast", false, "Skip downloading/digesting layers")
+
+ return validateCmd
+}
+
+func makeTarball(path string, _ ...crane.Option) (v1.Image, error) {
+ return tarball.ImageFromPath(path, nil)
+}
diff --git a/cmd/crane/cmd/version.go b/cmd/crane/cmd/version.go
new file mode 100644
index 0000000..b906a5d
--- /dev/null
+++ b/cmd/crane/cmd/version.go
@@ -0,0 +1,56 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+ "runtime/debug"
+
+ "github.com/spf13/cobra"
+)
+
+// Version can be set via:
+// -ldflags="-X 'github.com/google/go-containerregistry/cmd/crane/cmd.Version=$TAG'"
+var Version string
+
+func init() {
+ if Version == "" {
+ i, ok := debug.ReadBuildInfo()
+ if !ok {
+ return
+ }
+ Version = i.Main.Version
+ }
+}
+
+// NewCmdVersion creates a new cobra.Command for the version subcommand.
+func NewCmdVersion() *cobra.Command {
+ return &cobra.Command{
+ Use: "version",
+ Short: "Print the version",
+ Long: `The version string is completely dependent on how the binary was built, so you should not depend on the version format. It may change without notice.
+
+This could be an arbitrary string, if specified via -ldflags.
+This could also be the go module version, if built with go modules (often "(devel)").`,
+ Args: cobra.NoArgs,
+ Run: func(_ *cobra.Command, _ []string) {
+ if Version == "" {
+ fmt.Println("could not determine build information")
+ } else {
+ fmt.Println(Version)
+ }
+ },
+ }
+}
diff --git a/cmd/crane/depcheck_test.go b/cmd/crane/depcheck_test.go
new file mode 100644
index 0000000..f36056e
--- /dev/null
+++ b/cmd/crane/depcheck_test.go
@@ -0,0 +1,32 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/depcheck"
+)
+
+func TestDeps(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping slow depcheck")
+ }
+ depcheck.AssertNoDependency(t, map[string][]string{
+ "github.com/google/go-containerregistry/cmd/crane": {
+ "github.com/google/go-containerregistry/pkg/v1/daemon",
+ },
+ })
+}
diff --git a/cmd/crane/doc/crane.md b/cmd/crane/doc/crane.md
new file mode 100644
index 0000000..afd1b24
--- /dev/null
+++ b/cmd/crane/doc/crane.md
@@ -0,0 +1,42 @@
+## crane
+
+Crane is a tool for managing container images
+
+```
+crane [flags]
+```
+
+### Options
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ -h, --help help for crane
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane append](crane_append.md) - Append contents of a tarball to a remote image
+* [crane auth](crane_auth.md) - Log in or access credentials
+* [crane blob](crane_blob.md) - Read a blob from the registry
+* [crane catalog](crane_catalog.md) - List the repos in a registry
+* [crane config](crane_config.md) - Get the config of an image
+* [crane copy](crane_copy.md) - Efficiently copy a remote image from src to dst while retaining the digest value
+* [crane delete](crane_delete.md) - Delete an image reference from its registry
+* [crane digest](crane_digest.md) - Get the digest of an image
+* [crane export](crane_export.md) - Export filesystem of a container image as a tarball
+* [crane flatten](crane_flatten.md) - Flatten an image's layers into a single layer
+* [crane index](crane_index.md) - Modify an image index.
+* [crane ls](crane_ls.md) - List the tags in a repo
+* [crane manifest](crane_manifest.md) - Get the manifest of an image
+* [crane mutate](crane_mutate.md) - Modify image labels and annotations. The container must be pushed to a registry, and the manifest is updated there.
+* [crane pull](crane_pull.md) - Pull remote images by reference and store their contents locally
+* [crane push](crane_push.md) - Push local image contents to a remote registry
+* [crane rebase](crane_rebase.md) - Rebase an image onto a new base image
+* [crane registry](crane_registry.md) -
+* [crane tag](crane_tag.md) - Efficiently tag a remote image
+* [crane validate](crane_validate.md) - Validate that an image is well-formed
+* [crane version](crane_version.md) - Print the version
+
diff --git a/cmd/crane/doc/crane_append.md b/cmd/crane/doc/crane_append.md
new file mode 100644
index 0000000..d637dd1
--- /dev/null
+++ b/cmd/crane/doc/crane_append.md
@@ -0,0 +1,43 @@
+## crane append
+
+Append contents of a tarball to a remote image
+
+### Synopsis
+
+This sub-command pushes an image based on an (optional)
+base image, with appended layers containing the contents of the
+provided tarballs.
+
+If the base image is a Windows base image (i.e., its config.OS is "windows"),
+the contents of the tarballs will be modified to be suitable for a Windows
+container image.
+
+```
+crane append [flags]
+```
+
+### Options
+
+```
+ -b, --base string Name of base image to append to
+ -h, --help help for append
+ -f, --new_layer strings Path to tarball to append to image
+ -t, --new_tag string Tag to apply to resulting image
+ --oci-empty-base If true, empty base image will have OCI media types instead of Docker
+ -o, --output string Path to new tarball of resulting image
+ --set-base-image-annotations If true, annotate the resulting image as being based on the base image
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_auth.md b/cmd/crane/doc/crane_auth.md
new file mode 100644
index 0000000..6eb8fc8
--- /dev/null
+++ b/cmd/crane/doc/crane_auth.md
@@ -0,0 +1,29 @@
+## crane auth
+
+Log in or access credentials
+
+```
+crane auth [flags]
+```
+
+### Options
+
+```
+ -h, --help help for auth
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+* [crane auth get](crane_auth_get.md) - Implements a credential helper
+* [crane auth login](crane_auth_login.md) - Log in to a registry
+
diff --git a/cmd/crane/doc/crane_auth_get.md b/cmd/crane/doc/crane_auth_get.md
new file mode 100644
index 0000000..6ff89c1
--- /dev/null
+++ b/cmd/crane/doc/crane_auth_get.md
@@ -0,0 +1,38 @@
+## crane auth get
+
+Implements a credential helper
+
+```
+crane auth get [REGISTRY_ADDR] [flags]
+```
+
+### Examples
+
+```
+ # Read configured credentials for reg.example.com
+ $ echo "reg.example.com" | crane auth get
+ {"username":"AzureDiamond","password":"hunter2"}
+ # or
+ $ crane auth get reg.example.com
+ {"username":"AzureDiamond","password":"hunter2"}
+```
+
+### Options
+
+```
+ -h, --help help for get
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane auth](crane_auth.md) - Log in or access credentials
+
diff --git a/cmd/crane/doc/crane_auth_login.md b/cmd/crane/doc/crane_auth_login.md
new file mode 100644
index 0000000..1fec423
--- /dev/null
+++ b/cmd/crane/doc/crane_auth_login.md
@@ -0,0 +1,37 @@
+## crane auth login
+
+Log in to a registry
+
+```
+crane auth login [OPTIONS] [SERVER] [flags]
+```
+
+### Examples
+
+```
+ # Log in to reg.example.com
+ crane auth login reg.example.com -u AzureDiamond -p hunter2
+```
+
+### Options
+
+```
+ -h, --help help for login
+ -p, --password string Password
+ --password-stdin Take the password from stdin
+ -u, --username string Username
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane auth](crane_auth.md) - Log in or access credentials
+
diff --git a/cmd/crane/doc/crane_blob.md b/cmd/crane/doc/crane_blob.md
new file mode 100644
index 0000000..36f615a
--- /dev/null
+++ b/cmd/crane/doc/crane_blob.md
@@ -0,0 +1,33 @@
+## crane blob
+
+Read a blob from the registry
+
+```
+crane blob BLOB [flags]
+```
+
+### Examples
+
+```
+crane blob ubuntu@sha256:4c1d20cdee96111c8acf1858b62655a37ce81ae48648993542b7ac363ac5c0e5 > blob.tar.gz
+```
+
+### Options
+
+```
+ -h, --help help for blob
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_catalog.md b/cmd/crane/doc/crane_catalog.md
new file mode 100644
index 0000000..99b81ad
--- /dev/null
+++ b/cmd/crane/doc/crane_catalog.md
@@ -0,0 +1,34 @@
+## crane catalog
+
+List the repos in a registry
+
+```
+crane catalog [REGISTRY] [flags]
+```
+
+### Examples
+
+```
+ # list the repos for reg.example.com
+ $ crane catalog reg.example.com
+```
+
+### Options
+
+```
+ -h, --help help for catalog
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_config.md b/cmd/crane/doc/crane_config.md
new file mode 100644
index 0000000..5d7fa5a
--- /dev/null
+++ b/cmd/crane/doc/crane_config.md
@@ -0,0 +1,27 @@
+## crane config
+
+Get the config of an image
+
+```
+crane config IMAGE [flags]
+```
+
+### Options
+
+```
+ -h, --help help for config
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_copy.md b/cmd/crane/doc/crane_copy.md
new file mode 100644
index 0000000..8e7e1a8
--- /dev/null
+++ b/cmd/crane/doc/crane_copy.md
@@ -0,0 +1,27 @@
+## crane copy
+
+Efficiently copy a remote image from src to dst while retaining the digest value
+
+```
+crane copy SRC DST [flags]
+```
+
+### Options
+
+```
+ -h, --help help for copy
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_delete.md b/cmd/crane/doc/crane_delete.md
new file mode 100644
index 0000000..7932ea2
--- /dev/null
+++ b/cmd/crane/doc/crane_delete.md
@@ -0,0 +1,27 @@
+## crane delete
+
+Delete an image reference from its registry
+
+```
+crane delete IMAGE [flags]
+```
+
+### Options
+
+```
+ -h, --help help for delete
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_digest.md b/cmd/crane/doc/crane_digest.md
new file mode 100644
index 0000000..f141b36
--- /dev/null
+++ b/cmd/crane/doc/crane_digest.md
@@ -0,0 +1,29 @@
+## crane digest
+
+Get the digest of an image
+
+```
+crane digest IMAGE [flags]
+```
+
+### Options
+
+```
+ --full-ref (Optional) if true, print the full image reference by digest
+ -h, --help help for digest
+ --tarball string (Optional) path to tarball containing the image
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_export.md b/cmd/crane/doc/crane_export.md
new file mode 100644
index 0000000..ca10c56
--- /dev/null
+++ b/cmd/crane/doc/crane_export.md
@@ -0,0 +1,40 @@
+## crane export
+
+Export filesystem of a container image as a tarball
+
+```
+crane export IMAGE|- TARBALL|- [flags]
+```
+
+### Examples
+
+```
+ # Write tarball to stdout
+ crane export ubuntu -
+
+ # Write tarball to file
+ crane export ubuntu ubuntu.tar
+
+ # Read image from stdin
+ crane export - ubuntu.tar
+```
+
+### Options
+
+```
+ -h, --help help for export
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_flatten.md b/cmd/crane/doc/crane_flatten.md
new file mode 100644
index 0000000..68e6bc6
--- /dev/null
+++ b/cmd/crane/doc/crane_flatten.md
@@ -0,0 +1,28 @@
+## crane flatten
+
+Flatten an image's layers into a single layer
+
+```
+crane flatten [flags]
+```
+
+### Options
+
+```
+ -h, --help help for flatten
+ -t, --tag string New tag to apply to flattened image. If not provided, push by digest to the original image repository.
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_index.md b/cmd/crane/doc/crane_index.md
new file mode 100644
index 0000000..2adea48
--- /dev/null
+++ b/cmd/crane/doc/crane_index.md
@@ -0,0 +1,29 @@
+## crane index
+
+Modify an image index.
+
+```
+crane index [flags]
+```
+
+### Options
+
+```
+ -h, --help help for index
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+* [crane index append](crane_index_append.md) - Append manifests to a remote index.
+* [crane index filter](crane_index_filter.md) - Modifies a remote index by filtering based on platform.
+
diff --git a/cmd/crane/doc/crane_index_append.md b/cmd/crane/doc/crane_index_append.md
new file mode 100644
index 0000000..a6c1541
--- /dev/null
+++ b/cmd/crane/doc/crane_index_append.md
@@ -0,0 +1,47 @@
+## crane index append
+
+Append manifests to a remote index.
+
+### Synopsis
+
+This sub-command pushes an index based on an (optional) base index, with appended manifests.
+
+The platform for appended manifests is inferred from the config file or omitted if that is infeasible.
+
+```
+crane index append [flags]
+```
+
+### Examples
+
+```
+ # Append a windows hello-world image to ubuntu, push to example.com/hello-world:weird
+ crane index append ubuntu -m hello-world@sha256:87b9ca29151260634b95efb84d43b05335dc3ed36cc132e2b920dd1955342d20 -t example.com/hello-world:weird
+
+ # Create an index from scratch for etcd.
+ crane index append -m registry.k8s.io/etcd-amd64:3.4.9 -m registry.k8s.io/etcd-arm64:3.4.9 -t example.com/etcd
+```
+
+### Options
+
+```
+ --docker-empty-base If true, empty base index will have Docker media types instead of OCI
+ --flatten If true, appending an index will append each of its children rather than the index itself (default true)
+ -h, --help help for append
+ -m, --manifest strings References to manifests to append to the base index
+ -t, --tag string Tag to apply to resulting image
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane index](crane_index.md) - Modify an image index.
+
diff --git a/cmd/crane/doc/crane_index_filter.md b/cmd/crane/doc/crane_index_filter.md
new file mode 100644
index 0000000..bda1f8d
--- /dev/null
+++ b/cmd/crane/doc/crane_index_filter.md
@@ -0,0 +1,41 @@
+## crane index filter
+
+Modifies a remote index by filtering based on platform.
+
+```
+crane index filter [flags]
+```
+
+### Examples
+
+```
+ # Filter out weird platforms from ubuntu, copy result to example.com/ubuntu
+ crane index filter ubuntu --platform linux/amd64 --platform linux/arm64 -t example.com/ubuntu
+
+ # Filter out any non-linux platforms, push to example.com/hello-world
+ crane index filter hello-world --platform linux -t example.com/hello-world
+
+ # Same as above, but in-place
+ crane index filter example.com/hello-world:some-tag --platform linux
+```
+
+### Options
+
+```
+ -h, --help help for filter
+ --platform platform(s) Specifies the platform(s) to keep from base in the form os/arch[/variant][:osversion][,<platform>] (e.g. linux/amd64).
+ -t, --tag string Tag to apply to resulting image
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane index](crane_index.md) - Modify an image index.
+
diff --git a/cmd/crane/doc/crane_ls.md b/cmd/crane/doc/crane_ls.md
new file mode 100644
index 0000000..6616820
--- /dev/null
+++ b/cmd/crane/doc/crane_ls.md
@@ -0,0 +1,29 @@
+## crane ls
+
+List the tags in a repo
+
+```
+crane ls REPO [flags]
+```
+
+### Options
+
+```
+ --full-ref (Optional) if true, print the full image reference
+ -h, --help help for ls
+ --omit-digest-tags (Optional), if true, omit digest tags (e.g., ':sha256-...')
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_manifest.md b/cmd/crane/doc/crane_manifest.md
new file mode 100644
index 0000000..3d61b4e
--- /dev/null
+++ b/cmd/crane/doc/crane_manifest.md
@@ -0,0 +1,27 @@
+## crane manifest
+
+Get the manifest of an image
+
+```
+crane manifest IMAGE [flags]
+```
+
+### Options
+
+```
+ -h, --help help for manifest
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_mutate.md b/cmd/crane/doc/crane_mutate.md
new file mode 100644
index 0000000..0174b09
--- /dev/null
+++ b/cmd/crane/doc/crane_mutate.md
@@ -0,0 +1,37 @@
+## crane mutate
+
+Modify image labels and annotations. The container must be pushed to a registry, and the manifest is updated there.
+
+```
+crane mutate [flags]
+```
+
+### Options
+
+```
+ -a, --annotation stringToString New annotations to add (default [])
+ --append strings Path to tarball to append to image
+ --cmd strings New cmd to set
+ --entrypoint strings New entrypoint to set
+ -e, --env stringToString New envvar to add (default [])
+ -h, --help help for mutate
+ -l, --label stringToString New labels to add (default [])
+ -o, --output string Path to new tarball of resulting image
+ --repo string Repository to push the mutated image to. If provided, push by digest to this repository.
+ -t, --tag string New tag reference to apply to mutated image. If not provided, push by digest to the original image repository.
+ -u, --user string New user to set
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_pull.md b/cmd/crane/doc/crane_pull.md
new file mode 100644
index 0000000..790a1cb
--- /dev/null
+++ b/cmd/crane/doc/crane_pull.md
@@ -0,0 +1,30 @@
+## crane pull
+
+Pull remote images by reference and store their contents locally
+
+```
+crane pull IMAGE TARBALL [flags]
+```
+
+### Options
+
+```
+ --annotate-ref Preserves image reference used to pull as an annotation when used with --format=oci
+ -c, --cache_path string Path to cache image layers
+ --format string Format in which to save images ("tarball", "legacy", or "oci") (default "tarball")
+ -h, --help help for pull
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_push.md b/cmd/crane/doc/crane_push.md
new file mode 100644
index 0000000..64bacf6
--- /dev/null
+++ b/cmd/crane/doc/crane_push.md
@@ -0,0 +1,33 @@
+## crane push
+
+Push local image contents to a remote registry
+
+### Synopsis
+
+If the PATH is a directory, it will be read as an OCI image layout. Otherwise, PATH is assumed to be a docker-style tarball.
+
+```
+crane push PATH IMAGE [flags]
+```
+
+### Options
+
+```
+ -h, --help help for push
+ --image-refs string path to file where a list of the published image references will be written
+ --index push a collection of images as a single index, currently required if PATH contains multiple images
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_rebase.md b/cmd/crane/doc/crane_rebase.md
new file mode 100644
index 0000000..e30f078
--- /dev/null
+++ b/cmd/crane/doc/crane_rebase.md
@@ -0,0 +1,32 @@
+## crane rebase
+
+Rebase an image onto a new base image
+
+```
+crane rebase [flags]
+```
+
+### Options
+
+```
+ -h, --help help for rebase
+ --new_base string New base image to insert
+ --old_base string Old base image to remove
+ --original string Original image to rebase (DEPRECATED: use positional arg instead)
+ --rebased string Tag to apply to rebased image (DEPRECATED: use --tag)
+ -t, --tag string Tag to apply to rebased image
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_registry.md b/cmd/crane/doc/crane_registry.md
new file mode 100644
index 0000000..d2bf920
--- /dev/null
+++ b/cmd/crane/doc/crane_registry.md
@@ -0,0 +1,24 @@
+## crane registry
+
+
+
+### Options
+
+```
+ -h, --help help for registry
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+* [crane registry serve](crane_registry_serve.md) - Serve an in-memory registry implementation
+
diff --git a/cmd/crane/doc/crane_registry_serve.md b/cmd/crane/doc/crane_registry_serve.md
new file mode 100644
index 0000000..6c46861
--- /dev/null
+++ b/cmd/crane/doc/crane_registry_serve.md
@@ -0,0 +1,35 @@
+## crane registry serve
+
+Serve an in-memory registry implementation
+
+### Synopsis
+
+This sub-command serves an in-memory registry implementation on port :8080 (or $PORT)
+
+The command blocks while the server accepts pushes and pulls.
+
+Contents are only stored in memory, and when the process exits, pushed data is lost.
+
+```
+crane registry serve [flags]
+```
+
+### Options
+
+```
+ -h, --help help for serve
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane registry](crane_registry.md) -
+
diff --git a/cmd/crane/doc/crane_tag.md b/cmd/crane/doc/crane_tag.md
new file mode 100644
index 0000000..5433467
--- /dev/null
+++ b/cmd/crane/doc/crane_tag.md
@@ -0,0 +1,46 @@
+## crane tag
+
+Efficiently tag a remote image
+
+### Synopsis
+
+This differs slightly from the "copy" command in a couple subtle ways:
+
+1. You don't have to specify the entire repository for the tag you're adding. For example, these two commands are functionally equivalent:
+```
+crane cp registry.example.com/library/ubuntu:v0 registry.example.com/library/ubuntu:v1
+crane tag registry.example.com/library/ubuntu:v0 v1
+```
+
+2. We can skip layer existence checks because we know the manifest already exists. This makes "tag" slightly faster than "copy".
+
+```
+crane tag IMG TAG [flags]
+```
+
+### Examples
+
+```
+# Add a v1 tag to ubuntu
+crane tag ubuntu v1
+```
+
+### Options
+
+```
+ -h, --help help for tag
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_validate.md b/cmd/crane/doc/crane_validate.md
new file mode 100644
index 0000000..cff22f8
--- /dev/null
+++ b/cmd/crane/doc/crane_validate.md
@@ -0,0 +1,30 @@
+## crane validate
+
+Validate that an image is well-formed
+
+```
+crane validate [flags]
+```
+
+### Options
+
+```
+ --fast Skip downloading/digesting layers
+ -h, --help help for validate
+ --remote string Name of remote image to validate
+ --tarball string Path to tarball to validate
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/doc/crane_version.md b/cmd/crane/doc/crane_version.md
new file mode 100644
index 0000000..0972792
--- /dev/null
+++ b/cmd/crane/doc/crane_version.md
@@ -0,0 +1,34 @@
+## crane version
+
+Print the version
+
+### Synopsis
+
+The version string is completely dependent on how the binary was built, so you should not depend on the version format. It may change without notice.
+
+This could be an arbitrary string, if specified via -ldflags.
+This could also be the go module version, if built with go modules (often "(devel)").
+
+```
+crane version [flags]
+```
+
+### Options
+
+```
+ -h, --help help for version
+```
+
+### Options inherited from parent commands
+
+```
+ --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers
+ --insecure Allow image references to be fetched without TLS
+ --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all)
+ -v, --verbose Enable debug logs
+```
+
+### SEE ALSO
+
+* [crane](crane.md) - Crane is a tool for managing container images
+
diff --git a/cmd/crane/help/README.md b/cmd/crane/help/README.md
new file mode 100644
index 0000000..c97606c
--- /dev/null
+++ b/cmd/crane/help/README.md
@@ -0,0 +1,5 @@
+## Generate docs for `crane`
+
+```go
+go run cmd/crane/help/main.go --dir=cmd/crane/doc/
+```
diff --git a/cmd/crane/help/main.go b/cmd/crane/help/main.go
new file mode 100644
index 0000000..e086a55
--- /dev/null
+++ b/cmd/crane/help/main.go
@@ -0,0 +1,45 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/google/go-containerregistry/cmd/crane/cmd"
+ "github.com/spf13/cobra"
+ "github.com/spf13/cobra/doc"
+)
+
+var dir string
+var root = &cobra.Command{
+ Use: "gendoc",
+ Short: "Generate crane's help docs",
+ Args: cobra.NoArgs,
+ RunE: func(*cobra.Command, []string) error {
+ return doc.GenMarkdownTree(cmd.Root, dir)
+ },
+}
+
+func init() {
+ root.Flags().StringVarP(&dir, "dir", "d", ".", "Path to directory in which to generate docs")
+}
+
+func main() {
+ if err := root.Execute(); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/crane/main.go b/cmd/crane/main.go
new file mode 100644
index 0000000..6dbdf22
--- /dev/null
+++ b/cmd/crane/main.go
@@ -0,0 +1,38 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "os"
+ "os/signal"
+
+ "github.com/google/go-containerregistry/cmd/crane/cmd"
+ "github.com/google/go-containerregistry/pkg/logs"
+)
+
+func init() {
+ logs.Warn.SetOutput(os.Stderr)
+ logs.Progress.SetOutput(os.Stderr)
+}
+
+func main() {
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
+ defer cancel()
+ if err := cmd.Root.ExecuteContext(ctx); err != nil {
+ cancel()
+ os.Exit(1)
+ }
+}
diff --git a/cmd/crane/rebase.md b/cmd/crane/rebase.md
new file mode 100644
index 0000000..da4b255
--- /dev/null
+++ b/cmd/crane/rebase.md
@@ -0,0 +1,125 @@
+### This code is experimental and might break you if not used correctly.
+
+The `rebase` command efficiently rewrites an image to replace the base image it
+is `FROM` with a new base image.
+
+![rebase visualization](./rebase.png)
+
+([link](https://docs.google.com/drawings/d/1w8UxTZDRbDWVoqnbr17SJuU73pRxpOmOk_vzmC9WB2k/edit))
+
+**This is not safe in general**, but it can be extremely useful for platform
+providers, e.g. when a vulnerability is discovered in a base layer and many
+thousands or millions of applications need to be patched in a short period of
+time.
+
+A commonly accepted guideline for rebase-safety is ABI-compatibility, but this
+is still imperfect in a handful of ways, and the exact contract varies between
+platform providers.
+
+Rebasing is best suited for when rebuilding is either impossible (source is not
+available) or impractical (too much work, too little time).
+
+## Using `crane rebase`
+
+For purposes of illustration, imagine you've built a container image
+`my-app:latest`, which is `FROM ubuntu`:
+
+```
+FROM ubuntu
+
+RUN ./very-expensive-build-process.sh
+
+ENTRYPOINT ["/bin/myapp"]
+```
+
+A serious vulnerability has been found in the `ubuntu` base image, and a new
+patched version has been released, tagged as `ubuntu:latest`.
+
+You could build your app image again, and the Dockerfile's `FROM ubuntu`
+directive would pick up the new base image release, but that requires a full
+rebuild of your entire app from source, which might take a long time, and might
+pull in other unrelated changes in dependencies.
+
+You may have thousands of images containing the vulnerability. You just want to
+release this critical bug fix across all your apps, as quickly as possible.
+
+Instead, you could use `crane rebase` to replace the vulnerable base image
+layers in your image with the patched base image layers, without requiring a
+full rebuild from source.
+
+```
+$ crane rebase my-app:latest \
+ --old_base=ubuntu@sha256:deadbeef... \
+ --new_base=ubuntu:latest \
+ --tag=my-app:rebased
+```
+
+This command:
+
+1. fetches the manifest for the original image `my-app:latest`, and the
+ `old_base` and `new_base` images
+1. checks that the original image is indeed based on `old_base`
+1. removes `old_base`'s layers from the original image
+1. replaces them with `new_base`'s layers
+1. computes and uploads a new manifest for the image, tagged as `--tag`.
+
+If `--tag` is not specified, its value will be assumed to be the original
+image's name. If the original image was specified by digest, the resulting
+image will be pushed by digest only.
+
+`crane rebase` will print the rebased image name by digest to `stdout`.
+
+### Base Image Annotation Hints
+
+The OCI image spec includes some [standard image
+annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md)
+that can provide hints for the `--old_base` and `--new_base` flag values, so
+these don't need to be specified:
+
+- **`org.opencontainers.image.base.digest`** specifies the original digest of
+ the base image
+- **`org.opencontainers.image.base.name`** specifies the original base image's
+ reference
+
+If the original image has these annotations, you can omit the `--old_base` and
+`--new_base` flags, and their values will be assumed to be:
+
+- `--old_base`: the `base.name` annotation value, plus the `base.digest`
+ annotation value
+- `--new_base`: the `base.name` annotation value
+
+If these annotation values are invalid, and the flags aren't set, the operation
+will fail.
+
+Whether or not the annotation values were set on the original image, they
+_will_ be set on the resulting rebased image, to ease future rebase operations
+on that image.
+
+`crane append` also supports the `--set-base-image-annotations` flag, which, if
+true, will set these annotations on the resulting image.
+
+## Caveats
+
+The tool has no visibility into what the specific contents of the resulting
+image, and has no idea what constitutes a "valid" image. As a result, it's
+perfectly capable of producing an image that's entirely invalid garbage.
+Rebasing arbitrary layers in an image is not a good idea.
+
+To help prevent garbage images, rebasing should only be done at a point in the
+layer stack between "base" layers and "app" layers. These should adhere to some
+contract about what "base" layers can be expected to produce, and what "app"
+layers should expect from base layers.
+
+In the example above, for instance, we assume that the Ubuntu base image is
+adhering to some contract with downstream app layers, that it won't remove or
+drastically change what it provides to the app layer. If the `new_base` layers
+removed some installed package, or made a breaking change to the version of
+some compiler expected by the uppermost app layers, the resulting rebased image
+might be invalid.
+
+In general, it's a good practice to tag rebased images to some other tag than
+the `original` tag, perform some sanity checks, then tag the image to the
+`original` tag once it's determined the image is valid.
+
+There is ongoing work to standardize and advertise base image contract
+adherence to make rebasing safer.
diff --git a/cmd/crane/rebase.png b/cmd/crane/rebase.png
new file mode 100644
index 0000000..449bdfe
--- /dev/null
+++ b/cmd/crane/rebase.png
Binary files differ
diff --git a/cmd/crane/rebase_test.sh b/cmd/crane/rebase_test.sh
new file mode 100755
index 0000000..727062b
--- /dev/null
+++ b/cmd/crane/rebase_test.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+set -ex
+
+tmp=$(mktemp -d)
+
+go install ./cmd/registry
+go build -o ./crane ./cmd/crane
+
+# Start a local registry.
+registry &
+PID=$!
+function cleanup {
+ kill $PID
+ rm -r ${tmp}
+ rm ./crane
+}
+trap cleanup EXIT
+
+sleep 1 # Wait for registry to be up.
+
+# Create an image localhost:1338/base containing a.txt
+echo a > ${tmp}/a.txt
+old_base=$(./crane append -f <(tar -f - -c ${tmp}) -t localhost:1338/base)
+rm ${tmp}/a.txt
+
+# Append to that image localhost:1338/rebaseme
+echo top > ${tmp}/top.txt
+orig=$(./crane append -f <(tar -f - -c ${tmp}) -b ${old_base} -t localhost:1338/rebaseme)
+rm ${tmp}/top.txt
+
+# Annotate that image as the base image (by ref and digest)
+# TODO: do this with a flag to --append
+orig=$(./crane mutate ${orig} \
+ --annotation org.opencontainers.image.base.name=localhost:1338/base \
+ --annotation org.opencontainers.image.base.digest=$(./crane digest localhost:1338/base))
+
+# Update localhost:1338/base containing b.txt
+echo b > ${tmp}/b.txt
+new_base=$(./crane append -f <(tar -f - -c ${tmp}) -t localhost:1338/base)
+rm ${tmp}/b.txt
+
+# Rebase using annotations
+rebased=$(./crane rebase ${orig})
+
+# List files in the rebased image.
+./crane export ${rebased} - | tar -tvf -
+
+# Extract b.txt out of the rebased image.
+./crane export ${rebased} - | tar -Oxf - ${tmp:1}/b.txt
+
+# Extract top.txt out of the rebased image.
+./crane export ${rebased} - | tar -Oxf - ${tmp:1}/top.txt
+
+# a.txt is _not_ in the rebased image.
+set +e
+./crane export ${rebased} - | tar -Oxf - ${tmp:1}/a.txt # this should fail
+code=$?
+echo "finding a.txt exited ${code}"
+if [[ $code -eq 0 ]]; then
+ echo "a.txt was found in rebased image"
+ exit 1
+fi
diff --git a/cmd/crane/recipes.md b/cmd/crane/recipes.md
new file mode 100644
index 0000000..1c0121d
--- /dev/null
+++ b/cmd/crane/recipes.md
@@ -0,0 +1,105 @@
+# `crane` Recipes
+
+Useful tips and things you can do with `crane` and other standard tools.
+
+### List files in an image
+
+```
+crane export ubuntu - | tar -tvf - | less
+```
+
+### Extract a single file from an image
+
+```
+crane export ubuntu - | tar -Oxf - etc/passwd
+```
+
+Note: Be sure to remove the leading `/` from the path (not `/etc/passwd`). This behavior will not follow symlinks.
+
+### Bundle directory contents into an image
+
+```
+crane append -f <(tar -f - -c some-dir/) -t ${IMAGE}
+```
+
+By default, this produces an image with one layer containing the directory contents. Add `-b ${BASE_IMAGE}` to append the layer to a base image instead.
+
+You can extend this even further with `crane mutate`, to make an executable in the appended layer the image's entrypoint.
+
+```
+crane mutate ${IMAGE} --entrypoint=some-dir/entrypoint.sh
+```
+
+Because `crane append` emits the full image reference, these calls can even be chained together:
+
+```
+crane mutate $(
+ crane append -f <(tar -f - -c some-dir/) -t ${IMAGE}
+) --entrypoint=some-dir/entrypoint.sh
+```
+
+This will bundle `some-dir/` into an image, push it, mutate its entrypoint to `some-dir/entrypoint.sh`, and push that new image by digest.
+
+### Diff two configs
+
+```
+diff <(crane config busybox:1.32 | jq) <(crane config busybox:1.33 | jq)
+```
+
+### Diff two manifests
+
+```
+diff <(crane manifest busybox:1.32 | jq) <(crane manifest busybox:1.33 | jq)
+```
+
+### Diff filesystem contents
+
+```
+diff \
+ <(crane export gcr.io/kaniko-project/executor:v1.6.0-debug - | tar -tvf - | sort) \
+ <(crane export gcr.io/kaniko-project/executor:v1.7.0-debug - | tar -tvf - | sort)
+```
+
+This will show file size diffs and (unfortunately) modified time diffs.
+
+With some work, you can use `cut` and other built-in Unix tools to ignore these diffs.
+
+### Get total image size
+
+Given an image manifest, you can calculate the total size of all layer blobs and the image's config blob using `jq`:
+
+```
+crane manifest gcr.io/buildpacks/builder:v1 | jq '.config.size + ([.layers[].size] | add)'
+```
+
+This will produce a number of bytes, which you can make human-readable by passing to [`numfmt`](https://www.gnu.org/software/coreutils/manual/html_node/numfmt-invocation.html)
+
+```
+crane manifest gcr.io/buildpacks/builder:v1 | jq '.config.size + ([.layers[].size] | add)' | numfmt --to=iec
+```
+
+For image indexes, you can pass the `--platform` flag to `crane` to get a platform-specific image.
+
+### Filter irrelevant platforms from a multi-platform image
+
+Perhaps you use a base image that supports a wide variety of exotic platforms, but you only care about linux/amd64 and linux/arm64.
+If you want to copy that base image into a different registry, you will end up with a bunch of images you don't use.
+You can filter the base to include only platforms that are relevant to you.
+
+```
+crane index filter ubuntu --platform linux/amd64 --platform linux/arm64 -t ${IMAGE}
+```
+
+Note that this will obviously modify the digest of the multi-platform image you're using, so this may invalidate other artifacts that reference it, e.g. signatures.
+
+### Create a multi-platform image from scratch
+
+If you have a bunch of platform-specific images that you want to turn into a multi-platform image, `crane index append` can do that:
+
+```
+crane index append -t ${IMAGE} \
+ -m ubuntu@sha256:c985bc3f77946b8e92c9a3648c6f31751a7dd972e06604785e47303f4ad47c4c \
+ -m ubuntu@sha256:61bd0b97000996232eb07b8d0e9375d14197f78aa850c2506417ef995a7199a7
+```
+
+Note that this is less flexible than [`manifest-tool`](https://github.com/estesp/manifest-tool) because it derives the platform from each image's config file, but it should work in most cases.
diff --git a/cmd/gcrane/README.md b/cmd/gcrane/README.md
new file mode 100644
index 0000000..c8c9ba2
--- /dev/null
+++ b/cmd/gcrane/README.md
@@ -0,0 +1,65 @@
+# `gcrane`
+
+<img src="../../images/gcrane.png" width="40%">
+
+This tool implements a superset of the [`crane`](../crane/README.md) commands, with
+additional commands that are specific to [gcr.io](https://gcr.io).
+
+Note that this relies on some implementation details of GCR that are not
+consistent with the [registry spec](https://docs.docker.com/registry/spec/api/),
+so this may break in the future.
+
+## Installation
+
+Download [latest release](https://github.com/google/go-containerregistry/releases/latest).
+
+Install manually:
+
+```
+go install github.com/google/go-containerregistry/cmd/gcrane@latest
+```
+
+## Commands
+
+### ls
+
+`gcrane ls` exposes a more complex form of `ls` than `crane`, which allows for
+listing tags, manifests, and sub-repositories.
+
+### cp
+
+`gcrane cp` supports a `-r` flag that copies images recursively, which is useful
+for backing up images, georeplicating images, or renaming images en masse.
+
+### gc
+
+`gcrane gc` will calculate images that can be garbage-collected.
+By default, it will print any images that do not have tags pointing to them.
+
+This can be composed with `gcrane delete` to actually garbage collect them:
+```shell
+gcrane gc gcr.io/${PROJECT_ID}/repo | xargs -n1 gcrane delete
+```
+
+## Images
+
+You can also use gcrane as docker image
+
+```sh
+$ docker run --rm gcr.io/go-containerregistry/gcrane ls gcr.io/google-containers/busybox
+gcr.io/google-containers/busybox@sha256:4bdd623e848417d96127e16037743f0cd8b528c026e9175e22a84f639eca58ff
+gcr.io/google-containers/busybox:1.24
+gcr.io/google-containers/busybox@sha256:545e6a6310a27636260920bc07b994a299b6708a1b26910cfefd335fdfb60d2b
+gcr.io/google-containers/busybox:1.27
+gcr.io/google-containers/busybox:1.27.2
+gcr.io/google-containers/busybox@sha256:d8d3bc2c183ed2f9f10e7258f84971202325ee6011ba137112e01e30f206de67
+gcr.io/google-containers/busybox:latest
+```
+
+And it's also available with a shell, at the `:debug` tag:
+
+```sh
+docker run --rm -it --entrypoint "/busybox/sh" gcr.io/go-containerregistry/gcrane:debug
+```
+
+Tagged debug images are available at `gcr.io/go-containerregistry/gcrane/debug:[tag]`.
diff --git a/cmd/gcrane/cmd/copy.go b/cmd/gcrane/cmd/copy.go
new file mode 100644
index 0000000..5ec4224
--- /dev/null
+++ b/cmd/gcrane/cmd/copy.go
@@ -0,0 +1,47 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "runtime"
+
+ "github.com/google/go-containerregistry/pkg/gcrane"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdCopy creates a new cobra.Command for the copy subcommand.
+func NewCmdCopy() *cobra.Command {
+ recursive := false
+ jobs := 1
+ cmd := &cobra.Command{
+ Use: "copy SRC DST",
+ Aliases: []string{"cp"},
+ Short: "Efficiently copy a remote image from src to dst",
+ Args: cobra.ExactArgs(2),
+ RunE: func(cc *cobra.Command, args []string) error {
+ src, dst := args[0], args[1]
+ ctx := cc.Context()
+ if recursive {
+ return gcrane.CopyRepository(ctx, src, dst, gcrane.WithJobs(jobs), gcrane.WithUserAgent(userAgent()), gcrane.WithContext(ctx))
+ }
+ return gcrane.Copy(src, dst, gcrane.WithUserAgent(userAgent()), gcrane.WithContext(ctx))
+ },
+ }
+
+ cmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Whether to recurse through repos")
+ cmd.Flags().IntVarP(&jobs, "jobs", "j", runtime.GOMAXPROCS(0), "The maximum number of concurrent copies")
+
+ return cmd
+}
diff --git a/cmd/gcrane/cmd/gc.go b/cmd/gcrane/cmd/gc.go
new file mode 100644
index 0000000..b377be4
--- /dev/null
+++ b/cmd/gcrane/cmd/gc.go
@@ -0,0 +1,76 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/gcrane"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/google"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdGc creates a new cobra.Command for the gc subcommand.
+func NewCmdGc() *cobra.Command {
+ recursive := false
+ cmd := &cobra.Command{
+ Use: "gc",
+ Short: "List images that are not tagged",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cc *cobra.Command, args []string) error {
+ return gc(cc.Context(), args[0], recursive)
+ },
+ }
+
+ cmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Whether to recurse through repos")
+
+ return cmd
+}
+
+func gc(ctx context.Context, root string, recursive bool) error {
+ repo, err := name.NewRepository(root)
+ if err != nil {
+ return err
+ }
+
+ opts := []google.Option{
+ google.WithAuthFromKeychain(gcrane.Keychain),
+ google.WithUserAgent(userAgent()),
+ google.WithContext(ctx),
+ }
+
+ if recursive {
+ return google.Walk(repo, printUntaggedImages, opts...)
+ }
+
+ tags, err := google.List(repo, opts...)
+ return printUntaggedImages(repo, tags, err)
+}
+
+func printUntaggedImages(repo name.Repository, tags *google.Tags, err error) error {
+ if err != nil {
+ return err
+ }
+
+ for digest, manifest := range tags.Manifests {
+ if len(manifest.Tags) == 0 {
+ fmt.Printf("%s@%s\n", repo, digest)
+ }
+ }
+
+ return nil
+}
diff --git a/cmd/gcrane/cmd/list.go b/cmd/gcrane/cmd/list.go
new file mode 100644
index 0000000..892f2af
--- /dev/null
+++ b/cmd/gcrane/cmd/list.go
@@ -0,0 +1,121 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "path"
+
+ "github.com/google/go-containerregistry/cmd/crane/cmd"
+ "github.com/google/go-containerregistry/pkg/gcrane"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/google"
+ "github.com/spf13/cobra"
+)
+
+func userAgent() string {
+ if cmd.Version != "" {
+ return path.Join("gcrane", cmd.Version)
+ }
+
+ return "gcrane"
+}
+
+// NewCmdList creates a new cobra.Command for the ls subcommand.
+func NewCmdList() *cobra.Command {
+ recursive := false
+ json := false
+ cmd := &cobra.Command{
+ Use: "ls REPO",
+ Short: "List the contents of a repo",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cc *cobra.Command, args []string) error {
+ return ls(cc.Context(), args[0], recursive, json)
+ },
+ }
+
+ cmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Whether to recurse through repos")
+ cmd.Flags().BoolVar(&json, "json", false, "Format the response from the registry as JSON, one line per repo")
+
+ return cmd
+}
+
+func ls(ctx context.Context, root string, recursive, j bool) error {
+ repo, err := name.NewRepository(root)
+ if err != nil {
+ return err
+ }
+
+ opts := []google.Option{
+ google.WithAuthFromKeychain(gcrane.Keychain),
+ google.WithUserAgent(userAgent()),
+ google.WithContext(ctx),
+ }
+
+ if recursive {
+ return google.Walk(repo, printImages(j), opts...)
+ }
+
+ tags, err := google.List(repo, opts...)
+ if err != nil {
+ return err
+ }
+
+ if !j {
+ if len(tags.Manifests) == 0 && len(tags.Children) == 0 {
+ // If we didn't see any GCR extensions, just list the tags like normal.
+ for _, tag := range tags.Tags {
+ fmt.Printf("%s:%s\n", repo, tag)
+ }
+ return nil
+ }
+
+ // Since we're not recursing, print the subdirectories too.
+ for _, child := range tags.Children {
+ fmt.Printf("%s/%s\n", repo, child)
+ }
+ }
+
+ return printImages(j)(repo, tags, err)
+}
+
+func printImages(j bool) google.WalkFunc {
+ return func(repo name.Repository, tags *google.Tags, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if j {
+ b, err := json.Marshal(tags)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("%s\n", b)
+ return nil
+ }
+
+ for digest, manifest := range tags.Manifests {
+ fmt.Printf("%s@%s\n", repo, digest)
+
+ for _, tag := range manifest.Tags {
+ fmt.Printf("%s:%s\n", repo, tag)
+ }
+ }
+
+ return nil
+ }
+}
diff --git a/cmd/gcrane/depcheck_test.go b/cmd/gcrane/depcheck_test.go
new file mode 100644
index 0000000..83749b3
--- /dev/null
+++ b/cmd/gcrane/depcheck_test.go
@@ -0,0 +1,32 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/depcheck"
+)
+
+func TestDeps(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping slow depcheck")
+ }
+ depcheck.AssertNoDependency(t, map[string][]string{
+ "github.com/google/go-containerregistry/cmd/gcrane": {
+ "github.com/google/go-containerregistry/pkg/v1/daemon",
+ },
+ })
+}
diff --git a/cmd/gcrane/main.go b/cmd/gcrane/main.go
new file mode 100644
index 0000000..98871e7
--- /dev/null
+++ b/cmd/gcrane/main.go
@@ -0,0 +1,72 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "os"
+ "os/signal"
+
+ "github.com/google/go-containerregistry/cmd/crane/cmd"
+ gcmd "github.com/google/go-containerregistry/cmd/gcrane/cmd"
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/gcrane"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/spf13/cobra"
+)
+
+func init() {
+ logs.Warn.SetOutput(os.Stderr)
+ logs.Progress.SetOutput(os.Stderr)
+}
+
+const (
+ use = "gcrane"
+ short = "gcrane is a tool for managing container images on gcr.io and pkg.dev"
+)
+
+func main() {
+ options := []crane.Option{crane.WithAuthFromKeychain(gcrane.Keychain)}
+ // Same as crane, but override usage and keychain.
+ root := cmd.New(use, short, options)
+
+ // Add or override commands.
+ gcraneCmds := []*cobra.Command{gcmd.NewCmdList(), gcmd.NewCmdGc(), gcmd.NewCmdCopy(), cmd.NewCmdAuth(options, "gcrane", "auth")}
+
+ // Maintain a map of google-specific commands that we "override".
+ used := make(map[string]bool)
+ for _, cmd := range gcraneCmds {
+ used[cmd.Use] = true
+ }
+
+ // Remove those from crane's set of commands.
+ for _, cmd := range root.Commands() {
+ if _, ok := used[cmd.Use]; ok {
+ root.RemoveCommand(cmd)
+ }
+ }
+
+ // Add our own.
+ for _, cmd := range gcraneCmds {
+ root.AddCommand(cmd)
+ }
+
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
+ defer cancel()
+ if err := root.ExecuteContext(ctx); err != nil {
+ cancel()
+ os.Exit(1)
+ }
+}
diff --git a/cmd/ko/README.md b/cmd/ko/README.md
new file mode 100644
index 0000000..7f3627e
--- /dev/null
+++ b/cmd/ko/README.md
@@ -0,0 +1,3 @@
+# `ko` has moved
+
+Please find `ko` at its new home, https://github.com/google/ko
diff --git a/cmd/krane/README.md b/cmd/krane/README.md
new file mode 100644
index 0000000..2f21e00
--- /dev/null
+++ b/cmd/krane/README.md
@@ -0,0 +1,15 @@
+# `krane`
+
+<img src="../../images/crane.png" width="40%">
+
+This tool is a variant of the [`crane`](../crane/README.md) command, but builds in
+support for authenticating against registries using common credential helpers
+that find credentials from the environment.
+
+In particular this tool supports authenticating with common "workload identity"
+mechanisms on platforms such as GKE and EKS.
+
+This additional keychain logic only kicks in if alternative authentication
+mechanisms have NOT been configured and `crane` would otherwise perform the
+command without credentials, so **it is a drop-in replacement for `crane` that
+adds support for authenticating with cloud workload identity mechanisms**.
diff --git a/cmd/krane/go.mod b/cmd/krane/go.mod
new file mode 100644
index 0000000..3ca44a8
--- /dev/null
+++ b/cmd/krane/go.mod
@@ -0,0 +1,67 @@
+module github.com/google/go-containerregistry/cmd/krane
+
+go 1.18
+
+replace github.com/google/go-containerregistry => ../../
+
+require (
+ github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1
+ github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589
+ github.com/google/go-containerregistry v0.13.0
+)
+
+require (
+ cloud.google.com/go/compute v1.18.0 // indirect
+ cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
+ github.com/Azure/go-autorest v14.2.0+incompatible // indirect
+ github.com/Azure/go-autorest/autorest v0.11.28 // indirect
+ github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
+ github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
+ github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
+ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
+ github.com/Azure/go-autorest/logger v0.2.1 // indirect
+ github.com/Azure/go-autorest/tracing v0.6.0 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.17.5 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.18.15 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 // indirect
+ github.com/aws/smithy-go v1.13.5 // indirect
+ github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
+ github.com/dimchansky/utfbom v1.1.1 // indirect
+ github.com/docker/cli v23.0.1+incompatible // indirect
+ github.com/docker/distribution v2.8.1+incompatible // indirect
+ github.com/docker/docker v23.0.1+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.7.0 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/go-cmp v0.5.9 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/klauspost/compress v1.16.0 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/sirupsen/logrus v1.9.0 // indirect
+ github.com/spf13/cobra v1.6.1 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/vbatts/tar-split v0.11.2 // indirect
+ golang.org/x/crypto v0.7.0 // indirect
+ golang.org/x/net v0.8.0 // indirect
+ golang.org/x/oauth2 v0.6.0 // indirect
+ golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sys v0.6.0 // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/protobuf v1.29.0 // indirect
+ gotest.tools/v3 v3.1.0 // indirect
+)
diff --git a/cmd/krane/go.sum b/cmd/krane/go.sum
new file mode 100644
index 0000000..23db7e4
--- /dev/null
+++ b/cmd/krane/go.sum
@@ -0,0 +1,199 @@
+cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
+cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
+github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
+github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
+github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
+github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=
+github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
+github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc=
+github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
+github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk=
+github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0=
+github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
+github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
+github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
+github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
+github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
+github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
+github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/aws/aws-sdk-go-v2 v1.17.5 h1:TzCUW1Nq4H8Xscph5M/skINUitxM5UBAyvm2s7XBzL4=
+github.com/aws/aws-sdk-go-v2 v1.17.5/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
+github.com/aws/aws-sdk-go-v2/config v1.18.15 h1:509yMO0pJUGUugBP2H9FOFyV+7Mz7sRR+snfDN5W4NY=
+github.com/aws/aws-sdk-go-v2/config v1.18.15/go.mod h1:vS0tddZqpE8cD9CyW0/kITHF5Bq2QasW9Y1DFHD//O0=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.15 h1:0rZQIi6deJFjOEgHI9HI2eZcLPPEGQPictX66oRFLL8=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.15/go.mod h1:vRMLMD3/rXU+o6j2MW5YefrGMBmdTvkLLGqFwMLBHQc=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 h1:b/Vn141DBuLVgXbhRWIrl9g+ww7G+ScV5SzniWR13jQ=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23/go.mod h1:mr6c4cHC+S/MMkrjtSlG4QA36kOznDep+0fga5L/fGQ=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 h1:IVx9L7YFhpPq0tTnGo8u8TpluFu7nAn9X3sUDMb11c0=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30/go.mod h1:vsbq62AOBwQ1LJ/GWKFxX8beUEYeRp/Agitrxee2/qM=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5 h1:tGA4ZoAsrYhGBypKAo2jwoX/Z5ponBZOTEUMNN/rHP4=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5/go.mod h1:cDZh+PHP8Adt9E0zfZT9cK4qadbtIuU/czLpEJtm4wc=
+github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4 h1:6OBVD6KE4gLReaNfG7CSXFvNIVqKIqrywRcG1kUKr4M=
+github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4/go.mod h1:gUxgbzXs+gHsj/6al9dzzoByeSrEl03Oj4iJBu/m/Rk=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 h1:QoOybhwRfciWUBbZ0gp9S7XaDnCuSTeK/fySB99V1ls=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23/go.mod h1:9uPh+Hrz2Vn6oMnQYiUi/zbh3ovbnQk19YKINkQny44=
+github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 h1:qJdM48OOLl1FBSzI7ZrA1ZfLwOyCYqkXV5lko1hYDBw=
+github.com/aws/aws-sdk-go-v2/service/sso v1.12.4/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 h1:YRkWXQveFb0tFC0TLktmmhGsOcCgLwvq88MC2al47AA=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 h1:L1600eLr0YvTT7gNh3Ni24yGI7NSHkq9Gp62vijPRCs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.18.5/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s=
+github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
+github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
+github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1 h1:uQhxQriOPUu/knXSPM7D/VyS3GMz+4wsE43eB8f9ojg=
+github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1/go.mod h1:/JmJjW2NJpzRSI3pOxQPC6eOD/tR8SfOA9X1FurmzXI=
+github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4=
+github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM=
+github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
+github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
+github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
+github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM=
+github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
+github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY=
+github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
+github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
+github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
+github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
+github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
+github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
+google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk=
+gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ=
diff --git a/cmd/krane/main.go b/cmd/krane/main.go
new file mode 100644
index 0000000..6912463
--- /dev/null
+++ b/cmd/krane/main.go
@@ -0,0 +1,67 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "io"
+ "os"
+ "os/signal"
+
+ ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
+ "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper"
+ "github.com/google/go-containerregistry/cmd/crane/cmd"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/authn/github"
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/v1/google"
+)
+
+var (
+ amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard)))
+ azureKeychain authn.Keychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper())
+)
+
+func init() {
+ logs.Warn.SetOutput(os.Stderr)
+ logs.Progress.SetOutput(os.Stderr)
+}
+
+const (
+ use = "krane"
+ short = "krane is a tool for managing container images"
+)
+
+func main() {
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
+ defer cancel()
+
+ keychain := authn.NewMultiKeychain(
+ authn.DefaultKeychain,
+ google.Keychain,
+ github.Keychain,
+ amazonKeychain,
+ azureKeychain,
+ )
+
+ // Same as crane, but override usage and keychain.
+ root := cmd.New(use, short, []crane.Option{crane.WithAuthFromKeychain(keychain)})
+
+ if err := root.ExecuteContext(ctx); err != nil {
+ cancel()
+ os.Exit(1)
+ }
+}
diff --git a/cmd/registry/main.go b/cmd/registry/main.go
new file mode 100644
index 0000000..58a0e4e
--- /dev/null
+++ b/cmd/registry/main.go
@@ -0,0 +1,44 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "time"
+
+ "github.com/google/go-containerregistry/pkg/registry"
+)
+
+var port = flag.Int("port", 1338, "port to run registry on")
+
+func main() {
+ flag.Parse()
+
+ listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
+ if err != nil {
+ log.Fatal(err)
+ }
+ porti := listener.Addr().(*net.TCPAddr).Port
+ log.Printf("serving on port %d", porti)
+ s := &http.Server{
+ ReadHeaderTimeout: 5 * time.Second, // prevent slowloris, quiet linter
+ Handler: registry.New(),
+ }
+ log.Fatal(s.Serve(listener))
+}
diff --git a/cmd/registry/test.sh b/cmd/registry/test.sh
new file mode 100755
index 0000000..d0c9507
--- /dev/null
+++ b/cmd/registry/test.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+set -ex
+
+CONTAINER_OS=$(docker info -f '{{ .OSType }}')
+
+# crane can run on a Windows system, but doesn't currently support pulling Windows
+# containers, so this test can only run if Docker is in Linux container mode.
+if [[ ${CONTAINER_OS} = "windows" ]]; then
+ set +x
+ echo [TEST SKIPPED] Windows containers are not yet supported by crane
+ exit
+fi
+
+function cleanup {
+ [[ -n $PID ]] && kill $PID
+ [[ -n $CTR ]] && docker stop $CTR
+ rm -f ubuntu.tar debiand.tar debianc.tar
+ docker rmi -f \
+ localhost:1338/debianc:latest \
+ localhost:1338/debiand:latest \
+ localhost:1338/ubuntuc:foo \
+ localhost:1338/ubuntud:latest \
+ || true
+}
+trap cleanup EXIT
+
+case "$OSTYPE" in
+ # On Windows, Docker runs in a VM, so a registry running on the Windows
+ # host is not accessible via localhost for `docker pull|push`.
+ win*|msys*|cygwin*)
+ docker run -d --rm -p 1338:5000 --name test-reg registry:2
+ CTR=test-reg
+ ;;
+
+ *)
+ registry &
+ PID=$!
+ ;;
+esac
+
+go install ./cmd/registry
+go install ./cmd/crane
+
+
+crane pull debian:latest debianc.tar
+crane push debianc.tar localhost:1338/debianc:latest
+docker pull localhost:1338/debianc:latest
+docker tag localhost:1338/debianc:latest localhost:1338/debiand:latest
+docker push localhost:1338/debiand:latest
+crane pull localhost:1338/debiand:latest debiand.tar
+
+docker pull ubuntu:latest
+docker tag ubuntu:latest localhost:1338/ubuntud:latest
+docker push localhost:1338/ubuntud:latest
+crane pull localhost:1338/ubuntud:latest ubuntu.tar
+crane push ubuntu.tar localhost:1338/ubuntuc:foo
+docker pull localhost:1338/ubuntuc:foo
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..0c905fd
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,49 @@
+module github.com/google/go-containerregistry
+
+go 1.18
+
+require (
+ github.com/containerd/stargz-snapshotter/estargz v0.14.3
+ github.com/docker/cli v23.0.1+incompatible
+ github.com/docker/distribution v2.8.1+incompatible
+ github.com/docker/docker v23.0.1+incompatible
+ github.com/google/go-cmp v0.5.9
+ github.com/klauspost/compress v1.16.0
+ github.com/mitchellh/go-homedir v1.1.0
+ github.com/opencontainers/go-digest v1.0.0
+ github.com/opencontainers/image-spec v1.1.0-rc2
+ github.com/spf13/cobra v1.6.1
+ golang.org/x/oauth2 v0.6.0
+ golang.org/x/sync v0.1.0
+ golang.org/x/tools v0.7.0
+)
+
+require (
+ cloud.google.com/go/compute v1.18.0 // indirect
+ cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+ github.com/Microsoft/go-winio v0.6.0 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+ github.com/docker/docker-credential-helpers v0.7.0 // indirect
+ github.com/docker/go-connections v0.4.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/sirupsen/logrus v1.9.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/vbatts/tar-split v0.11.2 // indirect
+ golang.org/x/mod v0.9.0 // indirect
+ golang.org/x/net v0.8.0 // indirect
+ golang.org/x/sys v0.6.0 // indirect
+ golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/protobuf v1.29.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ gotest.tools/v3 v3.0.3 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..9cc1ef9
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,153 @@
+cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
+cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
+github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
+github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
+github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM=
+github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
+github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY=
+github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
+github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
+github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0=
+github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
+github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
+github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
+golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
+golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
+google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
+gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
+gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
diff --git a/hack/boilerplate/boilerplate.go.txt b/hack/boilerplate/boilerplate.go.txt
new file mode 100644
index 0000000..a237f5e
--- /dev/null
+++ b/hack/boilerplate/boilerplate.go.txt
@@ -0,0 +1,13 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
diff --git a/hack/bump-deps.sh b/hack/bump-deps.sh
new file mode 100755
index 0000000..0e7325d
--- /dev/null
+++ b/hack/bump-deps.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+pushd ${PROJECT_ROOT}
+trap popd EXIT
+
+go get -u ./...
+go mod tidy -compat=1.18
+go mod vendor
+
+cd ${PROJECT_ROOT}/pkg/authn/k8schain
+go get -u ./...
+go mod tidy -compat=1.18
+go mod download
+
+cd ${PROJECT_ROOT}/pkg/authn/kubernetes
+go get -u ./...
+go mod tidy -compat=1.18
+go mod download
+
+cd ${PROJECT_ROOT}/cmd/krane
+go get -u ./...
+go mod tidy -compat=1.18
+go mod download
+
+cd ${PROJECT_ROOT}
+
+./hack/update-deps.sh
diff --git a/hack/presubmit.sh b/hack/presubmit.sh
new file mode 100755
index 0000000..25afe44
--- /dev/null
+++ b/hack/presubmit.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+# We can't install in the current directory without changing the current module.
+TMP_DIR="$(mktemp -d)"
+export PATH="${PATH}:${TMP_DIR}/bin"
+export GOPATH="${TMP_DIR}"
+pushd ${TMP_DIR}
+trap popd EXIT
+go install honnef.co/go/tools/cmd/staticcheck@v0.3.3
+popd
+
+pushd ${PROJECT_ROOT}
+trap popd EXIT
+
+staticcheck ./pkg/...
+
+# Verify that all source files are correctly formatted.
+find . -name "*.go" | grep -v vendor/ | xargs gofmt -d -e -l
+
+# Verify that generated crane docs are up-to-date.
+mkdir -p /tmp/gendoc && go run cmd/crane/help/main.go --dir /tmp/gendoc && diff -Naur /tmp/gendoc/ cmd/crane/doc/
+
+go test ./...
+./pkg/name/internal/must_test.sh
+
+./cmd/crane/rebase_test.sh
+
+pushd ${PROJECT_ROOT}/cmd/krane
+trap popd EXIT
+go build ./...
+
+pushd ${PROJECT_ROOT}/pkg/authn/k8schain
+trap popd EXIT
+go build ./...
+
+pushd ${PROJECT_ROOT}/pkg/authn/kubernetes
+trap popd EXIT
+go test ./...
diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh
new file mode 100755
index 0000000..e237a50
--- /dev/null
+++ b/hack/update-codegen.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+BOILER_PLATE_FILE="${PROJECT_ROOT}/hack/boilerplate/boilerplate.go.txt"
+MODULE_NAME=github.com/google/go-containerregistry
+
+pushd ${PROJECT_ROOT}
+trap popd EXIT
+
+export GOPATH=$(go env GOPATH)
+export PATH="${GOPATH}/bin:${PATH}"
+
+go mod tidy
+go mod vendor
+
+export GOBIN=$(mktemp -d)
+export PATH="$GOBIN:$PATH"
+
+go install github.com/maxbrunsfeld/counterfeiter/v6@latest
+go install k8s.io/code-generator/cmd/deepcopy-gen@v0.20.7
+
+counterfeiter -o pkg/v1/fake/index.go ${PROJECT_ROOT}/pkg/v1 ImageIndex
+counterfeiter -o pkg/v1/fake/image.go ${PROJECT_ROOT}/pkg/v1 Image
+
+DEEPCOPY_OUTPUT=$(mktemp -d)
+
+deepcopy-gen -O zz_deepcopy_generated --go-header-file $BOILER_PLATE_FILE \
+ --input-dirs "$MODULE_NAME/pkg/v1" \
+ --output-base "$DEEPCOPY_OUTPUT"
+
+# TODO - Generalize this for all directories when we need it
+cp $DEEPCOPY_OUTPUT/$MODULE_NAME/pkg/v1/*.go $PROJECT_ROOT/pkg/v1
+
+go run $PROJECT_ROOT/cmd/crane/help/main.go --dir=$PROJECT_ROOT/cmd/crane/doc/
diff --git a/hack/update-deps.sh b/hack/update-deps.sh
new file mode 100755
index 0000000..25be810
--- /dev/null
+++ b/hack/update-deps.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+pushd ${PROJECT_ROOT}
+trap popd EXIT
+
+go mod tidy
+go mod vendor
+
+# Delete all vendored broken symlinks.
+# From https://stackoverflow.com/questions/22097130/delete-all-broken-symbolic-links-with-a-line
+find vendor/ -type l -exec sh -c 'for x; do [ -e "$x" ] || rm "$x"; done' _ {} +
diff --git a/hack/update-dots.sh b/hack/update-dots.sh
new file mode 100755
index 0000000..570c794
--- /dev/null
+++ b/hack/update-dots.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Copyright 2019 The original author or authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+pushd ${PROJECT_ROOT}
+trap popd EXIT
+
+dot -Tjpeg images/ociimage.gv > images/ociimage.jpeg
+
+for f in $(ls images/dot/ | grep -e '.dot$'); do
+ dot -Tsvg images/dot/$f > images/$f.svg
+done
diff --git a/images/containerd.dot.svg b/images/containerd.dot.svg
new file mode 100644
index 0000000..cb87da6
--- /dev/null
+++ b/images/containerd.dot.svg
@@ -0,0 +1,2074 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: godep Pages: 1 -->
+<svg width="7819pt" height="984pt"
+ viewBox="0.00 0.00 7819.00 984.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 980)">
+<title>godep</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-980 7815,-980 7815,4 -4,4"/>
+<!-- bufio -->
+<g id="node1" class="node">
+<title>bufio</title>
+<g id="a_node1"><a xlink:href="https://godoc.org/bufio" xlink:title="bufio" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4541,-36C4541,-36 4511,-36 4511,-36 4505,-36 4499,-30 4499,-24 4499,-24 4499,-12 4499,-12 4499,-6 4505,0 4511,0 4511,0 4541,0 4541,0 4547,0 4553,-6 4553,-12 4553,-12 4553,-24 4553,-24 4553,-30 4547,-36 4541,-36"/>
+<text text-anchor="middle" x="4526" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bufio</text>
+</a>
+</g>
+</g>
+<!-- bytes -->
+<g id="node2" class="node">
+<title>bytes</title>
+<g id="a_node2"><a xlink:href="https://godoc.org/bytes" xlink:title="bytes" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4991,-36C4991,-36 4961,-36 4961,-36 4955,-36 4949,-30 4949,-24 4949,-24 4949,-12 4949,-12 4949,-6 4955,0 4961,0 4961,0 4991,0 4991,0 4997,0 5003,-6 5003,-12 5003,-12 5003,-24 5003,-24 5003,-30 4997,-36 4991,-36"/>
+<text text-anchor="middle" x="4976" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bytes</text>
+</a>
+</g>
+</g>
+<!-- compress/gzip -->
+<g id="node3" class="node">
+<title>compress/gzip</title>
+<g id="a_node3"><a xlink:href="https://godoc.org/compress/gzip" xlink:title="compress/gzip" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1425.5,-318C1425.5,-318 1354.5,-318 1354.5,-318 1348.5,-318 1342.5,-312 1342.5,-306 1342.5,-306 1342.5,-294 1342.5,-294 1342.5,-288 1348.5,-282 1354.5,-282 1354.5,-282 1425.5,-282 1425.5,-282 1431.5,-282 1437.5,-288 1437.5,-294 1437.5,-294 1437.5,-306 1437.5,-306 1437.5,-312 1431.5,-318 1425.5,-318"/>
+<text text-anchor="middle" x="1390" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">compress/gzip</text>
+</a>
+</g>
+</g>
+<!-- container/list -->
+<g id="node4" class="node">
+<title>container/list</title>
+<g id="a_node4"><a xlink:href="https://godoc.org/container/list" xlink:title="container/list" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4535.5,-130C4535.5,-130 4472.5,-130 4472.5,-130 4466.5,-130 4460.5,-124 4460.5,-118 4460.5,-118 4460.5,-106 4460.5,-106 4460.5,-100 4466.5,-94 4472.5,-94 4472.5,-94 4535.5,-94 4535.5,-94 4541.5,-94 4547.5,-100 4547.5,-106 4547.5,-106 4547.5,-118 4547.5,-118 4547.5,-124 4541.5,-130 4535.5,-130"/>
+<text text-anchor="middle" x="4504" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">container/list</text>
+</a>
+</g>
+</g>
+<!-- context -->
+<g id="node5" class="node">
+<title>context</title>
+<g id="a_node5"><a xlink:href="https://godoc.org/context" xlink:title="context" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2490,-130C2490,-130 2458,-130 2458,-130 2452,-130 2446,-124 2446,-118 2446,-118 2446,-106 2446,-106 2446,-100 2452,-94 2458,-94 2458,-94 2490,-94 2490,-94 2496,-94 2502,-100 2502,-106 2502,-106 2502,-118 2502,-118 2502,-124 2496,-130 2490,-130"/>
+<text text-anchor="middle" x="2474" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">context</text>
+</a>
+</g>
+</g>
+<!-- crypto -->
+<g id="node6" class="node">
+<title>crypto</title>
+<g id="a_node6"><a xlink:href="https://godoc.org/crypto" xlink:title="crypto" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2064,-224C2064,-224 2034,-224 2034,-224 2028,-224 2022,-218 2022,-212 2022,-212 2022,-200 2022,-200 2022,-194 2028,-188 2034,-188 2034,-188 2064,-188 2064,-188 2070,-188 2076,-194 2076,-200 2076,-200 2076,-212 2076,-212 2076,-218 2070,-224 2064,-224"/>
+<text text-anchor="middle" x="2049" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">crypto</text>
+</a>
+</g>
+</g>
+<!-- encoding -->
+<g id="node7" class="node">
+<title>encoding</title>
+<g id="a_node7"><a xlink:href="https://godoc.org/encoding" xlink:title="encoding" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4030,-36C4030,-36 3988,-36 3988,-36 3982,-36 3976,-30 3976,-24 3976,-24 3976,-12 3976,-12 3976,-6 3982,0 3988,0 3988,0 4030,0 4030,0 4036,0 4042,-6 4042,-12 4042,-12 4042,-24 4042,-24 4042,-30 4036,-36 4030,-36"/>
+<text text-anchor="middle" x="4009" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding</text>
+</a>
+</g>
+</g>
+<!-- encoding/base64 -->
+<g id="node8" class="node">
+<title>encoding/base64</title>
+<g id="a_node8"><a xlink:href="https://godoc.org/encoding/base64" xlink:title="encoding/base64" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5682.5,-788C5682.5,-788 5599.5,-788 5599.5,-788 5593.5,-788 5587.5,-782 5587.5,-776 5587.5,-776 5587.5,-764 5587.5,-764 5587.5,-758 5593.5,-752 5599.5,-752 5599.5,-752 5682.5,-752 5682.5,-752 5688.5,-752 5694.5,-758 5694.5,-764 5694.5,-764 5694.5,-776 5694.5,-776 5694.5,-782 5688.5,-788 5682.5,-788"/>
+<text text-anchor="middle" x="5641" y="-766.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/base64</text>
+</a>
+</g>
+</g>
+<!-- encoding/json -->
+<g id="node9" class="node">
+<title>encoding/json</title>
+<g id="a_node9"><a xlink:href="https://godoc.org/encoding/json" xlink:title="encoding/json" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5859,-36C5859,-36 5791,-36 5791,-36 5785,-36 5779,-30 5779,-24 5779,-24 5779,-12 5779,-12 5779,-6 5785,0 5791,0 5791,0 5859,0 5859,0 5865,0 5871,-6 5871,-12 5871,-12 5871,-24 5871,-24 5871,-30 5865,-36 5859,-36"/>
+<text text-anchor="middle" x="5825" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/json</text>
+</a>
+</g>
+</g>
+<!-- errors -->
+<g id="node10" class="node">
+<title>errors</title>
+<g id="a_node10"><a xlink:href="https://godoc.org/errors" xlink:title="errors" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4332,-36C4332,-36 4302,-36 4302,-36 4296,-36 4290,-30 4290,-24 4290,-24 4290,-12 4290,-12 4290,-6 4296,0 4302,0 4302,0 4332,0 4332,0 4338,0 4344,-6 4344,-12 4344,-12 4344,-24 4344,-24 4344,-30 4338,-36 4332,-36"/>
+<text text-anchor="middle" x="4317" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">errors</text>
+</a>
+</g>
+</g>
+<!-- fmt -->
+<g id="node11" class="node">
+<title>fmt</title>
+<g id="a_node11"><a xlink:href="https://godoc.org/fmt" xlink:title="fmt" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2902,-36C2902,-36 2872,-36 2872,-36 2866,-36 2860,-30 2860,-24 2860,-24 2860,-12 2860,-12 2860,-6 2866,0 2872,0 2872,0 2902,0 2902,0 2908,0 2914,-6 2914,-12 2914,-12 2914,-24 2914,-24 2914,-30 2908,-36 2902,-36"/>
+<text text-anchor="middle" x="2887" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">fmt</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/archive/compression -->
+<g id="node12" class="node">
+<title>github.com/containerd/containerd/archive/compression</title>
+<g id="a_node12"><a xlink:href="https://godoc.org/github.com/containerd/containerd/archive/compression" xlink:title="github.com/containerd/containerd/archive/compression" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2617.5,-412C2617.5,-412 2324.5,-412 2324.5,-412 2318.5,-412 2312.5,-406 2312.5,-400 2312.5,-400 2312.5,-388 2312.5,-388 2312.5,-382 2318.5,-376 2324.5,-376 2324.5,-376 2617.5,-376 2617.5,-376 2623.5,-376 2629.5,-382 2629.5,-388 2629.5,-388 2629.5,-400 2629.5,-400 2629.5,-406 2623.5,-412 2617.5,-412"/>
+<text text-anchor="middle" x="2471" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/archive/compression</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;bufio -->
+<g id="edge1" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M2629.602,-388.754C3019.2578,-375.5526 3996.8201,-340.4127 4064,-318 4232.1526,-261.9007 4243.7536,-188.0662 4394,-94 4427.6882,-72.9085 4467.3208,-50.3852 4494.3045,-35.382"/>
+<polygon fill="#000000" stroke="#000000" points="4495.3344,-36.8119 4498.857,-32.8554 4493.636,-33.7516 4495.3344,-36.8119"/>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;bytes -->
+<g id="edge2" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2629.7757,-388.9701C3030.935,-375.9594 4058.4564,-340.6347 4129,-318 4202.1293,-294.5356 4211.1021,-268.0718 4274,-224 4293.619,-210.2533 4423.8873,-103.2143 4446,-94 4469.2273,-84.3212 4832.4387,-36.6303 4943.7948,-22.1661"/>
+<polygon fill="#000000" stroke="#000000" points="4944.1717,-23.882 4948.9048,-21.5028 4943.721,-20.4111 4944.1717,-23.882"/>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;compress/gzip -->
+<g id="edge3" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;compress/gzip</title>
+<path fill="none" stroke="#000000" d="M2312.1441,-386.1973C2068.1437,-373.5259 1612.5067,-346.9587 1452,-318 1448.9598,-317.4515 1445.8555,-316.8124 1442.7363,-316.1087"/>
+<polygon fill="#000000" stroke="#000000" points="1443.0972,-314.3959 1437.8292,-314.9533 1442.295,-317.8027 1443.0972,-314.3959"/>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;context -->
+<g id="edge4" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M2454.5412,-375.8636C2423.8251,-339.7074 2363.4457,-256.2566 2393,-188 2402.8888,-165.1614 2423.3943,-146.0342 2441.2983,-132.6811"/>
+<polygon fill="#000000" stroke="#000000" points="2442.7751,-133.7705 2445.786,-129.4119 2440.7142,-130.9415 2442.7751,-133.7705"/>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;fmt -->
+<g id="edge5" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2312.4669,-390.5832C2122.5633,-384.3347 1817.224,-366.8098 1715,-318 1618.7665,-272.0505 1553.068,-168.877 1629,-94 1673.9782,-49.6467 2661.7572,-23.4634 2854.544,-18.766"/>
+<polygon fill="#000000" stroke="#000000" points="2854.7933,-20.5105 2859.7494,-18.6396 2854.7084,-17.0115 2854.7933,-20.5105"/>
+</g>
+<!-- github.com/containerd/containerd/log -->
+<g id="node13" class="node">
+<title>github.com/containerd/containerd/log</title>
+<g id="a_node13"><a xlink:href="https://godoc.org/github.com/containerd/containerd/log" xlink:title="github.com/containerd/containerd/log" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M5049.5,-318C5049.5,-318 4852.5,-318 4852.5,-318 4846.5,-318 4840.5,-312 4840.5,-306 4840.5,-306 4840.5,-294 4840.5,-294 4840.5,-288 4846.5,-282 4852.5,-282 4852.5,-282 5049.5,-282 5049.5,-282 5055.5,-282 5061.5,-288 5061.5,-294 5061.5,-294 5061.5,-306 5061.5,-306 5061.5,-312 5055.5,-318 5049.5,-318"/>
+<text text-anchor="middle" x="4951" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/log</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;github.com/containerd/containerd/log -->
+<g id="edge6" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;github.com/containerd/containerd/log</title>
+<path fill="none" stroke="#000000" d="M2629.6722,-388.3943C2974.2953,-376.1595 3817.0248,-345.9187 4523,-318 4628.6495,-313.822 4748.9534,-308.7331 4835.0523,-305.0336"/>
+<polygon fill="#000000" stroke="#000000" points="4835.2364,-306.7774 4840.1566,-304.8142 4835.086,-303.2806 4835.2364,-306.7774"/>
+</g>
+<!-- io -->
+<g id="node14" class="node">
+<title>io</title>
+<g id="a_node14"><a xlink:href="https://godoc.org/io" xlink:title="io" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1035,-36C1035,-36 1005,-36 1005,-36 999,-36 993,-30 993,-24 993,-24 993,-12 993,-12 993,-6 999,0 1005,0 1005,0 1035,0 1035,0 1041,0 1047,-6 1047,-12 1047,-12 1047,-24 1047,-24 1047,-30 1041,-36 1035,-36"/>
+<text text-anchor="middle" x="1020" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;io -->
+<g id="edge7" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2312.4732,-389.5287C2019.6144,-380.3798 1415.5593,-357.0299 1328,-318 1187.4632,-255.3551 1073.5571,-99.0913 1034.5113,-40.5866"/>
+<polygon fill="#000000" stroke="#000000" points="1035.835,-39.4161 1031.6129,-36.2165 1032.9182,-41.3507 1035.835,-39.4161"/>
+</g>
+<!-- os -->
+<g id="node15" class="node">
+<title>os</title>
+<g id="a_node15"><a xlink:href="https://godoc.org/os" xlink:title="os" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3787,-36C3787,-36 3757,-36 3757,-36 3751,-36 3745,-30 3745,-24 3745,-24 3745,-12 3745,-12 3745,-6 3751,0 3757,0 3757,0 3787,0 3787,0 3793,0 3799,-6 3799,-12 3799,-12 3799,-24 3799,-24 3799,-30 3793,-36 3787,-36"/>
+<text text-anchor="middle" x="3772" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">os</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;os -->
+<g id="edge8" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2629.7853,-389.1047C3021.1366,-376.6656 4002.1395,-343.0552 4026,-318 4244.507,-88.5527 3799.4971,-190.9797 3750,-130 3729.3083,-104.5081 3744.6162,-65.0981 3757.9921,-40.4644"/>
+<polygon fill="#000000" stroke="#000000" points="3759.5508,-41.2626 3760.4581,-36.0434 3756.4941,-39.5576 3759.5508,-41.2626"/>
+</g>
+<!-- os/exec -->
+<g id="node16" class="node">
+<title>os/exec</title>
+<g id="a_node16"><a xlink:href="https://godoc.org/os/exec" xlink:title="os/exec" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1511.5,-318C1511.5,-318 1478.5,-318 1478.5,-318 1472.5,-318 1466.5,-312 1466.5,-306 1466.5,-306 1466.5,-294 1466.5,-294 1466.5,-288 1472.5,-282 1478.5,-282 1478.5,-282 1511.5,-282 1511.5,-282 1517.5,-282 1523.5,-288 1523.5,-294 1523.5,-294 1523.5,-306 1523.5,-306 1523.5,-312 1517.5,-318 1511.5,-318"/>
+<text text-anchor="middle" x="1495" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">os/exec</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;os/exec -->
+<g id="edge9" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;os/exec</title>
+<path fill="none" stroke="#000000" d="M2312.3273,-385.8993C2087.5074,-373.5761 1687.8131,-348.3206 1546,-318 1540.2613,-316.773 1534.2849,-315.0723 1528.5288,-313.2022"/>
+<polygon fill="#000000" stroke="#000000" points="1529.0527,-311.5321 1523.7556,-311.5982 1527.9377,-314.8498 1529.0527,-311.5321"/>
+</g>
+<!-- strconv -->
+<g id="node17" class="node">
+<title>strconv</title>
+<g id="a_node17"><a xlink:href="https://godoc.org/strconv" xlink:title="strconv" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M802,-36C802,-36 770,-36 770,-36 764,-36 758,-30 758,-24 758,-24 758,-12 758,-12 758,-6 764,0 770,0 770,0 802,0 802,0 808,0 814,-6 814,-12 814,-12 814,-24 814,-24 814,-30 808,-36 802,-36"/>
+<text text-anchor="middle" x="786" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strconv</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;strconv -->
+<g id="edge10" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M2312.3702,-388.4396C2006.6929,-377.082 1357.5028,-349.6065 1260,-318 1067.887,-255.7247 875.992,-97.4925 810.0182,-39.6228"/>
+<polygon fill="#000000" stroke="#000000" points="811.136,-38.2753 806.2265,-36.2855 808.8236,-40.9027 811.136,-38.2753"/>
+</g>
+<!-- sync -->
+<g id="node18" class="node">
+<title>sync</title>
+<g id="a_node18"><a xlink:href="https://godoc.org/sync" xlink:title="sync" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5596,-36C5596,-36 5566,-36 5566,-36 5560,-36 5554,-30 5554,-24 5554,-24 5554,-12 5554,-12 5554,-6 5560,0 5566,0 5566,0 5596,0 5596,0 5602,0 5608,-6 5608,-12 5608,-12 5608,-24 5608,-24 5608,-30 5602,-36 5596,-36"/>
+<text text-anchor="middle" x="5581" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sync</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/archive/compression&#45;&gt;sync -->
+<g id="edge11" class="edge">
+<title>github.com/containerd/containerd/archive/compression&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2629.6865,-390.0124C2932.6896,-381.724 3614.4742,-359.8289 4187,-318 4392.4035,-302.9932 4908.1504,-269.5549 5109,-224 5229.6478,-196.6357 5257.8998,-180.1302 5371,-130 5435.9761,-101.2002 5508.4426,-60.5387 5548.9999,-36.9573"/>
+<polygon fill="#000000" stroke="#000000" points="5550.3466,-38.1979 5553.785,-34.1681 5548.584,-35.1741 5550.3466,-38.1979"/>
+</g>
+<!-- github.com/containerd/containerd/log&#45;&gt;context -->
+<g id="edge45" class="edge">
+<title>github.com/containerd/containerd/log&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M4840.3081,-297.0826C4501.692,-287.8796 3482.8155,-258.045 3153,-224 2903.1457,-198.2089 2605.7286,-139.242 2507.3818,-118.9847"/>
+<polygon fill="#000000" stroke="#000000" points="2507.5459,-117.2317 2502.2954,-117.9349 2506.8384,-120.6595 2507.5459,-117.2317"/>
+</g>
+<!-- github.com/sirupsen/logrus -->
+<g id="node36" class="node">
+<title>github.com/sirupsen/logrus</title>
+<g id="a_node36"><a xlink:href="https://godoc.org/github.com/sirupsen/logrus" xlink:title="github.com/sirupsen/logrus" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M5082.5,-224C5082.5,-224 4941.5,-224 4941.5,-224 4935.5,-224 4929.5,-218 4929.5,-212 4929.5,-212 4929.5,-200 4929.5,-200 4929.5,-194 4935.5,-188 4941.5,-188 4941.5,-188 5082.5,-188 5082.5,-188 5088.5,-188 5094.5,-194 5094.5,-200 5094.5,-200 5094.5,-212 5094.5,-212 5094.5,-218 5088.5,-224 5082.5,-224"/>
+<text text-anchor="middle" x="5012" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/sirupsen/logrus</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/log&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge46" class="edge">
+<title>github.com/containerd/containerd/log&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M4962.7614,-281.8759C4972.6349,-266.661 4986.7857,-244.8548 4997.3856,-228.5205"/>
+<polygon fill="#000000" stroke="#000000" points="4999.0208,-229.2154 5000.2747,-224.0685 4996.0849,-227.3101 4999.0208,-229.2154"/>
+</g>
+<!-- sync/atomic -->
+<g id="node37" class="node">
+<title>sync/atomic</title>
+<g id="a_node37"><a xlink:href="https://godoc.org/sync/atomic" xlink:title="sync/atomic" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4798.5,-36C4798.5,-36 4739.5,-36 4739.5,-36 4733.5,-36 4727.5,-30 4727.5,-24 4727.5,-24 4727.5,-12 4727.5,-12 4727.5,-6 4733.5,0 4739.5,0 4739.5,0 4798.5,0 4798.5,0 4804.5,0 4810.5,-6 4810.5,-12 4810.5,-12 4810.5,-24 4810.5,-24 4810.5,-30 4804.5,-36 4798.5,-36"/>
+<text text-anchor="middle" x="4769" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sync/atomic</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/log&#45;&gt;sync/atomic -->
+<g id="edge47" class="edge">
+<title>github.com/containerd/containerd/log&#45;&gt;sync/atomic</title>
+<path fill="none" stroke="#000000" d="M5018.697,-281.9819C5073.108,-263.8348 5136.3106,-232.1895 5109,-188 5075.6291,-134.0048 4900.4349,-65.1685 4815.397,-34.2821"/>
+<polygon fill="#000000" stroke="#000000" points="4815.9694,-32.6282 4810.6722,-32.5714 4814.7778,-35.9192 4815.9694,-32.6282"/>
+</g>
+<!-- github.com/containerd/containerd/content -->
+<g id="node19" class="node">
+<title>github.com/containerd/containerd/content</title>
+<g id="a_node19"><a xlink:href="https://godoc.org/github.com/containerd/containerd/content" xlink:title="github.com/containerd/containerd/content" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2107,-600C2107,-600 1887,-600 1887,-600 1881,-600 1875,-594 1875,-588 1875,-588 1875,-576 1875,-576 1875,-570 1881,-564 1887,-564 1887,-564 2107,-564 2107,-564 2113,-564 2119,-570 2119,-576 2119,-576 2119,-588 2119,-588 2119,-594 2113,-600 2107,-600"/>
+<text text-anchor="middle" x="1997" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/content</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;context -->
+<g id="edge12" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M2009.4378,-563.7508C2061.0674,-488.0798 2256.6557,-202.2539 2275,-188 2325.5117,-148.7511 2398.6126,-127.8448 2440.7181,-118.4121"/>
+<polygon fill="#000000" stroke="#000000" points="2441.1878,-120.1007 2445.6956,-117.3182 2440.4365,-116.6823 2441.1878,-120.1007"/>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;io -->
+<g id="edge17" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M1944.6218,-563.9032C1776.8372,-505.8897 1262.6845,-327.7606 1247,-318 1149.3067,-257.2043 1130.5949,-227.1914 1069,-130 1050.7564,-101.2132 1036.1205,-64.4732 1027.7444,-41.0836"/>
+<polygon fill="#000000" stroke="#000000" points="1029.3705,-40.4329 1026.053,-36.3029 1026.0709,-41.6003 1029.3705,-40.4329"/>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;sync -->
+<g id="edge20" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2119.1524,-569.2682C2686.4218,-510.022 5042.6342,-262.568 5195,-224 5303.6508,-196.4975 5331.1031,-184.5672 5429,-130 5476.7566,-103.3807 5526.8082,-63.9031 5555.8907,-39.6231"/>
+<polygon fill="#000000" stroke="#000000" points="5557.3202,-40.7083 5560.0267,-36.1545 5555.0711,-38.0266 5557.3202,-40.7083"/>
+</g>
+<!-- github.com/containerd/containerd/errdefs -->
+<g id="node20" class="node">
+<title>github.com/containerd/containerd/errdefs</title>
+<g id="a_node20"><a xlink:href="https://godoc.org/github.com/containerd/containerd/errdefs" xlink:title="github.com/containerd/containerd/errdefs" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3363,-506C3363,-506 3145,-506 3145,-506 3139,-506 3133,-500 3133,-494 3133,-494 3133,-482 3133,-482 3133,-476 3139,-470 3145,-470 3145,-470 3363,-470 3363,-470 3369,-470 3375,-476 3375,-482 3375,-482 3375,-494 3375,-494 3375,-500 3369,-506 3363,-506"/>
+<text text-anchor="middle" x="3254" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/errdefs</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;github.com/containerd/containerd/errdefs -->
+<g id="edge13" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;github.com/containerd/containerd/errdefs</title>
+<path fill="none" stroke="#000000" d="M2119.1708,-575.9524C2324.3756,-565.2821 2751.1532,-540.8938 3111,-506 3116.4345,-505.473 3121.9814,-504.9051 3127.5822,-504.3071"/>
+<polygon fill="#000000" stroke="#000000" points="3127.9562,-506.0269 3132.7392,-503.7497 3127.5801,-502.5472 3127.9562,-506.0269"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest -->
+<g id="node21" class="node">
+<title>github.com/opencontainers/go&#45;digest</title>
+<g id="a_node21"><a xlink:href="https://godoc.org/github.com/opencontainers/go-digest" xlink:title="github.com/opencontainers/go&#45;digest" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M2153,-318C2153,-318 1959,-318 1959,-318 1953,-318 1947,-312 1947,-306 1947,-306 1947,-294 1947,-294 1947,-288 1953,-282 1959,-282 1959,-282 2153,-282 2153,-282 2159,-282 2165,-288 2165,-294 2165,-294 2165,-306 2165,-306 2165,-312 2159,-318 2153,-318"/>
+<text text-anchor="middle" x="2056" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/go&#45;digest</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge14" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M1936.6218,-563.8866C1908.715,-552.1503 1878.2214,-533.7473 1862,-506 1853.9249,-492.1872 1855.3939,-484.5725 1862,-470 1893.6148,-400.2607 1969.5416,-347.8431 2016.4844,-320.7194"/>
+<polygon fill="#000000" stroke="#000000" points="2017.4632,-322.1756 2020.9343,-318.1737 2015.7252,-319.1376 2017.4632,-322.1756"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="node22" class="node">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<g id="a_node22"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" xlink:title="github.com/opencontainers/image&#45;spec/specs&#45;go/v1" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M1540.5,-506C1540.5,-506 1265.5,-506 1265.5,-506 1259.5,-506 1253.5,-500 1253.5,-494 1253.5,-494 1253.5,-482 1253.5,-482 1253.5,-476 1259.5,-470 1265.5,-470 1265.5,-470 1540.5,-470 1540.5,-470 1546.5,-470 1552.5,-476 1552.5,-482 1552.5,-482 1552.5,-494 1552.5,-494 1552.5,-500 1546.5,-506 1540.5,-506"/>
+<text text-anchor="middle" x="1403" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image&#45;spec/specs&#45;go/v1</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge15" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M1883.1737,-563.9871C1779.5464,-547.5881 1627.1084,-523.465 1521.9193,-506.8189"/>
+<polygon fill="#000000" stroke="#000000" points="1522.1585,-505.085 1516.9464,-506.0319 1521.6113,-508.542 1522.1585,-505.085"/>
+</g>
+<!-- github.com/pkg/errors -->
+<g id="node23" class="node">
+<title>github.com/pkg/errors</title>
+<g id="a_node23"><a xlink:href="https://godoc.org/github.com/pkg/errors" xlink:title="github.com/pkg/errors" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M5522,-412C5522,-412 5408,-412 5408,-412 5402,-412 5396,-406 5396,-400 5396,-400 5396,-388 5396,-388 5396,-382 5402,-376 5408,-376 5408,-376 5522,-376 5522,-376 5528,-376 5534,-382 5534,-388 5534,-388 5534,-400 5534,-400 5534,-406 5528,-412 5522,-412"/>
+<text text-anchor="middle" x="5465" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/pkg/errors</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;github.com/pkg/errors -->
+<g id="edge16" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M2119.1552,-575.378C2674.1481,-545.2919 4950.03,-421.9165 5390.8598,-398.0191"/>
+<polygon fill="#000000" stroke="#000000" points="5391.0632,-399.7608 5395.9611,-397.7426 5390.8737,-396.2659 5391.0632,-399.7608"/>
+</g>
+<!-- io/ioutil -->
+<g id="node24" class="node">
+<title>io/ioutil</title>
+<g id="a_node24"><a xlink:href="https://godoc.org/io/ioutil" xlink:title="io/ioutil" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M105.5,-36C105.5,-36 70.5,-36 70.5,-36 64.5,-36 58.5,-30 58.5,-24 58.5,-24 58.5,-12 58.5,-12 58.5,-6 64.5,0 70.5,0 70.5,0 105.5,0 105.5,0 111.5,0 117.5,-6 117.5,-12 117.5,-12 117.5,-24 117.5,-24 117.5,-30 111.5,-36 105.5,-36"/>
+<text text-anchor="middle" x="88" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io/ioutil</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;io/ioutil -->
+<g id="edge18" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M1874.9668,-576.2824C1724.3992,-567.7358 1461.0731,-547.9156 1239,-506 934.6672,-448.5581 861.3836,-417.2175 568,-318 461.0476,-281.8304 428.832,-282.057 332,-224 240.797,-169.318 149.2829,-80.9301 109.1253,-40.0444"/>
+<polygon fill="#000000" stroke="#000000" points="110.149,-38.5883 105.4012,-36.2387 107.6475,-41.0363 110.149,-38.5883"/>
+</g>
+<!-- math/rand -->
+<g id="node25" class="node">
+<title>math/rand</title>
+<g id="a_node25"><a xlink:href="https://godoc.org/math/rand" xlink:title="math/rand" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1937,-506C1937,-506 1889,-506 1889,-506 1883,-506 1877,-500 1877,-494 1877,-494 1877,-482 1877,-482 1877,-476 1883,-470 1889,-470 1889,-470 1937,-470 1937,-470 1943,-470 1949,-476 1949,-482 1949,-482 1949,-494 1949,-494 1949,-500 1943,-506 1937,-506"/>
+<text text-anchor="middle" x="1913" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">math/rand</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;math/rand -->
+<g id="edge19" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;math/rand</title>
+<path fill="none" stroke="#000000" d="M1980.804,-563.8759C1967.0915,-548.531 1947.3877,-526.4815 1932.7515,-510.1029"/>
+<polygon fill="#000000" stroke="#000000" points="1933.7829,-508.6307 1929.1463,-506.0685 1931.1731,-510.9629 1933.7829,-508.6307"/>
+</g>
+<!-- time -->
+<g id="node26" class="node">
+<title>time</title>
+<g id="a_node26"><a xlink:href="https://godoc.org/time" xlink:title="time" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1165,-36C1165,-36 1135,-36 1135,-36 1129,-36 1123,-30 1123,-24 1123,-24 1123,-12 1123,-12 1123,-6 1129,0 1135,0 1135,0 1165,0 1165,0 1171,0 1177,-6 1177,-12 1177,-12 1177,-24 1177,-24 1177,-30 1171,-36 1165,-36"/>
+<text text-anchor="middle" x="1150" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">time</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/content&#45;&gt;time -->
+<g id="edge21" class="edge">
+<title>github.com/containerd/containerd/content&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M1957.6231,-563.9006C1927.4327,-549.4905 1885.1252,-528.1289 1850,-506 1768.925,-454.9229 1766.3052,-413.2051 1678,-376 1519.3439,-309.1544 1438.6175,-412.9426 1295,-318 1196.1619,-252.6601 1162.6623,-99.839 1153.2179,-41.3442"/>
+<polygon fill="#000000" stroke="#000000" points="1154.9321,-40.9786 1152.4274,-36.3106 1151.4744,-41.5216 1154.9321,-40.9786"/>
+</g>
+<!-- github.com/containerd/containerd/errdefs&#45;&gt;context -->
+<g id="edge22" class="edge">
+<title>github.com/containerd/containerd/errdefs&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M3132.887,-479.02C3033.5864,-469.2886 2890.3056,-449.7779 2770,-412 2735.1057,-401.0426 2729.6095,-390.4251 2696,-376 2628.4087,-346.9899 2591.864,-371.1321 2541,-318 2491.3694,-266.1563 2478.5014,-177.1309 2475.1662,-135.1689"/>
+<polygon fill="#000000" stroke="#000000" points="2476.9018,-134.9073 2474.7885,-130.0497 2473.4113,-135.1649 2476.9018,-134.9073"/>
+</g>
+<!-- github.com/containerd/containerd/errdefs&#45;&gt;github.com/pkg/errors -->
+<g id="edge23" class="edge">
+<title>github.com/containerd/containerd/errdefs&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M3375.2316,-482.8459C3778.3763,-465.7063 5066.7743,-410.9304 5390.5994,-397.1631"/>
+<polygon fill="#000000" stroke="#000000" points="5390.8397,-398.9046 5395.7608,-396.9437 5390.691,-395.4077 5390.8397,-398.9046"/>
+</g>
+<!-- google.golang.org/grpc/codes -->
+<g id="node27" class="node">
+<title>google.golang.org/grpc/codes</title>
+<g id="a_node27"><a xlink:href="https://godoc.org/google.golang.org/grpc/codes" xlink:title="google.golang.org/grpc/codes" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M1360,-130C1360,-130 1206,-130 1206,-130 1200,-130 1194,-124 1194,-118 1194,-118 1194,-106 1194,-106 1194,-100 1200,-94 1206,-94 1206,-94 1360,-94 1360,-94 1366,-94 1372,-100 1372,-106 1372,-106 1372,-118 1372,-118 1372,-124 1366,-130 1360,-130"/>
+<text text-anchor="middle" x="1283" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/codes</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/errdefs&#45;&gt;google.golang.org/grpc/codes -->
+<g id="edge24" class="edge">
+<title>github.com/containerd/containerd/errdefs&#45;&gt;google.golang.org/grpc/codes</title>
+<path fill="none" stroke="#000000" d="M3132.6544,-479.2349C2715.2007,-448.8354 1361.9526,-348.1512 1328,-318 1275.3941,-271.284 1276.4122,-178.609 1280.1883,-135.3099"/>
+<polygon fill="#000000" stroke="#000000" points="1281.9354,-135.4221 1280.6591,-130.2807 1278.4506,-135.0958 1281.9354,-135.4221"/>
+</g>
+<!-- google.golang.org/grpc/status -->
+<g id="node28" class="node">
+<title>google.golang.org/grpc/status</title>
+<g id="a_node28"><a xlink:href="https://godoc.org/google.golang.org/grpc/status" xlink:title="google.golang.org/grpc/status" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M3331,-412C3331,-412 3177,-412 3177,-412 3171,-412 3165,-406 3165,-400 3165,-400 3165,-388 3165,-388 3165,-382 3171,-376 3177,-376 3177,-376 3331,-376 3331,-376 3337,-376 3343,-382 3343,-388 3343,-388 3343,-400 3343,-400 3343,-406 3337,-412 3331,-412"/>
+<text text-anchor="middle" x="3254" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/status</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/errdefs&#45;&gt;google.golang.org/grpc/status -->
+<g id="edge25" class="edge">
+<title>github.com/containerd/containerd/errdefs&#45;&gt;google.golang.org/grpc/status</title>
+<path fill="none" stroke="#000000" d="M3254,-469.8759C3254,-454.9211 3254,-433.5983 3254,-417.3629"/>
+<polygon fill="#000000" stroke="#000000" points="3255.7501,-417.0685 3254,-412.0685 3252.2501,-417.0685 3255.7501,-417.0685"/>
+</g>
+<!-- strings -->
+<g id="node29" class="node">
+<title>strings</title>
+<g id="a_node29"><a xlink:href="https://godoc.org/strings" xlink:title="strings" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M6629,-36C6629,-36 6599,-36 6599,-36 6593,-36 6587,-30 6587,-24 6587,-24 6587,-12 6587,-12 6587,-6 6593,0 6599,0 6599,0 6629,0 6629,0 6635,0 6641,-6 6641,-12 6641,-12 6641,-24 6641,-24 6641,-30 6635,-36 6629,-36"/>
+<text text-anchor="middle" x="6614" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strings</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/errdefs&#45;&gt;strings -->
+<g id="edge26" class="edge">
+<title>github.com/containerd/containerd/errdefs&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3375.1762,-487.1805C3873.2089,-483.4687 5748.4723,-466.003 6008,-412 6117.5254,-389.2097 6376.4903,-277.0193 6475,-224 6539.7013,-189.1768 6576.0056,-194.6089 6611,-130 6625.8998,-102.4911 6623.2617,-65.3396 6619.2414,-41.52"/>
+<polygon fill="#000000" stroke="#000000" points="6620.9198,-40.9656 6618.3125,-36.3543 6617.4751,-41.5851 6620.9198,-40.9656"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;crypto -->
+<g id="edge171" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;crypto</title>
+<path fill="none" stroke="#000000" d="M2054.6503,-281.8759C2053.5367,-266.9211 2051.9488,-245.5983 2050.7398,-229.3629"/>
+<polygon fill="#000000" stroke="#000000" points="2052.4621,-228.9247 2050.3455,-224.0685 2048.9718,-229.1847 2052.4621,-228.9247"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;fmt -->
+<g id="edge172" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2064.262,-281.9692C2086.8443,-232.8516 2148.4873,-99.9413 2157,-94 2214.9068,-53.5846 2719.9028,-26.1757 2854.2748,-19.5526"/>
+<polygon fill="#000000" stroke="#000000" points="2854.7384,-21.2821 2859.6467,-19.2892 2854.567,-17.7863 2854.7384,-21.2821"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;io -->
+<g id="edge174" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M1946.8622,-283.4425C1828.2455,-265.0424 1651.5748,-236.3031 1622,-224 1529.5511,-185.5412 1530.4369,-132.4876 1438,-94 1300.5256,-36.7603 1254.2945,-63.8126 1108,-36 1089.535,-32.4895 1068.95,-28.2945 1052.3346,-24.8378"/>
+<polygon fill="#000000" stroke="#000000" points="1052.5643,-23.0981 1047.3123,-23.7903 1051.8496,-26.5244 1052.5643,-23.0981"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;strings -->
+<g id="edge176" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2165.0049,-298.1793C2701.4226,-289.1093 5037.956,-248.2757 5109,-224 5134.5719,-215.2621 5132.5236,-197.0125 5158,-188 5419.9471,-95.3342 6145.0529,-222.6658 6407,-130 6432.4764,-120.9875 6432.8354,-107.9167 6456,-94 6498.0289,-68.7501 6549.721,-45.1863 6582.2764,-31.1834"/>
+<polygon fill="#000000" stroke="#000000" points="6583.0298,-32.7646 6586.9373,-29.1877 6581.6521,-29.5471 6583.0298,-32.7646"/>
+</g>
+<!-- regexp -->
+<g id="node38" class="node">
+<title>regexp</title>
+<g id="a_node38"><a xlink:href="https://godoc.org/regexp" xlink:title="regexp" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5461,-224C5461,-224 5431,-224 5431,-224 5425,-224 5419,-218 5419,-212 5419,-212 5419,-200 5419,-200 5419,-194 5425,-188 5431,-188 5431,-188 5461,-188 5461,-188 5467,-188 5473,-194 5473,-200 5473,-200 5473,-212 5473,-212 5473,-218 5467,-224 5461,-224"/>
+<text text-anchor="middle" x="5446" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">regexp</text>
+</a>
+</g>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;regexp -->
+<g id="edge175" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M2165.0208,-298.8542C2564.825,-294.3286 3988.3012,-275.3653 5161,-224 5252.7011,-219.9834 5360.8281,-212.3619 5413.6596,-208.4505"/>
+<polygon fill="#000000" stroke="#000000" points="5414.0477,-210.1766 5418.9043,-208.061 5413.7884,-206.6862 5414.0477,-210.1766"/>
+</g>
+<!-- hash -->
+<g id="node61" class="node">
+<title>hash</title>
+<g id="a_node61"><a xlink:href="https://godoc.org/hash" xlink:title="hash" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1981,-224C1981,-224 1951,-224 1951,-224 1945,-224 1939,-218 1939,-212 1939,-212 1939,-200 1939,-200 1939,-194 1945,-188 1951,-188 1951,-188 1981,-188 1981,-188 1987,-188 1993,-194 1993,-200 1993,-200 1993,-212 1993,-212 1993,-218 1987,-224 1981,-224"/>
+<text text-anchor="middle" x="1966" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">hash</text>
+</a>
+</g>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;hash -->
+<g id="edge173" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;hash</title>
+<path fill="none" stroke="#000000" d="M2038.6471,-281.8759C2023.8307,-266.401 2002.4857,-244.1073 1986.7648,-227.6877"/>
+<polygon fill="#000000" stroke="#000000" points="1988.0216,-226.4698 1983.2996,-224.0685 1985.4935,-228.8903 1988.0216,-226.4698"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge178" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M1424.1623,-469.9086C1455.0921,-444.5238 1515.4618,-398.884 1575,-376 1592.0312,-369.4539 1804.8774,-337.1682 1941.6634,-316.8398"/>
+<polygon fill="#000000" stroke="#000000" points="1942.2132,-318.5274 1946.9017,-316.0616 1941.6988,-315.0654 1942.2132,-318.5274"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;time -->
+<g id="edge180" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M1253.2425,-470.0197C1199.6674,-458.9087 1140.5754,-440.9629 1092,-412 995.243,-354.3089 962.482,-329.1255 922,-224 883.434,-123.8499 1046.2762,-53.8911 1117.7978,-28.5184"/>
+<polygon fill="#000000" stroke="#000000" points="1118.6466,-30.0753 1122.7855,-26.7689 1117.488,-26.7726 1118.6466,-30.0753"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go -->
+<g id="node50" class="node">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go</title>
+<g id="a_node50"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go" xlink:title="github.com/opencontainers/image&#45;spec/specs&#45;go" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M1375.5,-412C1375.5,-412 1118.5,-412 1118.5,-412 1112.5,-412 1106.5,-406 1106.5,-400 1106.5,-400 1106.5,-388 1106.5,-388 1106.5,-382 1112.5,-376 1118.5,-376 1118.5,-376 1375.5,-376 1375.5,-376 1381.5,-376 1387.5,-382 1387.5,-388 1387.5,-388 1387.5,-400 1387.5,-400 1387.5,-406 1381.5,-412 1375.5,-412"/>
+<text text-anchor="middle" x="1247" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image&#45;spec/specs&#45;go</text>
+</a>
+</g>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go -->
+<g id="edge179" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go</title>
+<path fill="none" stroke="#000000" d="M1372.9216,-469.8759C1346.7004,-454.0759 1308.6824,-431.1676 1281.288,-414.6607"/>
+<polygon fill="#000000" stroke="#000000" points="1282.1719,-413.1502 1276.986,-412.0685 1280.3654,-416.148 1282.1719,-413.1502"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;fmt -->
+<g id="edge181" class="edge">
+<title>github.com/pkg/errors&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M5395.898,-392.9764C4999.7014,-386.9744 3032.5298,-355.3296 2983,-318 2893.7342,-250.7222 2885.8151,-99.6195 2886.2489,-41.4463"/>
+<polygon fill="#000000" stroke="#000000" points="2888.0028,-41.1338 2886.3145,-36.1126 2884.503,-41.0907 2888.0028,-41.1338"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;io -->
+<g id="edge182" class="edge">
+<title>github.com/pkg/errors&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M5395.9615,-393.6255C4900.328,-390.8096 1898.4662,-371.5806 1715,-318 1638.0279,-295.5206 1627.4758,-268.844 1561,-224 1558.4899,-222.3067 1388.7624,-95.2397 1386,-94 1382.1359,-92.2659 1141.004,-42.7832 1052.2928,-24.6108"/>
+<polygon fill="#000000" stroke="#000000" points="1052.4254,-22.8517 1047.1759,-23.5627 1051.723,-26.2805 1052.4254,-22.8517"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;strings -->
+<g id="edge185" class="edge">
+<title>github.com/pkg/errors&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M5534.2326,-377.154C5764.6537,-321.0134 6492.3732,-143.0889 6514,-130 6551.5248,-107.2895 6582.9231,-66.0466 6600.0369,-40.4451"/>
+<polygon fill="#000000" stroke="#000000" points="6601.5679,-41.3025 6602.8621,-36.1656 6598.647,-39.3742 6601.5679,-41.3025"/>
+</g>
+<!-- runtime -->
+<g id="node39" class="node">
+<title>runtime</title>
+<g id="a_node39"><a xlink:href="https://godoc.org/runtime" xlink:title="runtime" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M7014.5,-36C7014.5,-36 6979.5,-36 6979.5,-36 6973.5,-36 6967.5,-30 6967.5,-24 6967.5,-24 6967.5,-12 6967.5,-12 6967.5,-6 6973.5,0 6979.5,0 6979.5,0 7014.5,0 7014.5,0 7020.5,0 7026.5,-6 7026.5,-12 7026.5,-12 7026.5,-24 7026.5,-24 7026.5,-30 7020.5,-36 7014.5,-36"/>
+<text text-anchor="middle" x="6997" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">runtime</text>
+</a>
+</g>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;runtime -->
+<g id="edge184" class="edge">
+<title>github.com/pkg/errors&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M5534.2114,-383.597C5614.772,-370.8712 5751.523,-347.3538 5867,-318 5917.6745,-305.1188 5928.4786,-295.4692 5979,-282 6098.435,-250.1582 6130.929,-253.3525 6251,-224 6527.2287,-156.4734 6856.5962,-59.8037 6962.4471,-28.3297"/>
+<polygon fill="#000000" stroke="#000000" points="6963.0243,-29.9838 6967.3176,-26.8806 6962.0261,-26.6292 6963.0243,-29.9838"/>
+</g>
+<!-- path -->
+<g id="node42" class="node">
+<title>path</title>
+<g id="a_node42"><a xlink:href="https://godoc.org/path" xlink:title="path" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M6036,-318C6036,-318 6006,-318 6006,-318 6000,-318 5994,-312 5994,-306 5994,-306 5994,-294 5994,-294 5994,-288 6000,-282 6006,-282 6006,-282 6036,-282 6036,-282 6042,-282 6048,-288 6048,-294 6048,-294 6048,-306 6048,-306 6048,-312 6042,-318 6036,-318"/>
+<text text-anchor="middle" x="6021" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">path</text>
+</a>
+</g>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;path -->
+<g id="edge183" class="edge">
+<title>github.com/pkg/errors&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M5534.3585,-388.1067C5632.8353,-378.727 5818.2984,-357.3151 5972,-318 5977.5122,-316.59 5983.2678,-314.7891 5988.8159,-312.8737"/>
+<polygon fill="#000000" stroke="#000000" points="5989.7132,-314.4122 5993.8397,-311.0904 5988.5424,-311.1138 5989.7132,-314.4122"/>
+</g>
+<!-- google.golang.org/grpc/codes&#45;&gt;fmt -->
+<g id="edge224" class="edge">
+<title>google.golang.org/grpc/codes&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1372.0294,-101.6826C1398.0988,-98.9028 1426.6686,-96.0903 1453,-94 2012.9986,-49.5443 2698.1532,-24.4678 2854.294,-19.097"/>
+<polygon fill="#000000" stroke="#000000" points="2854.6871,-20.8347 2859.6242,-18.9143 2854.5671,-17.3367 2854.6871,-20.8347"/>
+</g>
+<!-- google.golang.org/grpc/codes&#45;&gt;strconv -->
+<g id="edge225" class="edge">
+<title>google.golang.org/grpc/codes&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M1193.8595,-95.1404C1081.8778,-73.9608 895.2191,-38.6571 819.4715,-24.3306"/>
+<polygon fill="#000000" stroke="#000000" points="819.45,-22.5456 814.2119,-23.3359 818.7996,-25.9846 819.45,-22.5456"/>
+</g>
+<!-- google.golang.org/grpc/status&#45;&gt;context -->
+<g id="edge236" class="edge">
+<title>google.golang.org/grpc/status&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M3164.9707,-389.2956C3032.8474,-380.9995 2792.4411,-360.4695 2718,-318 2653.7318,-281.3343 2670.749,-235.4797 2614,-188 2581.0809,-160.4578 2536.6218,-138.378 2506.7134,-125.26"/>
+<polygon fill="#000000" stroke="#000000" points="2507.3256,-123.618 2502.0422,-123.2321 2505.9318,-126.8285 2507.3256,-123.618"/>
+</g>
+<!-- google.golang.org/grpc/status&#45;&gt;errors -->
+<g id="edge237" class="edge">
+<title>google.golang.org/grpc/status&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3343.2399,-392.8388C3587.0083,-388.9557 4251.3796,-373.3562 4332,-318 4382.2849,-283.473 4386.1409,-248.1486 4376,-188 4366.8301,-133.6104 4342.2458,-73.3834 4327.8702,-41.2405"/>
+<polygon fill="#000000" stroke="#000000" points="4329.3056,-40.1665 4325.6546,-36.3281 4326.1151,-41.6055 4329.3056,-40.1665"/>
+</g>
+<!-- google.golang.org/grpc/status&#45;&gt;fmt -->
+<g id="edge238" class="edge">
+<title>google.golang.org/grpc/status&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3164.7891,-385.0252C3040.7909,-371.6391 2828.8912,-345.1016 2806,-318 2741.7598,-241.9441 2766.4425,-185.3593 2806,-94 2816.1863,-70.4743 2837.6378,-50.8867 2855.9068,-37.4917"/>
+<polygon fill="#000000" stroke="#000000" points="2856.9498,-38.8972 2859.991,-34.5597 2854.9086,-36.054 2856.9498,-38.8972"/>
+</g>
+<!-- google.golang.org/grpc/status&#45;&gt;google.golang.org/grpc/codes -->
+<g id="edge242" class="edge">
+<title>google.golang.org/grpc/status&#45;&gt;google.golang.org/grpc/codes</title>
+<path fill="none" stroke="#000000" d="M3164.7863,-391.2316C2851.2943,-381.2971 1811.186,-346.5195 1664,-318 1632.8139,-311.9572 1420.8725,-240.9397 1394,-224 1355.2506,-199.5735 1319.7363,-159.2322 1299.7815,-134.2144"/>
+<polygon fill="#000000" stroke="#000000" points="1300.9499,-132.8704 1296.4769,-130.0324 1298.2038,-135.0404 1300.9499,-132.8704"/>
+</g>
+<!-- github.com/golang/protobuf/proto -->
+<g id="node51" class="node">
+<title>github.com/golang/protobuf/proto</title>
+<g id="a_node51"><a xlink:href="https://godoc.org/github.com/golang/protobuf/proto" xlink:title="github.com/golang/protobuf/proto" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M4183,-130C4183,-130 4005,-130 4005,-130 3999,-130 3993,-124 3993,-118 3993,-118 3993,-106 3993,-106 3993,-100 3999,-94 4005,-94 4005,-94 4183,-94 4183,-94 4189,-94 4195,-100 4195,-106 4195,-106 4195,-118 4195,-118 4195,-124 4189,-130 4183,-130"/>
+<text text-anchor="middle" x="4094" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/proto</text>
+</a>
+</g>
+</g>
+<!-- google.golang.org/grpc/status&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge239" class="edge">
+<title>google.golang.org/grpc/status&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3343.0024,-392.0198C3578.8082,-386.1359 4207.127,-366.0808 4287,-318 4341.3606,-285.2768 4383.8381,-238.9329 4346,-188 4327.9099,-163.6493 4260.8032,-144.1145 4200.3879,-130.95"/>
+<polygon fill="#000000" stroke="#000000" points="4200.5663,-129.1983 4195.3097,-129.8549 4199.8284,-132.6197 4200.5663,-129.1983"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes -->
+<g id="node57" class="node">
+<title>github.com/golang/protobuf/ptypes</title>
+<g id="a_node57"><a xlink:href="https://godoc.org/github.com/golang/protobuf/ptypes" xlink:title="github.com/golang/protobuf/ptypes" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3683.5,-318C3683.5,-318 3498.5,-318 3498.5,-318 3492.5,-318 3486.5,-312 3486.5,-306 3486.5,-306 3486.5,-294 3486.5,-294 3486.5,-288 3492.5,-282 3498.5,-282 3498.5,-282 3683.5,-282 3683.5,-282 3689.5,-282 3695.5,-288 3695.5,-294 3695.5,-294 3695.5,-306 3695.5,-306 3695.5,-312 3689.5,-318 3683.5,-318"/>
+<text text-anchor="middle" x="3591" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/ptypes</text>
+</a>
+</g>
+</g>
+<!-- google.golang.org/grpc/status&#45;&gt;github.com/golang/protobuf/ptypes -->
+<g id="edge240" class="edge">
+<title>google.golang.org/grpc/status&#45;&gt;github.com/golang/protobuf/ptypes</title>
+<path fill="none" stroke="#000000" d="M3318.5782,-375.9871C3376.6702,-359.7834 3461.7991,-336.0382 3521.3897,-319.4165"/>
+<polygon fill="#000000" stroke="#000000" points="3522.0077,-321.061 3526.3537,-318.0319 3521.0673,-317.6897 3522.0077,-321.061"/>
+</g>
+<!-- google.golang.org/genproto/googleapis/rpc/status -->
+<g id="node64" class="node">
+<title>google.golang.org/genproto/googleapis/rpc/status</title>
+<g id="a_node64"><a xlink:href="https://godoc.org/google.golang.org/genproto/googleapis/rpc/status" xlink:title="google.golang.org/genproto/googleapis/rpc/status" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3999.5,-318C3999.5,-318 3738.5,-318 3738.5,-318 3732.5,-318 3726.5,-312 3726.5,-306 3726.5,-306 3726.5,-294 3726.5,-294 3726.5,-288 3732.5,-282 3738.5,-282 3738.5,-282 3999.5,-282 3999.5,-282 4005.5,-282 4011.5,-288 4011.5,-294 4011.5,-294 4011.5,-306 4011.5,-306 4011.5,-312 4005.5,-318 3999.5,-318"/>
+<text text-anchor="middle" x="3869" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/genproto/googleapis/rpc/status</text>
+</a>
+</g>
+</g>
+<!-- google.golang.org/grpc/status&#45;&gt;google.golang.org/genproto/googleapis/rpc/status -->
+<g id="edge241" class="edge">
+<title>google.golang.org/grpc/status&#45;&gt;google.golang.org/genproto/googleapis/rpc/status</title>
+<path fill="none" stroke="#000000" d="M3343.0976,-380.3818C3449.1289,-364.1754 3626.7644,-337.0246 3745.8794,-318.8184"/>
+<polygon fill="#000000" stroke="#000000" points="3746.4743,-320.4979 3751.1525,-318.0125 3745.9455,-317.0381 3746.4743,-320.4979"/>
+</g>
+<!-- google.golang.org/grpc/internal -->
+<g id="node67" class="node">
+<title>google.golang.org/grpc/internal</title>
+<g id="a_node67"><a xlink:href="https://godoc.org/google.golang.org/grpc/internal" xlink:title="google.golang.org/grpc/internal" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1906,-318C1906,-318 1742,-318 1742,-318 1736,-318 1730,-312 1730,-306 1730,-306 1730,-294 1730,-294 1730,-288 1736,-282 1742,-282 1742,-282 1906,-282 1906,-282 1912,-282 1918,-288 1918,-294 1918,-294 1918,-306 1918,-306 1918,-312 1912,-318 1906,-318"/>
+<text text-anchor="middle" x="1824" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/internal</text>
+</a>
+</g>
+</g>
+<!-- google.golang.org/grpc/status&#45;&gt;google.golang.org/grpc/internal -->
+<g id="edge243" class="edge">
+<title>google.golang.org/grpc/status&#45;&gt;google.golang.org/grpc/internal</title>
+<path fill="none" stroke="#000000" d="M3164.7476,-392.1849C2951.1801,-387.1198 2394.082,-369.6371 1932,-318 1929.1663,-317.6833 1926.294,-317.342 1923.3971,-316.9799"/>
+<polygon fill="#000000" stroke="#000000" points="1923.3418,-315.2085 1918.1595,-316.3067 1922.8956,-318.68 1923.3418,-315.2085"/>
+</g>
+<!-- github.com/containerd/containerd/images -->
+<g id="node30" class="node">
+<title>github.com/containerd/containerd/images</title>
+<g id="a_node30"><a xlink:href="https://godoc.org/github.com/containerd/containerd/images" xlink:title="github.com/containerd/containerd/images" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3722.5,-694C3722.5,-694 3503.5,-694 3503.5,-694 3497.5,-694 3491.5,-688 3491.5,-682 3491.5,-682 3491.5,-670 3491.5,-670 3491.5,-664 3497.5,-658 3503.5,-658 3503.5,-658 3722.5,-658 3722.5,-658 3728.5,-658 3734.5,-664 3734.5,-670 3734.5,-670 3734.5,-682 3734.5,-682 3734.5,-688 3728.5,-694 3722.5,-694"/>
+<text text-anchor="middle" x="3613" y="-672.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/images</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;context -->
+<g id="edge27" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M3554.1735,-657.8645C3522.0815,-645.6168 3483.4246,-626.7461 3456,-600 3409.466,-554.6174 3442.5167,-506.891 3389,-470 3294.1781,-404.6359 2986.0649,-438.5539 2874,-412 2844.6165,-405.0375 2642.9962,-336.3316 2619,-318 2552.4056,-267.1259 2504.2667,-176.999 2484.2649,-134.8826"/>
+<polygon fill="#000000" stroke="#000000" points="2485.7895,-134.0122 2482.0782,-130.2321 2482.6221,-135.5015 2485.7895,-134.0122"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;encoding/json -->
+<g id="edge28" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3734.5923,-675.0239C3980.838,-671.8318 4530.1746,-658.2013 4708,-600 4736.4765,-590.6798 4737.6511,-576.2398 4765,-564 4850.6465,-525.6696 4878.5614,-534.3776 4968,-506 5013.2679,-491.6372 5743.1045,-259.1873 5775,-224 5822.1683,-171.9636 5826.7979,-83.0328 5826.0988,-41.1315"/>
+<polygon fill="#000000" stroke="#000000" points="5827.8458,-40.9788 5825.9827,-36.0198 5824.3467,-41.0583 5827.8458,-40.9788"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;fmt -->
+<g id="edge29" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3603.7904,-657.893C3575.4351,-604.2377 3483.6486,-445.5089 3357,-376 3194.5074,-286.8187 3094.034,-426.8436 2944,-318 2855.8997,-254.0867 2872.4469,-99.7406 2882.4557,-41.116"/>
+<polygon fill="#000000" stroke="#000000" points="2884.1997,-41.302 2883.341,-36.0746 2880.7525,-40.6966 2884.1997,-41.302"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;github.com/containerd/containerd/log -->
+<g id="edge32" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;github.com/containerd/containerd/log</title>
+<path fill="none" stroke="#000000" d="M3734.8909,-672.8566C3981.6171,-665.5885 4522.8612,-644.7126 4594,-600 4691.4556,-538.7465 4634.7869,-448.7366 4724,-376 4757.8157,-348.4297 4802.0716,-330.6908 4842.518,-319.3564"/>
+<polygon fill="#000000" stroke="#000000" points="4843.0736,-321.0187 4847.4307,-318.0057 4842.1456,-317.6439 4843.0736,-321.0187"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;io -->
+<g id="edge39" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3491.196,-674.4534C3094.4414,-668.989 1841.7309,-648.262 1440,-600 1090.9143,-558.0626 708,-651.5957 708,-300 708,-300 708,-300 708,-206 708,-76.3582 908.0683,-33.7909 987.7845,-21.9756"/>
+<polygon fill="#000000" stroke="#000000" points="988.1601,-23.6895 992.8578,-21.2412 987.6587,-20.2256 988.1601,-23.6895"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;github.com/containerd/containerd/content -->
+<g id="edge30" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;github.com/containerd/containerd/content</title>
+<path fill="none" stroke="#000000" d="M3491.2063,-668.9155C3193.0397,-651.5716 2428.6826,-607.1103 2124.5811,-589.4212"/>
+<polygon fill="#000000" stroke="#000000" points="2124.3454,-587.6546 2119.2522,-589.1112 2124.1421,-591.1487 2124.3454,-587.6546"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;github.com/containerd/containerd/errdefs -->
+<g id="edge31" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;github.com/containerd/containerd/errdefs</title>
+<path fill="none" stroke="#000000" d="M3543.9259,-657.9696C3500.2798,-645.0794 3443.5392,-625.5075 3397,-600 3350.6113,-574.5749 3303.6153,-534.3797 3276.7265,-509.6884"/>
+<polygon fill="#000000" stroke="#000000" points="3277.7682,-508.2682 3272.9077,-506.1615 3275.3935,-510.8395 3277.7682,-508.2682"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge34" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M3491.3502,-661.3216C3256.3551,-630.849 2725.462,-551.6624 2298,-412 2222.6955,-387.3962 2139.934,-345.6408 2093.3326,-320.6574"/>
+<polygon fill="#000000" stroke="#000000" points="2093.8933,-318.9719 2088.6609,-318.1441 2092.235,-322.0541 2093.8933,-318.9719"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge35" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M3491.4591,-673.8472C3148.1824,-667.2714 2175.7975,-645.0934 1860,-600 1716.7417,-579.5438 1552.9136,-533.7667 1465.5844,-507.5083"/>
+<polygon fill="#000000" stroke="#000000" points="1466.0064,-505.8077 1460.7141,-506.0402 1464.9963,-509.1588 1466.0064,-505.8077"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;github.com/pkg/errors -->
+<g id="edge36" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M3734.703,-670.8191C3989.5107,-659.5089 4571.7578,-631.1157 4768,-600 4935.575,-573.4297 4975.3866,-554.4175 5138,-506 5235.392,-477.0019 5347.0185,-437.3611 5411.4338,-413.8456"/>
+<polygon fill="#000000" stroke="#000000" points="5412.2757,-415.4012 5416.371,-412.041 5411.0741,-412.1139 5412.2757,-415.4012"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;time -->
+<g id="edge42" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3491.3994,-673.9778C3124.1018,-667.4347 2029.6154,-644.6294 1676,-600 1478.8989,-575.1241 1428.0919,-566.9255 1239,-506 1021.1423,-435.8062 767,-528.8867 767,-300 767,-300 767,-300 767,-206 767,-128.1229 1025.4001,-51.3413 1117.7082,-26.3972"/>
+<polygon fill="#000000" stroke="#000000" points="1118.3884,-28.0266 1122.7626,-25.0384 1117.4797,-24.6466 1118.3884,-28.0266"/>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;strings -->
+<g id="edge41" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3734.7956,-673.5673C4144.691,-665.0866 5458.9257,-635.4084 5646,-600 5867.4609,-558.0831 6418.9011,-373.022 6588,-224 6628.3063,-188.4792 6646.0859,-180.6498 6664,-130 6675.2966,-98.0605 6653.2405,-62.5623 6634.9244,-40.2647"/>
+<polygon fill="#000000" stroke="#000000" points="6636.1137,-38.9599 6631.5567,-36.2587 6633.4346,-41.2121 6636.1137,-38.9599"/>
+</g>
+<!-- github.com/containerd/containerd/platforms -->
+<g id="node31" class="node">
+<title>github.com/containerd/containerd/platforms</title>
+<g id="a_node31"><a xlink:href="https://godoc.org/github.com/containerd/containerd/platforms" xlink:title="github.com/containerd/containerd/platforms" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M4567.5,-600C4567.5,-600 4334.5,-600 4334.5,-600 4328.5,-600 4322.5,-594 4322.5,-588 4322.5,-588 4322.5,-576 4322.5,-576 4322.5,-570 4328.5,-564 4334.5,-564 4334.5,-564 4567.5,-564 4567.5,-564 4573.5,-564 4579.5,-570 4579.5,-576 4579.5,-576 4579.5,-588 4579.5,-588 4579.5,-594 4573.5,-600 4567.5,-600"/>
+<text text-anchor="middle" x="4451" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/platforms</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;github.com/containerd/containerd/platforms -->
+<g id="edge33" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;github.com/containerd/containerd/platforms</title>
+<path fill="none" stroke="#000000" d="M3734.5994,-662.7541C3871.5683,-647.777 4100.8089,-622.5383 4298,-600 4304.3525,-599.2739 4310.8529,-598.5265 4317.418,-597.768"/>
+<polygon fill="#000000" stroke="#000000" points="4317.6838,-599.499 4322.4496,-597.186 4317.2816,-596.0222 4317.6838,-599.499"/>
+</g>
+<!-- golang.org/x/sync/errgroup -->
+<g id="node32" class="node">
+<title>golang.org/x/sync/errgroup</title>
+<g id="a_node32"><a xlink:href="https://godoc.org/golang.org/x/sync/errgroup" xlink:title="golang.org/x/sync/errgroup" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M4942,-506C4942,-506 4802,-506 4802,-506 4796,-506 4790,-500 4790,-494 4790,-494 4790,-482 4790,-482 4790,-476 4796,-470 4802,-470 4802,-470 4942,-470 4942,-470 4948,-470 4954,-476 4954,-482 4954,-482 4954,-494 4954,-494 4954,-500 4948,-506 4942,-506"/>
+<text text-anchor="middle" x="4872" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/sync/errgroup</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;golang.org/x/sync/errgroup -->
+<g id="edge37" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;golang.org/x/sync/errgroup</title>
+<path fill="none" stroke="#000000" d="M3734.9512,-673.0492C3970.7669,-666.36 4480.925,-646.8295 4650,-600 4659.6044,-597.3398 4770.8962,-540.2248 4832.154,-508.6112"/>
+<polygon fill="#000000" stroke="#000000" points="4833.1969,-510.0423 4836.8371,-506.1937 4831.5914,-506.9322 4833.1969,-510.0423"/>
+</g>
+<!-- golang.org/x/sync/semaphore -->
+<g id="node33" class="node">
+<title>golang.org/x/sync/semaphore</title>
+<g id="a_node33"><a xlink:href="https://godoc.org/golang.org/x/sync/semaphore" xlink:title="golang.org/x/sync/semaphore" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M4828.5,-224C4828.5,-224 4675.5,-224 4675.5,-224 4669.5,-224 4663.5,-218 4663.5,-212 4663.5,-212 4663.5,-200 4663.5,-200 4663.5,-194 4669.5,-188 4675.5,-188 4675.5,-188 4828.5,-188 4828.5,-188 4834.5,-188 4840.5,-194 4840.5,-200 4840.5,-200 4840.5,-212 4840.5,-212 4840.5,-218 4834.5,-224 4828.5,-224"/>
+<text text-anchor="middle" x="4752" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/sync/semaphore</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;golang.org/x/sync/semaphore -->
+<g id="edge38" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;golang.org/x/sync/semaphore</title>
+<path fill="none" stroke="#000000" d="M3656.9522,-657.8634C3836.6756,-583.7019 4515.0372,-303.781 4703.2874,-226.1009"/>
+<polygon fill="#000000" stroke="#000000" points="4704.2243,-227.6075 4708.1787,-224.0825 4702.8892,-224.3721 4704.2243,-227.6075"/>
+</g>
+<!-- sort -->
+<g id="node34" class="node">
+<title>sort</title>
+<g id="a_node34"><a xlink:href="https://godoc.org/sort" xlink:title="sort" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M6105,-36C6105,-36 6075,-36 6075,-36 6069,-36 6063,-30 6063,-24 6063,-24 6063,-12 6063,-12 6063,-6 6069,0 6075,0 6075,0 6105,0 6105,0 6111,0 6117,-6 6117,-12 6117,-12 6117,-24 6117,-24 6117,-30 6111,-36 6105,-36"/>
+<text text-anchor="middle" x="6090" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sort</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/images&#45;&gt;sort -->
+<g id="edge40" class="edge">
+<title>github.com/containerd/containerd/images&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3734.7198,-675.4067C4135.7009,-672.8876 5412.7667,-660.2191 5820,-600 6164.9264,-548.9945 7197.618,-457.603 7317,-130 7322.4782,-114.967 7327.9584,-105.6582 7317,-94 7295.899,-71.5516 6314.1461,-27.7481 6122.3096,-19.3946"/>
+<polygon fill="#000000" stroke="#000000" points="6122.201,-17.6383 6117.1296,-19.1693 6122.0488,-21.135 6122.201,-17.6383"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;bufio -->
+<g id="edge48" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M4457.2985,-563.805C4471.5353,-524.9361 4509.5733,-432.7298 4568,-376 4671.5569,-275.4508 4780.5582,-347.6632 4855,-224 4917.1763,-120.7124 4754.837,-164.4686 4687,-130 4635.8697,-104.0202 4582.3241,-63.7418 4551.6765,-39.2274"/>
+<polygon fill="#000000" stroke="#000000" points="4552.683,-37.7913 4547.6892,-36.0236 4550.4907,-40.5196 4552.683,-37.7913"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;github.com/containerd/containerd/log -->
+<g id="edge50" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;github.com/containerd/containerd/log</title>
+<path fill="none" stroke="#000000" d="M4473.7187,-563.9126C4491.0069,-549.4272 4514.6946,-527.9621 4532,-506 4573.1472,-453.7806 4552.4793,-415.4397 4606,-376 4625.3419,-361.7469 4750.2634,-336.4681 4844.2684,-319.0013"/>
+<polygon fill="#000000" stroke="#000000" points="4844.6868,-320.7036 4849.2839,-318.0713 4844.0486,-317.2623 4844.6868,-320.7036"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;os -->
+<g id="edge53" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M4440.5305,-563.6051C4403.4981,-500.3088 4271.8066,-289.8069 4103,-188 4009.3996,-131.5498 3956.5746,-188.1171 3864,-130 3828.2877,-107.5803 3799.9092,-66.5233 3784.565,-40.8338"/>
+<polygon fill="#000000" stroke="#000000" points="3785.8966,-39.6459 3781.8527,-36.2238 3782.8799,-41.4207 3785.8966,-39.6459"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;strconv -->
+<g id="edge56" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M4322.2764,-575.3646C4084.3726,-562.9447 3559.7974,-534.8274 3118,-506 2683.3605,-477.6397 1579.9996,-456.3733 1167,-318 1019.937,-268.7273 983.5252,-238.8094 873,-130 844.4244,-101.868 816.9959,-64.2004 800.8204,-40.516"/>
+<polygon fill="#000000" stroke="#000000" points="802.1891,-39.4163 797.9333,-36.2617 799.2931,-41.3817 802.1891,-39.4163"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;github.com/containerd/containerd/errdefs -->
+<g id="edge49" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;github.com/containerd/containerd/errdefs</title>
+<path fill="none" stroke="#000000" d="M4322.3319,-571.8957C4091.4136,-553.7618 3609.4274,-515.9116 3380.2642,-497.9155"/>
+<polygon fill="#000000" stroke="#000000" points="3380.355,-496.1673 3375.2333,-497.5204 3380.0809,-499.6566 3380.355,-496.1673"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge51" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M4322.3532,-578.672C3944.6882,-568.8056 2806.3201,-538.3755 1862,-506 1760.6033,-502.5237 1646.6299,-498.0572 1557.6208,-494.4432"/>
+<polygon fill="#000000" stroke="#000000" points="1557.6665,-492.6937 1552.5996,-494.2391 1557.5244,-496.1908 1557.6665,-492.6937"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;github.com/pkg/errors -->
+<g id="edge52" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M4579.7226,-565.8313C4684.5946,-551.9978 4836.3282,-530.478 4968,-506 5118.9977,-477.9293 5293.8146,-436.4098 5390.6666,-412.6008"/>
+<polygon fill="#000000" stroke="#000000" points="5391.353,-414.2341 5395.7899,-411.3399 5390.5166,-410.8355 5391.353,-414.2341"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;strings -->
+<g id="edge57" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M4579.7388,-572.9867C4913.7607,-548.9063 5802.0793,-480.2494 6089,-412 6299.071,-362.0307 6357.6962,-344.32 6537,-224 6588.1908,-189.6489 6614.2516,-186.4624 6639,-130 6651.8968,-100.5766 6638.6278,-63.7835 6627.039,-40.5786"/>
+<polygon fill="#000000" stroke="#000000" points="6628.5928,-39.7734 6624.7519,-36.125 6625.4794,-41.3723 6628.5928,-39.7734"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;regexp -->
+<g id="edge54" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M4492.4289,-563.901C4605.1146,-515.0116 4927.156,-377.548 5202,-282 5287.4053,-252.3093 5311.1874,-252.492 5397,-224 5402.5773,-222.1482 5408.4729,-220.0797 5414.1678,-218.023"/>
+<polygon fill="#000000" stroke="#000000" points="5414.793,-219.6578 5418.8929,-216.3032 5413.5959,-216.3689 5414.793,-219.6578"/>
+</g>
+<!-- github.com/containerd/containerd/platforms&#45;&gt;runtime -->
+<g id="edge55" class="edge">
+<title>github.com/containerd/containerd/platforms&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M4579.5055,-576.8422C4788.1966,-567.6683 5213.5603,-545.6503 5572,-506 5945.212,-464.7156 6912.9149,-411.9129 7238,-224 7296.5341,-190.1648 7358.9214,-147.0439 7317,-94 7281.6896,-49.3212 7107.1387,-28.1276 7031.7624,-20.9457"/>
+<polygon fill="#000000" stroke="#000000" points="7031.6517,-19.1777 7026.51,-20.4527 7031.3245,-22.6624 7031.6517,-19.1777"/>
+</g>
+<!-- golang.org/x/sync/errgroup&#45;&gt;context -->
+<g id="edge207" class="edge">
+<title>golang.org/x/sync/errgroup&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M4789.8255,-486.0063C4492.5335,-478.5388 3473.5058,-450.6612 3150,-412 3042.2917,-399.1281 2765.2433,-371.7118 2671,-318 2603.7237,-279.6574 2609.0661,-243.4356 2555,-188 2536.6303,-169.165 2514.8336,-148.8191 2498.4805,-133.9228"/>
+<polygon fill="#000000" stroke="#000000" points="2499.3233,-132.3241 2494.4456,-130.2573 2496.9698,-134.9147 2499.3233,-132.3241"/>
+</g>
+<!-- golang.org/x/sync/errgroup&#45;&gt;sync -->
+<g id="edge208" class="edge">
+<title>golang.org/x/sync/errgroup&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M4908.0634,-469.9668C4983.5093,-432.5652 5163.9482,-344.8058 5320,-282 5392.8893,-252.6644 5418.3373,-262.195 5487,-224 5544.9895,-191.7421 5574.4424,-189.8984 5603,-130 5616.6941,-101.2772 5604.5527,-64.5209 5593.599,-41.1103"/>
+<polygon fill="#000000" stroke="#000000" points="5595.0388,-40.0673 5591.2893,-36.325 5591.8867,-41.5887 5595.0388,-40.0673"/>
+</g>
+<!-- golang.org/x/sync/semaphore&#45;&gt;container/list -->
+<g id="edge209" class="edge">
+<title>golang.org/x/sync/semaphore&#45;&gt;container/list</title>
+<path fill="none" stroke="#000000" d="M4704.4765,-187.9871C4660.7806,-171.4249 4596.2979,-146.9839 4552.344,-130.3239"/>
+<polygon fill="#000000" stroke="#000000" points="4552.9188,-128.6704 4547.6231,-128.5346 4551.6783,-131.9432 4552.9188,-128.6704"/>
+</g>
+<!-- golang.org/x/sync/semaphore&#45;&gt;context -->
+<g id="edge210" class="edge">
+<title>golang.org/x/sync/semaphore&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M4663.4131,-204.23C4377.3008,-198.1938 3452.166,-176.181 2688,-130 2623.9371,-126.1285 2549.2699,-119.325 2507.2246,-115.283"/>
+<polygon fill="#000000" stroke="#000000" points="2507.1572,-113.5184 2502.0122,-114.7799 2506.821,-117.0022 2507.1572,-113.5184"/>
+</g>
+<!-- golang.org/x/sync/semaphore&#45;&gt;sync -->
+<g id="edge211" class="edge">
+<title>golang.org/x/sync/semaphore&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M4809.5083,-187.9853C4886.811,-164.2111 5028.7802,-122.0947 5152,-94 5298.9164,-60.5023 5476.9718,-33.1153 5548.7301,-22.6142"/>
+<polygon fill="#000000" stroke="#000000" points="5549.0162,-24.3411 5553.7111,-21.8875 5548.5108,-20.8778 5549.0162,-24.3411"/>
+</g>
+<!-- github.com/containerd/containerd/labels -->
+<g id="node35" class="node">
+<title>github.com/containerd/containerd/labels</title>
+<g id="a_node35"><a xlink:href="https://godoc.org/github.com/containerd/containerd/labels" xlink:title="github.com/containerd/containerd/labels" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M5620,-600C5620,-600 5408,-600 5408,-600 5402,-600 5396,-594 5396,-588 5396,-588 5396,-576 5396,-576 5396,-570 5402,-564 5408,-564 5408,-564 5620,-564 5620,-564 5626,-564 5632,-570 5632,-576 5632,-576 5632,-588 5632,-588 5632,-594 5626,-600 5620,-600"/>
+<text text-anchor="middle" x="5514" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/labels</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/labels&#45;&gt;github.com/containerd/containerd/errdefs -->
+<g id="edge43" class="edge">
+<title>github.com/containerd/containerd/labels&#45;&gt;github.com/containerd/containerd/errdefs</title>
+<path fill="none" stroke="#000000" d="M5395.6746,-577.0785C5008.2098,-560.9627 3779.517,-509.8578 3380.4896,-493.2611"/>
+<polygon fill="#000000" stroke="#000000" points="3380.3228,-491.5027 3375.2544,-493.0433 3380.1773,-494.9997 3380.3228,-491.5027"/>
+</g>
+<!-- github.com/containerd/containerd/labels&#45;&gt;github.com/pkg/errors -->
+<g id="edge44" class="edge">
+<title>github.com/containerd/containerd/labels&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M5528.2881,-563.7154C5544.048,-541.1716 5565.3913,-501.9235 5551,-470 5540.6463,-447.0329 5519.5416,-428.2136 5500.8313,-415.0474"/>
+<polygon fill="#000000" stroke="#000000" points="5501.7438,-413.551 5496.6329,-412.1577 5499.7594,-416.4341 5501.7438,-413.551"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;bufio -->
+<g id="edge186" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M4950.2956,-187.8823C4902.7559,-173.4625 4835.6307,-152.0967 4778,-130 4697.3969,-99.0952 4605.3245,-56.164 4557.9243,-33.4786"/>
+<polygon fill="#000000" stroke="#000000" points="4558.4662,-31.7977 4553.201,-31.2143 4556.9531,-34.9538 4558.4662,-31.7977"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;bytes -->
+<g id="edge187" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M5008.5482,-187.9738C5002.0064,-153.8113 4987.8075,-79.6613 4980.5179,-41.5936"/>
+<polygon fill="#000000" stroke="#000000" points="4982.1535,-40.8295 4979.4943,-36.2479 4978.7159,-41.4878 4982.1535,-40.8295"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;context -->
+<g id="edge188" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M4929.4228,-195.1615C4905.4575,-192.3879 4879.2202,-189.7093 4855,-188 3893.9342,-120.1759 3649.9813,-183.2872 2688,-130 2623.9185,-126.4503 2549.2577,-119.5358 2507.2186,-115.3866"/>
+<polygon fill="#000000" stroke="#000000" points="2507.1553,-113.6218 2502.007,-114.8698 2506.8099,-117.1047 2507.1553,-113.6218"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;encoding/json -->
+<g id="edge189" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M5094.681,-190.048C5167.6634,-175.5983 5276.7939,-153.092 5371,-130 5428.8125,-115.8289 5442.3402,-108.7802 5500,-94 5596.419,-69.2844 5709.771,-43.5191 5773.9208,-29.2405"/>
+<polygon fill="#000000" stroke="#000000" points="5774.3323,-30.9419 5778.8332,-28.1482 5773.5725,-27.5253 5774.3323,-30.9419"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;fmt -->
+<g id="edge190" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M4929.3841,-195.6542C4905.4181,-192.89 4879.19,-190.0929 4855,-188 4415.444,-149.9694 4303.5562,-168.0278 3864,-130 3491.9236,-97.8101 3042.3637,-38.8556 2919.4065,-22.3797"/>
+<polygon fill="#000000" stroke="#000000" points="2919.329,-20.6037 2914.1407,-21.6733 2918.8636,-24.0726 2919.329,-20.6037"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;io -->
+<g id="edge192" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M4929.4215,-195.1807C4905.4562,-192.4076 4879.2191,-189.7243 4855,-188 3936.5195,-122.609 3701.8039,-204.2869 2784,-130 2664.2108,-120.3043 2635.7227,-104.4844 2516,-94 1892.0793,-39.3618 1729.6939,-111.8883 1108,-36 1089.3427,-33.7226 1068.7466,-29.5995 1052.1731,-25.8736"/>
+<polygon fill="#000000" stroke="#000000" points="1052.4293,-24.1372 1047.1654,-24.7325 1051.6516,-27.5497 1052.4293,-24.1372"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;os -->
+<g id="edge194" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M4929.3775,-195.7294C4905.4114,-192.9666 4879.1849,-190.1515 4855,-188 4652.06,-169.9463 4133.0332,-195.1825 3940,-130 3882.7878,-110.6809 3826.5009,-66.647 3795.8004,-39.9081"/>
+<polygon fill="#000000" stroke="#000000" points="3796.734,-38.399 3791.8222,-36.4149 3794.4246,-41.029 3796.734,-38.399"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;sync -->
+<g id="edge199" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M5094.7172,-191.0166C5156.6085,-178.4342 5242.6774,-158.0016 5315,-130 5346.5238,-117.7947 5351.2582,-108.0591 5382,-94 5439.4539,-67.7246 5508.8431,-42.6741 5548.6547,-28.9141"/>
+<polygon fill="#000000" stroke="#000000" points="5549.677,-30.4129 5553.8345,-27.1299 5548.5371,-27.1038 5549.677,-30.4129"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;time -->
+<g id="edge201" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M4929.4039,-195.4145C4905.4383,-192.6457 4879.2055,-189.9063 4855,-188 4257.6283,-140.9547 4106.5709,-157.9134 3508,-130 3189.2783,-115.1369 3109.6972,-109.3807 2791,-94 2142.8381,-62.719 1350.5377,-27.0067 1182.3252,-19.4502"/>
+<polygon fill="#000000" stroke="#000000" points="1182.1407,-17.6903 1177.0671,-19.2141 1181.9836,-21.1868 1182.1407,-17.6903"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;strings -->
+<g id="edge198" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M5094.7534,-191.3629C5102.2669,-190.1703 5109.7674,-189.0298 5117,-188 5702.9958,-104.5673 6422.7257,-35.802 6581.8753,-20.9659"/>
+<polygon fill="#000000" stroke="#000000" points="6582.0444,-22.7078 6586.8606,-20.5017 6581.7199,-19.2229 6582.0444,-22.7078"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;sort -->
+<g id="edge197" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M5094.5358,-191.8406C5102.1305,-190.5408 5109.7075,-189.2453 5117,-188 5268.5387,-162.1235 5306.5345,-156.3019 5458,-130 5688.0499,-90.052 5964.9174,-40.4643 6057.8372,-23.7808"/>
+<polygon fill="#000000" stroke="#000000" points="6058.2965,-25.4764 6062.9085,-22.8701 6057.6779,-22.0315 6058.2965,-25.4764"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;sync/atomic -->
+<g id="edge200" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;sync/atomic</title>
+<path fill="none" stroke="#000000" d="M4929.3492,-193.0786C4887.4079,-182.5813 4838.5801,-163.7196 4806,-130 4782.6197,-105.802 4774.0097,-66.5266 4770.8415,-41.4972"/>
+<polygon fill="#000000" stroke="#000000" points="4772.5625,-41.1464 4770.2446,-36.383 4769.0861,-41.5522 4772.5625,-41.1464"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;runtime -->
+<g id="edge196" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M5094.9792,-202.1258C5375.3404,-188.9464 6277.1331,-145.7927 6407,-130 6620.0577,-104.0908 6871.9738,-47.4279 6962.484,-26.2251"/>
+<polygon fill="#000000" stroke="#000000" points="6962.9753,-27.9074 6967.443,-25.061 6962.1754,-24.5001 6962.9753,-27.9074"/>
+</g>
+<!-- log -->
+<g id="node52" class="node">
+<title>log</title>
+<g id="a_node52"><a xlink:href="https://godoc.org/log" xlink:title="log" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3935,-36C3935,-36 3905,-36 3905,-36 3899,-36 3893,-30 3893,-24 3893,-24 3893,-12 3893,-12 3893,-6 3899,0 3905,0 3905,0 3935,0 3935,0 3941,0 3947,-6 3947,-12 3947,-12 3947,-24 3947,-24 3947,-30 3941,-36 3935,-36"/>
+<text text-anchor="middle" x="3920" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">log</text>
+</a>
+</g>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;log -->
+<g id="edge193" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;log</title>
+<path fill="none" stroke="#000000" d="M4929.3737,-195.7719C4905.4075,-193.0099 4879.1819,-190.1846 4855,-188 4757.7388,-179.2135 4059.7133,-183.4778 3978,-130 3947.0816,-109.7652 3931.7227,-67.5412 3924.8449,-41.11"/>
+<polygon fill="#000000" stroke="#000000" points="3926.4896,-40.4726 3923.5803,-36.0455 3923.0938,-41.3205 3926.4896,-40.4726"/>
+</g>
+<!-- reflect -->
+<g id="node54" class="node">
+<title>reflect</title>
+<g id="a_node54"><a xlink:href="https://godoc.org/reflect" xlink:title="reflect" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4228,-36C4228,-36 4198,-36 4198,-36 4192,-36 4186,-30 4186,-24 4186,-24 4186,-12 4186,-12 4186,-6 4192,0 4198,0 4198,0 4228,0 4228,0 4234,0 4240,-6 4240,-12 4240,-12 4240,-24 4240,-24 4240,-30 4234,-36 4228,-36"/>
+<text text-anchor="middle" x="4213" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">reflect</text>
+</a>
+</g>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;reflect -->
+<g id="edge195" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M4929.5706,-187.9369C4863.7053,-173.212 4769.1441,-151.4186 4687,-130 4631.0568,-115.4131 4618.1579,-107.7373 4562,-94 4435.5929,-63.0784 4401.3739,-67.0571 4275,-36 4265.1751,-33.5855 4254.5704,-30.6358 4244.9574,-27.8285"/>
+<polygon fill="#000000" stroke="#000000" points="4245.3209,-26.1113 4240.0302,-26.3771 4244.3319,-29.4687 4245.3209,-26.1113"/>
+</g>
+<!-- golang.org/x/sys/unix -->
+<g id="node62" class="node">
+<title>golang.org/x/sys/unix</title>
+<g id="a_node62"><a xlink:href="https://godoc.org/golang.org/x/sys/unix" xlink:title="golang.org/x/sys/unix" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M5289,-130C5289,-130 5179,-130 5179,-130 5173,-130 5167,-124 5167,-118 5167,-118 5167,-106 5167,-106 5167,-100 5173,-94 5179,-94 5179,-94 5289,-94 5289,-94 5295,-94 5301,-100 5301,-106 5301,-106 5301,-118 5301,-118 5301,-124 5295,-130 5289,-130"/>
+<text text-anchor="middle" x="5234" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/sys/unix</text>
+</a>
+</g>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;golang.org/x/sys/unix -->
+<g id="edge191" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;golang.org/x/sys/unix</title>
+<path fill="none" stroke="#000000" d="M5054.5412,-187.9871C5092.2879,-172.0042 5147.3632,-148.6841 5186.5296,-132.1001"/>
+<polygon fill="#000000" stroke="#000000" points="5187.303,-133.6731 5191.2249,-130.112 5185.9383,-130.4501 5187.303,-133.6731"/>
+</g>
+<!-- github.com/containerd/containerd/reference -->
+<g id="node40" class="node">
+<title>github.com/containerd/containerd/reference</title>
+<g id="a_node40"><a xlink:href="https://godoc.org/github.com/containerd/containerd/reference" xlink:title="github.com/containerd/containerd/reference" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M5982,-412C5982,-412 5752,-412 5752,-412 5746,-412 5740,-406 5740,-400 5740,-400 5740,-388 5740,-388 5740,-382 5746,-376 5752,-376 5752,-376 5982,-376 5982,-376 5988,-376 5994,-382 5994,-388 5994,-388 5994,-400 5994,-400 5994,-406 5988,-412 5982,-412"/>
+<text text-anchor="middle" x="5867" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/reference</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/reference&#45;&gt;errors -->
+<g id="edge58" class="edge">
+<title>github.com/containerd/containerd/reference&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M5841.2979,-375.8358C5782.0108,-334.9163 5629.7974,-235.4049 5487,-188 5472.97,-183.3424 4535.9512,-49.291 4349.1765,-22.5971"/>
+<polygon fill="#000000" stroke="#000000" points="4349.3255,-20.8507 4344.1282,-21.8756 4348.8303,-24.3155 4349.3255,-20.8507"/>
+</g>
+<!-- github.com/containerd/containerd/reference&#45;&gt;fmt -->
+<g id="edge59" class="edge">
+<title>github.com/containerd/containerd/reference&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M5739.7534,-385.9888C5681.3487,-382.5254 5611.1797,-378.6645 5548,-376 4625.7931,-337.107 4388.1865,-430.1635 3472,-318 3278.2564,-294.2811 3209.7168,-325.512 3043,-224 2968.8745,-178.8658 2917.6962,-84.3114 2897.1123,-40.8006"/>
+<polygon fill="#000000" stroke="#000000" points="2898.6885,-40.0397 2894.9845,-36.2525 2895.5183,-41.523 2898.6885,-40.0397"/>
+</g>
+<!-- github.com/containerd/containerd/reference&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge60" class="edge">
+<title>github.com/containerd/containerd/reference&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M5739.7607,-385.8088C5681.3575,-382.3092 5611.1876,-378.4696 5548,-376 5204.848,-362.5884 2734.7241,-313.4034 2170.5403,-302.2566"/>
+<polygon fill="#000000" stroke="#000000" points="2170.243,-300.5004 2165.2094,-302.1512 2170.1738,-303.9998 2170.243,-300.5004"/>
+</g>
+<!-- github.com/containerd/containerd/reference&#45;&gt;strings -->
+<g id="edge64" class="edge">
+<title>github.com/containerd/containerd/reference&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M5938.2336,-375.9356C6099.4178,-333.7744 6485.2279,-225.2244 6577,-130 6600.3494,-105.7722 6608.9681,-66.5052 6612.1467,-41.4858"/>
+<polygon fill="#000000" stroke="#000000" points="6613.9018,-41.5434 6612.7457,-36.3737 6610.4256,-41.136 6613.9018,-41.5434"/>
+</g>
+<!-- github.com/containerd/containerd/reference&#45;&gt;regexp -->
+<g id="edge63" class="edge">
+<title>github.com/containerd/containerd/reference&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M5785.9056,-375.9744C5735.3849,-363.1943 5669.8041,-343.7397 5615,-318 5561.1841,-292.7243 5504.8086,-252.0421 5472.6813,-227.289"/>
+<polygon fill="#000000" stroke="#000000" points="5473.528,-225.7313 5468.503,-224.054 5471.3853,-228.4988 5473.528,-225.7313"/>
+</g>
+<!-- net/url -->
+<g id="node41" class="node">
+<title>net/url</title>
+<g id="a_node41"><a xlink:href="https://godoc.org/net/url" xlink:title="net/url" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M7290,-130C7290,-130 7260,-130 7260,-130 7254,-130 7248,-124 7248,-118 7248,-118 7248,-106 7248,-106 7248,-100 7254,-94 7260,-94 7260,-94 7290,-94 7290,-94 7296,-94 7302,-100 7302,-106 7302,-106 7302,-118 7302,-118 7302,-124 7296,-130 7290,-130"/>
+<text text-anchor="middle" x="7275" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/url</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/reference&#45;&gt;net/url -->
+<g id="edge61" class="edge">
+<title>github.com/containerd/containerd/reference&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M5994.0069,-383.7068C6306.7824,-357.5239 7096.2659,-286.0623 7198,-224 7231.9449,-203.2921 7254.5649,-161.3174 7266.0611,-135.0462"/>
+<polygon fill="#000000" stroke="#000000" points="7267.7201,-135.6175 7268.0785,-130.3322 7264.5024,-134.2404 7267.7201,-135.6175"/>
+</g>
+<!-- github.com/containerd/containerd/reference&#45;&gt;path -->
+<g id="edge62" class="edge">
+<title>github.com/containerd/containerd/reference&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M5896.6927,-375.8759C5923.2976,-359.6366 5962.2044,-335.8882 5989.3825,-319.299"/>
+<polygon fill="#000000" stroke="#000000" points="5990.6209,-320.5934 5993.977,-316.4946 5988.7974,-317.6059 5990.6209,-320.5934"/>
+</g>
+<!-- github.com/containerd/containerd/remotes -->
+<g id="node43" class="node">
+<title>github.com/containerd/containerd/remotes</title>
+<g id="a_node43"><a xlink:href="https://godoc.org/github.com/containerd/containerd/remotes" xlink:title="github.com/containerd/containerd/remotes" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3725,-788C3725,-788 3501,-788 3501,-788 3495,-788 3489,-782 3489,-776 3489,-776 3489,-764 3489,-764 3489,-758 3495,-752 3501,-752 3501,-752 3725,-752 3725,-752 3731,-752 3737,-758 3737,-764 3737,-764 3737,-776 3737,-776 3737,-782 3731,-788 3725,-788"/>
+<text text-anchor="middle" x="3613" y="-766.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/remotes</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;context -->
+<g id="edge65" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M3488.7927,-757.9452C3233.5893,-731.4978 2674,-664.1409 2674,-582 2674,-582 2674,-582 2674,-488 2674,-436.4674 2677.6692,-415.0127 2644,-376 2601.6004,-326.8713 2554.4569,-365.2752 2510,-318 2462.5354,-267.5264 2465.8312,-177.8284 2470.5842,-135.4353"/>
+<polygon fill="#000000" stroke="#000000" points="2472.3466,-135.4336 2471.1982,-130.2622 2468.871,-135.021 2472.3466,-135.4336"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;fmt -->
+<g id="edge66" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3488.625,-763.9719C3298.7184,-753.7603 2945.7825,-730.5358 2825,-694 2570.7311,-617.0855 2434.5175,-639.8848 2298,-412 2219.2715,-280.5806 2305.9019,-182.4291 2431,-94 2499.8245,-45.3495 2762.6518,-25.3717 2854.8259,-19.7656"/>
+<polygon fill="#000000" stroke="#000000" points="2854.9821,-21.5095 2859.8682,-19.463 2854.7723,-18.0158 2854.9821,-21.5095"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/log -->
+<g id="edge70" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/log</title>
+<path fill="none" stroke="#000000" d="M3737.3654,-758.5467C3974.1272,-735.2487 4479.9371,-677.8745 4636,-600 4645.0245,-595.4968 4858.4388,-389.5064 4928.7577,-321.5187"/>
+<polygon fill="#000000" stroke="#000000" points="4930.0148,-322.7374 4932.3928,-318.0037 4927.5818,-320.2213 4930.0148,-322.7374"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;io -->
+<g id="edge75" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3488.6993,-765.4998C2993.2005,-747.1027 1173.7861,-675.0242 927,-600 676.8598,-523.9561 460.6046,-295.6565 627,-94 650.119,-65.9818 897.6287,-33.034 987.5704,-21.8989"/>
+<polygon fill="#000000" stroke="#000000" points="988.0066,-23.6084 992.7548,-21.2596 987.5782,-20.1347 988.0066,-23.6084"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;sync -->
+<g id="edge77" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3737.091,-766.9878C3903.8116,-761.111 4208.6992,-743.92 4464,-694 4977.7539,-593.5436 5132.3449,-547.3801 5544,-224 5588.2929,-189.2052 5608.759,-182.5628 5629,-130 5641.1665,-98.4055 5619.362,-62.5424 5601.3058,-40.0954"/>
+<polygon fill="#000000" stroke="#000000" points="5602.516,-38.812 5597.9869,-36.0642 5599.8139,-41.0366 5602.516,-38.812"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/content -->
+<g id="edge67" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/content</title>
+<path fill="none" stroke="#000000" d="M3488.9905,-766.637C3288.1743,-759.9821 2879.7171,-741.4268 2537,-694 2370.1199,-670.9063 2177.5341,-626.6471 2073.4178,-601.2278"/>
+<polygon fill="#000000" stroke="#000000" points="2073.7998,-599.5197 2068.5272,-600.0318 2072.9683,-602.9195 2073.7998,-599.5197"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/errdefs -->
+<g id="edge68" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/errdefs</title>
+<path fill="none" stroke="#000000" d="M3572.218,-751.8387C3514.7297,-724.8126 3408.7669,-669.6355 3336,-600 3307.4801,-572.7073 3281.8851,-534.4863 3267.1486,-510.4965"/>
+<polygon fill="#000000" stroke="#000000" points="3268.6204,-509.5487 3264.5251,-506.1885 3265.6311,-511.3692 3268.6204,-509.5487"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge72" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M3488.5587,-765.9854C3259.8601,-757.8536 2757.5078,-736.4871 2336,-694 2041.2003,-664.2848 1961.92,-677.7149 1676,-600 1590.5823,-576.7829 1496.1043,-533.885 1443.7081,-508.4495"/>
+<polygon fill="#000000" stroke="#000000" points="1444.3419,-506.8117 1439.0804,-506.1952 1442.8091,-509.9582 1444.3419,-506.8117"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;github.com/pkg/errors -->
+<g id="edge73" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M3737.044,-768.1019C4024.6262,-762.8781 4729.9151,-745.113 4961,-694 5071.2384,-669.6167 5098.8634,-655.8236 5197,-600 5295.0397,-544.2315 5396.9303,-456.0869 5441.6276,-415.6157"/>
+<polygon fill="#000000" stroke="#000000" points="5442.9917,-416.7408 5445.5166,-412.0838 5440.6387,-414.1498 5442.9917,-416.7408"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;strings -->
+<g id="edge76" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3737.2736,-766.7935C3986.0064,-759.6275 4562.5779,-739.2573 5045,-694 5740.3951,-628.7632 5953.5604,-697.4836 6591,-412 6763.1318,-334.9091 6984.9617,-245.0364 6872,-94 6844.73,-57.5384 6709.2853,-32.5244 6646.3035,-22.6762"/>
+<polygon fill="#000000" stroke="#000000" points="6646.3764,-20.9167 6641.1676,-21.8817 6645.8412,-24.3756 6646.3764,-20.9167"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/images -->
+<g id="edge69" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/images</title>
+<path fill="none" stroke="#000000" d="M3613,-751.8759C3613,-736.9211 3613,-715.5983 3613,-699.3629"/>
+<polygon fill="#000000" stroke="#000000" points="3614.7501,-699.0685 3613,-694.0685 3611.2501,-699.0685 3614.7501,-699.0685"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/platforms -->
+<g id="edge71" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;github.com/containerd/containerd/platforms</title>
+<path fill="none" stroke="#000000" d="M3693.351,-751.9738C3851.7051,-716.448 4202.8112,-637.6796 4365.3572,-601.2134"/>
+<polygon fill="#000000" stroke="#000000" points="4366.0896,-602.8427 4370.5852,-600.0406 4365.3234,-599.4276 4366.0896,-602.8427"/>
+</g>
+<!-- github.com/containerd/containerd/remotes&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge74" class="edge">
+<title>github.com/containerd/containerd/remotes&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M3737.0407,-765.2426C3888.6146,-757.6835 4152.2804,-738.8783 4374,-694 4512.6118,-665.9436 4543.1754,-642.6933 4678,-600 4807.1693,-559.0975 4849.6229,-571.9135 4968,-506 5024.481,-474.5508 5051.812,-470.6513 5079,-412 5087.4344,-393.805 5093.5239,-327.7361 5076,-282 5067.9893,-261.0926 5051.6494,-241.8489 5037.4857,-227.9565"/>
+<polygon fill="#000000" stroke="#000000" points="5038.3755,-226.3847 5033.5572,-224.1831 5035.9509,-228.9089 5038.3755,-226.3847"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker -->
+<g id="node44" class="node">
+<title>github.com/containerd/containerd/remotes/docker</title>
+<g id="a_node44"><a xlink:href="https://godoc.org/github.com/containerd/containerd/remotes/docker" xlink:title="github.com/containerd/containerd/remotes/docker" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M5989,-976C5989,-976 5725,-976 5725,-976 5719,-976 5713,-970 5713,-964 5713,-964 5713,-952 5713,-952 5713,-946 5719,-940 5725,-940 5725,-940 5989,-940 5989,-940 5995,-940 6001,-946 6001,-952 6001,-952 6001,-964 6001,-964 6001,-970 5995,-976 5989,-976"/>
+<text text-anchor="middle" x="5857" y="-954.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/remotes/docker</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;bytes -->
+<g id="edge78" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M5893.8804,-939.8635C5917.2107,-926.7 5946.3763,-906.8651 5965,-882 6049.7537,-768.8427 6050.0175,-682.5582 5973,-564 5936.2835,-507.4799 5911.6156,-504.9723 5854,-470 5800.263,-437.3819 5775.5243,-449.4019 5725,-412 5618.6768,-333.2915 5644.9454,-251.6892 5529,-188 5380.4141,-106.3814 5310.5403,-190.0361 5152,-130 5124.3294,-119.5217 5120.9429,-109.9157 5096,-94 5066.0572,-74.8939 5031.8209,-53.2288 5007.6013,-37.9325"/>
+<polygon fill="#000000" stroke="#000000" points="5008.3609,-36.3425 5003.1988,-35.1525 5006.4921,-39.3019 5008.3609,-36.3425"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;context -->
+<g id="edge79" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M5712.7449,-956.193C5121.0696,-948.6011 2878.1511,-918.0075 2170,-882 1912.1533,-868.8892 1187.0196,-977.8024 1012,-788 931.5387,-700.7426 342.8116,-1264.2126 1092,-376 1273.3864,-160.9544 2247.2244,-119.1609 2440.6036,-112.9436"/>
+<polygon fill="#000000" stroke="#000000" points="2440.8874,-114.6856 2445.8295,-112.7782 2440.7766,-111.1874 2440.8874,-114.6856"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;encoding/base64 -->
+<g id="edge80" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;encoding/base64</title>
+<path fill="none" stroke="#000000" d="M5811.2828,-939.912C5782.0918,-926.8279 5744.6724,-907.0636 5717,-882 5688.4259,-856.1196 5665.2621,-817.4112 5652.3098,-792.9464"/>
+<polygon fill="#000000" stroke="#000000" points="5653.7233,-791.8727 5649.8569,-788.2514 5650.6211,-793.4934 5653.7233,-791.8727"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;encoding/json -->
+<g id="edge81" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M6001.0276,-949.9455C6141.861,-940.0217 6344.903,-919.6482 6414,-882 6471.5703,-850.6322 6510,-835.5612 6510,-770 6510,-770 6510,-770 6510,-488 6510,-240.3471 6310.8683,-226.9948 6083,-130 6008.5721,-98.3189 5922.4352,-60.7412 5870.9084,-38.1641"/>
+<polygon fill="#000000" stroke="#000000" points="5871.3135,-36.431 5866.0315,-36.0267 5869.9085,-39.6367 5871.3135,-36.431"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;fmt -->
+<g id="edge82" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M5712.8551,-957.8407C4880.5707,-956.686 722.5806,-947.636 609,-882 506.1407,-822.5596 472,-565.1488 472,-488 472,-488 472,-488 472,-394 472,-286.0484 806.213,-154.2271 1062,-94 1242.9819,-51.3864 2624.0586,-23.0658 2854.4576,-18.6149"/>
+<polygon fill="#000000" stroke="#000000" points="2854.7196,-20.3603 2859.6849,-18.5142 2854.6521,-16.8609 2854.7196,-20.3603"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/log -->
+<g id="edge87" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/log</title>
+<path fill="none" stroke="#000000" d="M5852.0515,-939.9706C5840.3822,-900.6129 5807.5481,-806.1972 5748,-752 5617.4339,-633.1662 5535.8577,-684.7826 5381,-600 5218.4948,-511.0304 5042.5863,-373.9018 4977.3057,-321.4285"/>
+<polygon fill="#000000" stroke="#000000" points="4978.1591,-319.8688 4973.1671,-318.096 4975.964,-322.5949 4978.1591,-319.8688"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;io -->
+<g id="edge98" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M5712.6806,-957.8779C4874.1611,-956.932 657.0564,-948.8558 392,-882 287.5284,-855.6489 177,-877.7436 177,-770 177,-770 177,-770 177,-582 177,-453.9529 236,-428.0471 236,-300 236,-300 236,-300 236,-206 236,-133.0461 289.762,-124.5744 356,-94 385.0945,-80.5705 858.5389,-33.714 987.6817,-21.1307"/>
+<polygon fill="#000000" stroke="#000000" points="988.0446,-22.8538 992.8515,-20.6274 987.7054,-19.3702 988.0446,-22.8538"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;sync -->
+<g id="edge105" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M6001.0118,-941.6052C6104.2416,-928.0996 6233.1649,-907.1241 6281,-882 6343.0455,-849.4123 6392,-840.0828 6392,-770 6392,-770 6392,-770 6392,-488 6392,-180.4017 6106.3403,-213.7288 5823,-94 5750.0075,-63.1562 5660.0626,-38.1232 5613.0607,-25.9713"/>
+<polygon fill="#000000" stroke="#000000" points="5613.3752,-24.2453 5608.0968,-24.6946 5612.5033,-27.635 5613.3752,-24.2453"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/content -->
+<g id="edge83" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/content</title>
+<path fill="none" stroke="#000000" d="M5712.7483,-957.4584C5061.6795,-954.6949 2421.4798,-939.9455 2256,-882 2137.7688,-840.5993 2114.6068,-798.8129 2046,-694 2027.3349,-665.4846 2012.8063,-628.6755 2004.5687,-605.1968"/>
+<polygon fill="#000000" stroke="#000000" points="2006.1965,-604.5493 2002.9071,-600.3968 2002.889,-605.6942 2006.1965,-604.5493"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/errdefs -->
+<g id="edge84" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/errdefs</title>
+<path fill="none" stroke="#000000" d="M5712.6814,-955.2407C5164.4755,-943.8244 3242,-894.8858 3242,-770 3242,-770 3242,-770 3242,-676 3242,-616.705 3248.0187,-547.1037 3251.5311,-511.5337"/>
+<polygon fill="#000000" stroke="#000000" points="3253.3158,-511.2733 3252.072,-506.124 3249.8331,-510.925 3253.3158,-511.2733"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge93" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M5712.6937,-957.1814C4877.3713,-952.2881 697.3237,-925.6344 649,-882 279.3722,-548.2406 1084.0163,-378.1375 1092,-376 1453.4906,-279.2178 1559.8999,-357.7966 1932,-318 1935.1938,-317.6584 1938.4317,-317.2964 1941.698,-316.9173"/>
+<polygon fill="#000000" stroke="#000000" points="1942.0918,-318.633 1946.8518,-316.3082 1941.681,-315.1572 1942.0918,-318.633"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge94" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M5712.8296,-957.0129C4881.7833,-951.187 738.0921,-920.238 686,-882 643.7396,-850.979 649,-822.4237 649,-770 649,-770 649,-770 649,-676 649,-553.6886 1027.2628,-510.3263 1247.9489,-495.4358"/>
+<polygon fill="#000000" stroke="#000000" points="1248.2602,-497.169 1253.1324,-495.0894 1248.0268,-493.6768 1248.2602,-497.169"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/pkg/errors -->
+<g id="edge95" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M5866.7427,-939.916C5883.5003,-907.0719 5916,-834.996 5916,-770 5916,-770 5916,-770 5916,-676 5916,-500.6663 5667.9148,-430.291 5539.7403,-405.6317"/>
+<polygon fill="#000000" stroke="#000000" points="5539.7192,-403.8465 5534.4804,-404.6325 5539.0659,-407.285 5539.7192,-403.8465"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;io/ioutil -->
+<g id="edge99" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M5712.7605,-956.988C4844.7911,-950.785 349.2635,-917.0122 212,-882 108.7432,-855.662 0,-876.5629 0,-770 0,-770 0,-770 0,-206 0,-140.9881 43.6996,-74.2442 69.5096,-40.4935"/>
+<polygon fill="#000000" stroke="#000000" points="71.1951,-41.175 72.8723,-36.1501 68.4276,-39.0324 71.1951,-41.175"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;time -->
+<g id="edge106" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M5712.8875,-957.8482C4890.6398,-956.7414 819.9318,-947.9386 564,-882 461.5673,-855.6091 354,-875.7778 354,-770 354,-770 354,-770 354,-582 354,-453.9529 413,-428.0471 413,-300 413,-300 413,-300 413,-206 413,-169.0883 975.3755,-53.2085 1117.6191,-24.4889"/>
+<polygon fill="#000000" stroke="#000000" points="1118.3405,-26.1287 1122.8957,-23.4246 1117.6484,-22.6978 1118.3405,-26.1287"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;strings -->
+<g id="edge104" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M6001.2152,-948.3295C6192.9633,-934.7212 6521.3367,-908.597 6639,-882 6781.4815,-849.7931 6948,-916.0763 6948,-770 6948,-770 6948,-770 6948,-300 6948,-206.3793 6968.6641,-161.7008 6904,-94 6868.4379,-56.7679 6714.6337,-31.6915 6646.5909,-22.2199"/>
+<polygon fill="#000000" stroke="#000000" points="6646.6433,-20.4607 6641.4512,-21.5115 6646.1654,-23.9279 6646.6433,-20.4607"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/images -->
+<g id="edge85" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/images</title>
+<path fill="none" stroke="#000000" d="M5833.6601,-939.8145C5816.2664,-925.4398 5792.7286,-904.1634 5776,-882 5736.8413,-830.1195 5763.878,-786.8338 5709,-752 5625.6974,-699.1236 4180.5748,-681.3019 3739.6784,-677.0819"/>
+<polygon fill="#000000" stroke="#000000" points="3739.5543,-675.3307 3734.5378,-677.0328 3739.5209,-678.8305 3739.5543,-675.3307"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;sort -->
+<g id="edge103" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M6001.0248,-956.1323C6395.4646,-949.1192 7464,-916.5925 7464,-770 7464,-770 7464,-770 7464,-206 7464,-132.3938 7409.7482,-122.776 7342,-94 7284.1662,-69.4351 6312.6485,-27.3767 6122.231,-19.3463"/>
+<polygon fill="#000000" stroke="#000000" points="6122.1578,-17.5918 6117.0885,-19.1297 6122.0104,-21.0887 6122.1578,-17.5918"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/labels -->
+<g id="edge86" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/labels</title>
+<path fill="none" stroke="#000000" d="M5858.6254,-939.7901C5861.0978,-901.2892 5861.7387,-810.3776 5820,-752 5762.8732,-672.0999 5655.9127,-625.3281 5584.8115,-601.7014"/>
+<polygon fill="#000000" stroke="#000000" points="5585.0518,-599.9383 5579.7553,-600.0401 5583.9592,-603.2634 5585.0518,-599.9383"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge96" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M5878.3689,-939.8456C5912.3733,-908.8006 5975,-841.8685 5975,-770 5975,-770 5975,-770 5975,-676 5975,-556.4411 5904.9717,-530.7548 5802,-470 5675.3558,-395.2781 5251.3659,-272.6684 5082.3839,-225.4123"/>
+<polygon fill="#000000" stroke="#000000" points="5082.7459,-223.6965 5077.4594,-224.0363 5081.8039,-227.0674 5082.7459,-223.6965"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/reference -->
+<g id="edge88" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/reference</title>
+<path fill="none" stroke="#000000" d="M6001.2512,-942.9087C6058.1807,-932.132 6122.214,-913.6604 6174,-882 6229.9357,-847.8025 6270,-835.5612 6270,-770 6270,-770 6270,-770 6270,-676 6270,-522.7086 6071.8424,-445.7373 5953.4304,-413.4265"/>
+<polygon fill="#000000" stroke="#000000" points="5953.8365,-411.7235 5948.5531,-412.1085 5952.9234,-415.1023 5953.8365,-411.7235"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;net/url -->
+<g id="edge101" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M6001.2543,-955.3986C6273.1337,-949.4027 6851.4461,-931.1484 7045,-882 7159.8704,-852.8314 7287,-888.5159 7287,-770 7287,-770 7287,-770 7287,-300 7287,-240.705 7280.9813,-171.1037 7277.4689,-135.5337"/>
+<polygon fill="#000000" stroke="#000000" points="7279.1669,-134.925 7276.928,-130.124 7275.6842,-135.2733 7279.1669,-134.925"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;path -->
+<g id="edge102" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M5983.9901,-939.9559C6073.338,-925.734 6184.0039,-904.5893 6225,-882 6285.5651,-848.628 6333,-839.1508 6333,-770 6333,-770 6333,-770 6333,-582 6333,-432.3234 6132.9317,-341.5311 6053.2155,-311.296"/>
+<polygon fill="#000000" stroke="#000000" points="6053.4383,-309.5105 6048.1422,-309.3921 6052.2085,-312.7873 6053.4383,-309.5105"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/remotes -->
+<g id="edge89" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/remotes</title>
+<path fill="none" stroke="#000000" d="M5712.7849,-945.9178C5302.5821,-911.5514 4133.6827,-813.6223 3742.4045,-780.8414"/>
+<polygon fill="#000000" stroke="#000000" points="3742.3976,-779.0848 3737.2689,-780.4111 3742.1053,-782.5725 3742.3976,-779.0848"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1 -->
+<g id="node45" class="node">
+<title>github.com/containerd/containerd/remotes/docker/schema1</title>
+<g id="a_node45"><a xlink:href="https://godoc.org/github.com/containerd/containerd/remotes/docker/schema1" xlink:title="github.com/containerd/containerd/remotes/docker/schema1" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2599,-882C2599,-882 2283,-882 2283,-882 2277,-882 2271,-876 2271,-870 2271,-870 2271,-858 2271,-858 2271,-852 2277,-846 2283,-846 2283,-846 2599,-846 2599,-846 2605,-846 2611,-852 2611,-858 2611,-858 2611,-870 2611,-870 2611,-876 2605,-882 2599,-882"/>
+<text text-anchor="middle" x="2441" y="-860.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/remotes/docker/schema1</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/remotes/docker/schema1 -->
+<g id="edge90" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/remotes/docker/schema1</title>
+<path fill="none" stroke="#000000" d="M5712.9438,-954.0359C5165.3868,-938.9685 3214.2719,-885.2786 2616.4137,-868.827"/>
+<polygon fill="#000000" stroke="#000000" points="2616.225,-867.0712 2611.1788,-868.6829 2616.1287,-870.5699 2616.225,-867.0712"/>
+</g>
+<!-- github.com/containerd/containerd/version -->
+<g id="node46" class="node">
+<title>github.com/containerd/containerd/version</title>
+<g id="a_node46"><a xlink:href="https://godoc.org/github.com/containerd/containerd/version" xlink:title="github.com/containerd/containerd/version" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M7799,-882C7799,-882 7579,-882 7579,-882 7573,-882 7567,-876 7567,-870 7567,-870 7567,-858 7567,-858 7567,-852 7573,-846 7579,-846 7579,-846 7799,-846 7799,-846 7805,-846 7811,-852 7811,-858 7811,-858 7811,-870 7811,-870 7811,-876 7805,-882 7799,-882"/>
+<text text-anchor="middle" x="7689" y="-860.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/version</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/version -->
+<g id="edge91" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/containerd/containerd/version</title>
+<path fill="none" stroke="#000000" d="M6001.1996,-955.3635C6293.0883,-949.176 6974.6468,-930.4099 7545,-882 7550.4778,-881.5351 7556.068,-881.0186 7561.7116,-880.4628"/>
+<polygon fill="#000000" stroke="#000000" points="7562.1075,-882.182 7566.9079,-879.9416 7561.7582,-878.6994 7562.1075,-882.182"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode -->
+<g id="node47" class="node">
+<title>github.com/docker/distribution/registry/api/errcode</title>
+<g id="a_node47"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/errcode" xlink:title="github.com/docker/distribution/registry/api/errcode" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M6380.5,-130C6380.5,-130 6109.5,-130 6109.5,-130 6103.5,-130 6097.5,-124 6097.5,-118 6097.5,-118 6097.5,-106 6097.5,-106 6097.5,-100 6103.5,-94 6109.5,-94 6109.5,-94 6380.5,-94 6380.5,-94 6386.5,-94 6392.5,-100 6392.5,-106 6392.5,-106 6392.5,-118 6392.5,-118 6392.5,-124 6386.5,-130 6380.5,-130"/>
+<text text-anchor="middle" x="6245" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/errcode</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/docker/distribution/registry/api/errcode -->
+<g id="edge92" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;github.com/docker/distribution/registry/api/errcode</title>
+<path fill="none" stroke="#000000" d="M6001.1578,-948.0244C6169.1593,-935.3034 6436.013,-911.392 6532,-882 6634.6453,-850.5692 6746,-877.3497 6746,-770 6746,-770 6746,-770 6746,-488 6746,-323.7022 6665.2578,-281.269 6530,-188 6489.5284,-160.0922 6439.3314,-142.4278 6392.3947,-131.2487"/>
+<polygon fill="#000000" stroke="#000000" points="6392.5447,-129.4866 6387.2776,-130.0523 6391.7478,-132.8947 6392.5447,-129.4866"/>
+</g>
+<!-- golang.org/x/net/context/ctxhttp -->
+<g id="node48" class="node">
+<title>golang.org/x/net/context/ctxhttp</title>
+<g id="a_node48"><a xlink:href="https://godoc.org/golang.org/x/net/context/ctxhttp" xlink:title="golang.org/x/net/context/ctxhttp" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M7171.5,-224C7171.5,-224 7004.5,-224 7004.5,-224 6998.5,-224 6992.5,-218 6992.5,-212 6992.5,-212 6992.5,-200 6992.5,-200 6992.5,-194 6998.5,-188 7004.5,-188 7004.5,-188 7171.5,-188 7171.5,-188 7177.5,-188 7183.5,-194 7183.5,-200 7183.5,-200 7183.5,-212 7183.5,-212 7183.5,-218 7177.5,-224 7171.5,-224"/>
+<text text-anchor="middle" x="7088" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/net/context/ctxhttp</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;golang.org/x/net/context/ctxhttp -->
+<g id="edge97" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;golang.org/x/net/context/ctxhttp</title>
+<path fill="none" stroke="#000000" d="M6001.0717,-952.5904C6281.5208,-941.414 6881.179,-914.1155 6970,-882 7049.927,-853.1004 7125,-854.9912 7125,-770 7125,-770 7125,-770 7125,-394 7125,-333.6006 7106.3872,-264.3058 7095.5642,-229.1176"/>
+<polygon fill="#000000" stroke="#000000" points="7097.1839,-228.4326 7094.027,-224.1785 7093.842,-229.4727 7097.1839,-228.4326"/>
+</g>
+<!-- net/http -->
+<g id="node49" class="node">
+<title>net/http</title>
+<g id="a_node49"><a xlink:href="https://godoc.org/net/http" xlink:title="net/http" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M7221,-36C7221,-36 7187,-36 7187,-36 7181,-36 7175,-30 7175,-24 7175,-24 7175,-12 7175,-12 7175,-6 7181,0 7187,0 7187,0 7221,0 7221,0 7227,0 7233,-6 7233,-12 7233,-12 7233,-24 7233,-24 7233,-30 7227,-36 7221,-36"/>
+<text text-anchor="middle" x="7204" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http</text>
+</a>
+</g>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker&#45;&gt;net/http -->
+<g id="edge100" class="edge">
+<title>github.com/containerd/containerd/remotes/docker&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M6001.1221,-953.4119C6338.4793,-942.2282 7160.2018,-912.2544 7282,-882 7396.6293,-853.5264 7523,-888.1127 7523,-770 7523,-770 7523,-770 7523,-206 7523,-75.0043 7320.8767,-33.434 7238.4103,-21.9248"/>
+<polygon fill="#000000" stroke="#000000" points="7238.3451,-20.1499 7233.1548,-21.2095 7237.8731,-23.6179 7238.3451,-20.1499"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;bytes -->
+<g id="edge107" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2611.1225,-862.8764C3171.65,-858.6781 4939.0521,-841.3155 5190,-788 5297.2146,-765.2216 5585.0301,-691.0851 5646,-600 5667.9018,-567.2801 5680.8066,-338.3944 5487,-188 5349.9442,-81.6444 5257.9671,-203.8725 5101,-130 5056.2041,-108.918 5015.8072,-66.4833 4993.7031,-40.3268"/>
+<polygon fill="#000000" stroke="#000000" points="4994.8646,-38.9877 4990.3147,-36.2744 4992.1795,-41.2328 4994.8646,-38.9877"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;context -->
+<g id="edge108" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M2383.9493,-845.9228C2315.2563,-820.1691 2209,-765.8163 2209,-676 2209,-676 2209,-676 2209,-488 2209,-326.3074 2374.7576,-185.1874 2444.0822,-133.2376"/>
+<polygon fill="#000000" stroke="#000000" points="2445.143,-134.6296 2448.1082,-130.2398 2443.0527,-131.8224 2445.143,-134.6296"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;encoding/base64 -->
+<g id="edge109" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;encoding/base64</title>
+<path fill="none" stroke="#000000" d="M2611.0782,-861.934C3220.2177,-854.2491 5272.3077,-825.8015 5565,-788 5570.5598,-787.2819 5576.306,-786.3263 5582.0335,-785.2287"/>
+<polygon fill="#000000" stroke="#000000" points="5582.7203,-786.8763 5587.2822,-784.1834 5582.0366,-783.4437 5582.7203,-786.8763"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;encoding/json -->
+<g id="edge110" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M2611.2328,-862.9662C3197.131,-858.9428 5109.2292,-841.7903 5380,-788 5548.9825,-754.4306 5954.2319,-549.5287 6058,-412 6093.6553,-364.7444 6109.0584,-336.9347 6087,-282 6041.2977,-168.1819 5919.1822,-77.9277 5859.2035,-38.9278"/>
+<polygon fill="#000000" stroke="#000000" points="5860.1439,-37.4518 5854.9944,-36.2087 5858.2447,-40.3918 5860.1439,-37.4518"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;fmt -->
+<g id="edge111" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2270.7749,-860.9716C1867.0301,-851.5307 885,-814.3476 885,-676 885,-676 885,-676 885,-582 885,-339.9071 914.2372,-209.5031 1127,-94 1205.9227,-51.155 2621.8459,-22.949 2854.7116,-18.5906"/>
+<polygon fill="#000000" stroke="#000000" points="2855.0246,-20.3351 2859.9911,-18.4921 2854.9592,-16.8357 2855.0246,-20.3351"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/archive/compression -->
+<g id="edge112" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/archive/compression</title>
+<path fill="none" stroke="#000000" d="M2419.6311,-845.8456C2385.6267,-814.8006 2323,-747.8685 2323,-676 2323,-676 2323,-676 2323,-582 2323,-507.9565 2394.7531,-445.8785 2438.3376,-415.0007"/>
+<polygon fill="#000000" stroke="#000000" points="2439.3726,-416.4124 2442.4641,-412.1106 2437.3647,-413.5456 2439.3726,-416.4124"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/log -->
+<g id="edge116" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/log</title>
+<path fill="none" stroke="#000000" d="M2611.1081,-862.0235C3131.7079,-854.6093 4679.2031,-821.9243 4863,-694 4953.2437,-631.1895 4973.477,-579.8139 4968,-470 4965.3703,-417.2752 4958.1803,-355.6937 4954.0419,-323.0545"/>
+<polygon fill="#000000" stroke="#000000" points="4955.7747,-322.8083 4953.4051,-318.0704 4952.303,-323.2519 4955.7747,-322.8083"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;io -->
+<g id="edge123" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2270.9619,-860.435C1900.5818,-851.9604 1043.9266,-828.2536 918,-788 693.4359,-716.2161 354,-349.1122 354,-300 354,-300 354,-300 354,-206 354,-153.5763 350.4348,-127.2071 391,-94 414.4274,-74.8221 862.9207,-32.407 987.9084,-20.9183"/>
+<polygon fill="#000000" stroke="#000000" points="988.0983,-22.6583 992.9174,-20.4586 987.7783,-19.173 988.0983,-22.6583"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;strconv -->
+<g id="edge125" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M2270.8846,-861.1752C1884.5002,-854.0134 963.8085,-832.473 830,-788 714.5677,-749.6345 177,-419.6046 177,-300 177,-300 177,-300 177,-206 177,-153.5763 173.6322,-127.4468 214,-94 255.8056,-59.3619 636.9615,-28.8903 752.7766,-20.3662"/>
+<polygon fill="#000000" stroke="#000000" points="752.9052,-22.1115 757.7638,-20.0006 752.6492,-18.6209 752.9052,-22.1115"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;sync -->
+<g id="edge127" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2611.2905,-862.4104C3184.3968,-856.6504 5021.9665,-834.7064 5284,-788 5412.4037,-765.1126 5444.2714,-750.1367 5562,-694 5812.9731,-574.3281 5990.3274,-586.6482 6062,-318 6066.1244,-302.5407 6070.1494,-295.7691 6062,-282 6010.3506,-194.7343 5711.9208,-70.0207 5612.911,-30.5058"/>
+<polygon fill="#000000" stroke="#000000" points="5613.3619,-28.8018 5608.0692,-28.578 5612.0672,-32.0536 5613.3619,-28.8018"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/content -->
+<g id="edge113" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/content</title>
+<path fill="none" stroke="#000000" d="M2336.5141,-845.9734C2288.4418,-834.4603 2232.1897,-816.2221 2187,-788 2107.4065,-738.2919 2039.8955,-646.5514 2011.4414,-604.3313"/>
+<polygon fill="#000000" stroke="#000000" points="2012.8848,-603.3416 2008.649,-600.1602 2009.9764,-605.2887 2012.8848,-603.3416"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/errdefs -->
+<g id="edge114" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/errdefs</title>
+<path fill="none" stroke="#000000" d="M2479.9388,-845.9914C2615.6149,-783.2433 3067.0515,-574.4608 3210.1132,-508.297"/>
+<polygon fill="#000000" stroke="#000000" points="3210.8602,-509.8797 3214.6637,-506.1924 3209.391,-506.7029 3210.8602,-509.8797"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge118" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M2292.5369,-845.9996C2050.3608,-814.5999 1597,-746.4152 1597,-676 1597,-676 1597,-676 1597,-488 1597,-410.3135 1825.0079,-348.7457 1959.6218,-319.1663"/>
+<polygon fill="#000000" stroke="#000000" points="1960.2568,-320.8188 1964.7676,-318.0411 1959.5091,-317.3996 1960.2568,-320.8188"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge120" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M2270.9997,-857.4914C2035.426,-847.2012 1632.1008,-824.5093 1575,-788 1471.4008,-721.7604 1423.4754,-569.4792 1408.4532,-511.2363"/>
+<polygon fill="#000000" stroke="#000000" points="1410.1077,-510.6411 1407.1823,-506.2247 1406.7151,-511.5015 1410.1077,-510.6411"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/pkg/errors -->
+<g id="edge121" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M2611.0946,-862.9983C3185.5056,-859.1216 5023.9932,-842.4965 5138,-788 5308.3482,-706.5717 5421.1274,-489.0367 5454.6001,-417.3046"/>
+<polygon fill="#000000" stroke="#000000" points="5456.3601,-417.6683 5456.8728,-412.3957 5453.184,-416.1978 5456.3601,-417.6683"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;io/ioutil -->
+<g id="edge124" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2270.7799,-860.9065C1870.4568,-852.9802 892.1737,-829.5848 749,-788 469.2076,-706.7342 384.2999,-646.9482 212,-412 123.7291,-291.6338 97.0245,-106.5293 90.1254,-41.5353"/>
+<polygon fill="#000000" stroke="#000000" points="91.8436,-41.1367 89.5896,-36.3428 88.3621,-41.496 91.8436,-41.1367"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;time -->
+<g id="edge128" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2270.7464,-860.2118C1917.5124,-851.5894 1128.0987,-828.0822 1012,-788 756.4859,-699.7856 649,-570.3132 649,-300 649,-300 649,-300 649,-206 649,-109.8838 745.1087,-130.5604 834,-94 885.8763,-72.6636 1047.7765,-38.6276 1117.5119,-24.4876"/>
+<polygon fill="#000000" stroke="#000000" points="1118.2173,-26.1304 1122.771,-23.4235 1117.5232,-22.6999 1118.2173,-26.1304"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;strings -->
+<g id="edge126" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2611.0343,-863.2771C3207.3293,-860.2519 5182.5155,-845.9198 5461,-788 5569.4253,-765.4495 6271.2459,-448.6364 6497,-318 6600.0952,-258.3423 6655.9365,-244.7152 6688,-130 6692.307,-114.5906 6694.0243,-108.8226 6688,-94 6679.1149,-72.1384 6660.584,-53.1206 6644.2898,-39.5772"/>
+<polygon fill="#000000" stroke="#000000" points="6645.186,-38.0504 6640.2027,-36.2535 6642.9778,-40.7658 6645.186,-38.0504"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/images -->
+<g id="edge115" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/images</title>
+<path fill="none" stroke="#000000" d="M2553.3763,-845.9738C2775.6105,-810.3253 3269.2876,-731.1348 3495.5696,-694.837"/>
+<polygon fill="#000000" stroke="#000000" points="3495.8748,-696.5605 3500.5344,-694.0406 3495.3203,-693.1046 3495.8748,-696.5605"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;golang.org/x/sync/errgroup -->
+<g id="edge122" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;golang.org/x/sync/errgroup</title>
+<path fill="none" stroke="#000000" d="M2611.4347,-857.4778C3143.589,-836.3819 4743.8801,-767.113 4827,-694 4879.9585,-647.4173 4878.6918,-554.2691 4874.8239,-511.032"/>
+<polygon fill="#000000" stroke="#000000" points="4876.562,-510.8233 4874.3428,-506.0131 4873.078,-511.1573 4876.562,-510.8233"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/remotes -->
+<g id="edge117" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/containerd/containerd/remotes</title>
+<path fill="none" stroke="#000000" d="M2611.1056,-850.3567C2847.7452,-831.3771 3271.1007,-797.422 3483.6981,-780.3706"/>
+<polygon fill="#000000" stroke="#000000" points="3484.148,-782.0903 3488.9921,-779.946 3483.8681,-778.6015 3484.148,-782.0903"/>
+</g>
+<!-- github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go -->
+<g id="edge119" class="edge">
+<title>github.com/containerd/containerd/remotes/docker/schema1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go</title>
+<path fill="none" stroke="#000000" d="M2270.7328,-848.7319C2259.9952,-847.7987 2249.349,-846.8814 2239,-846 2007.8513,-826.3132 1209,-907.9855 1209,-676 1209,-676 1209,-676 1209,-582 1209,-521.539 1228.1158,-452.2699 1239.2314,-417.102"/>
+<polygon fill="#000000" stroke="#000000" points="1240.9537,-417.4613 1240.8101,-412.1658 1237.62,-416.3951 1240.9537,-417.4613"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;encoding/json -->
+<g id="edge129" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M6164.5167,-93.9871C6079.4982,-74.9591 5948.014,-45.5317 5876.1688,-29.4521"/>
+<polygon fill="#000000" stroke="#000000" points="5876.362,-27.7021 5871.1004,-28.3177 5875.5975,-31.1176 5876.362,-27.7021"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;fmt -->
+<g id="edge130" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M6097.2268,-108.3051C5679.3357,-97.791 4453.297,-66.4942 3435,-36 3239.0548,-30.1322 3003.7351,-22.0566 2919.1929,-19.1226"/>
+<polygon fill="#000000" stroke="#000000" points="2919.1405,-17.3699 2914.0828,-18.9452 2919.019,-20.8677 2919.1405,-17.3699"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;sync -->
+<g id="edge134" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M6117.7598,-93.9871C5960.659,-71.7469 5703.2024,-35.2997 5613.2394,-22.564"/>
+<polygon fill="#000000" stroke="#000000" points="5613.2606,-20.7996 5608.0647,-21.8314 5612.77,-24.2651 5613.2606,-20.7996"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;strings -->
+<g id="edge133" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M6315.7103,-93.9871C6395.7464,-73.5985 6522.653,-41.27 6581.7476,-26.216"/>
+<polygon fill="#000000" stroke="#000000" points="6582.4966,-27.8312 6586.9098,-24.901 6581.6325,-24.4395 6582.4966,-27.8312"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;sort -->
+<g id="edge132" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M6215.1144,-93.8759C6188.2262,-77.5694 6148.8535,-53.6918 6121.4843,-37.0937"/>
+<polygon fill="#000000" stroke="#000000" points="6122.3812,-35.591 6117.1985,-34.4946 6120.5663,-38.5837 6122.3812,-35.591"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;net/http -->
+<g id="edge131" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M6392.5053,-98.5746C6552.3458,-83.8729 6814.8726,-59.2793 7041,-36 7085.491,-31.4198 7136.7591,-25.6826 7169.7326,-21.9332"/>
+<polygon fill="#000000" stroke="#000000" points="7170.0938,-23.6535 7174.8637,-21.349 7169.6978,-20.1759 7170.0938,-23.6535"/>
+</g>
+<!-- golang.org/x/net/context/ctxhttp&#45;&gt;context -->
+<g id="edge202" class="edge">
+<title>golang.org/x/net/context/ctxhttp&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M6992.4249,-204.0529C6368.0903,-191.3334 2884.7092,-120.3673 2507.1831,-112.676"/>
+<polygon fill="#000000" stroke="#000000" points="2507.1426,-110.9249 2502.108,-112.5726 2507.0712,-114.4242 2507.1426,-110.9249"/>
+</g>
+<!-- golang.org/x/net/context/ctxhttp&#45;&gt;io -->
+<g id="edge203" class="edge">
+<title>golang.org/x/net/context/ctxhttp&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M6992.231,-203.9498C6611.3193,-195.7338 5166.7286,-163.9911 3978,-130 3818.5352,-125.4402 1266.4346,-54.6623 1108,-36 1089.3333,-33.8012 1068.7366,-29.6827 1052.1652,-25.9397"/>
+<polygon fill="#000000" stroke="#000000" points="1052.4228,-24.2034 1047.1582,-24.7926 1051.6411,-27.615 1052.4228,-24.2034"/>
+</g>
+<!-- golang.org/x/net/context/ctxhttp&#45;&gt;strings -->
+<g id="edge206" class="edge">
+<title>golang.org/x/net/context/ctxhttp&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M7099.2702,-187.7532C7112.6941,-163.4277 7130.6356,-120.3844 7108,-94 7077.6837,-58.663 6750.973,-29.1584 6646.1626,-20.5446"/>
+<polygon fill="#000000" stroke="#000000" points="6646.1726,-18.7897 6641.0466,-20.1263 6645.8873,-22.2781 6646.1726,-18.7897"/>
+</g>
+<!-- golang.org/x/net/context/ctxhttp&#45;&gt;net/url -->
+<g id="edge205" class="edge">
+<title>golang.org/x/net/context/ctxhttp&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M7124.0555,-187.8759C7158.5503,-170.5362 7210.0763,-144.6354 7243.1106,-128.03"/>
+<polygon fill="#000000" stroke="#000000" points="7244.1647,-129.4588 7247.8461,-125.6496 7242.5927,-126.3317 7244.1647,-129.4588"/>
+</g>
+<!-- golang.org/x/net/context/ctxhttp&#45;&gt;net/http -->
+<g id="edge204" class="edge">
+<title>golang.org/x/net/context/ctxhttp&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M7112.4793,-187.8148C7129.8694,-173.7517 7152.601,-152.8695 7167,-130 7184.5484,-102.1283 7194.6513,-65.0699 7199.7374,-41.3696"/>
+<polygon fill="#000000" stroke="#000000" points="7201.5013,-41.4829 7200.8064,-36.2312 7198.0746,-40.7699 7201.5013,-41.4829"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go&#45;&gt;fmt -->
+<g id="edge177" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1177.1337,-375.8811C1147.6604,-364.4067 1116.2357,-346.2262 1099,-318 1053.5978,-243.6469 1034.2573,-194.405 1179,-94 1250.4981,-44.4032 2624.4964,-21.8944 2854.4781,-18.4675"/>
+<polygon fill="#000000" stroke="#000000" points="2854.7233,-20.2142 2859.6968,-18.3901 2854.6714,-16.7146 2854.7233,-20.2142"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;bufio -->
+<g id="edge135" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M4176.7828,-93.9871C4273.0097,-73.0488 4427.1078,-39.5182 4493.6607,-25.0368"/>
+<polygon fill="#000000" stroke="#000000" points="4494.1679,-26.7175 4498.6815,-23.9443 4493.4237,-23.2975 4494.1679,-26.7175"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;bytes -->
+<g id="edge136" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M4195.1547,-101.969C4336.2798,-87.8636 4600.2349,-61.0793 4825,-36 4865.9252,-31.4336 4913.0355,-25.7496 4943.6014,-22.005"/>
+<polygon fill="#000000" stroke="#000000" points="4943.986,-23.721 4948.7357,-21.3752 4943.5598,-20.2471 4943.986,-23.721"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;encoding -->
+<g id="edge137" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;encoding</title>
+<path fill="none" stroke="#000000" d="M4077.6111,-93.8759C4063.7355,-78.531 4043.7971,-56.4815 4028.9866,-40.1029"/>
+<polygon fill="#000000" stroke="#000000" points="4029.9901,-38.6034 4025.3385,-36.0685 4027.3941,-40.9509 4029.9901,-38.6034"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;encoding/json -->
+<g id="edge138" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M4195.2794,-106.7722C4265.6178,-103.1535 4361.51,-98.2438 4446,-94 4968.6431,-67.7487 5099.9382,-72.0057 5622,-36 5673.6208,-32.4398 5732.5117,-27.073 5773.5199,-23.1255"/>
+<polygon fill="#000000" stroke="#000000" points="5773.873,-24.8497 5778.6816,-22.6271 5773.5365,-21.3659 5773.873,-24.8497"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;errors -->
+<g id="edge139" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M4136.7328,-93.9871C4180.1184,-75.699 4246.2937,-47.8045 4285.2417,-31.3869"/>
+<polygon fill="#000000" stroke="#000000" points="4285.9255,-32.9979 4289.8531,-29.4431 4284.5659,-29.7727 4285.9255,-32.9979"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;fmt -->
+<g id="edge140" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3992.9094,-104.1272C3737.0964,-84.2047 3073.4421,-32.5199 2919.3089,-20.5162"/>
+<polygon fill="#000000" stroke="#000000" points="2919.1631,-18.7496 2914.0423,-20.106 2918.8913,-22.2391 2919.1631,-18.7496"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;io -->
+<g id="edge141" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3992.9379,-103.2442C3949.8469,-99.8242 3898.9949,-96.202 3853,-94 3243.5619,-64.8233 1714.3512,-103.8555 1108,-36 1089.3208,-33.9097 1068.7234,-29.7975 1052.1547,-26.0308"/>
+<polygon fill="#000000" stroke="#000000" points="1052.4142,-24.2947 1047.1487,-24.8755 1051.6271,-27.7051 1052.4142,-24.2947"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;os -->
+<g id="edge144" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M4032.2962,-93.9871C3964.187,-74.1043 3857.1816,-42.8667 3804.0901,-27.3679"/>
+<polygon fill="#000000" stroke="#000000" points="3804.4137,-25.6394 3799.1236,-25.9181 3803.4328,-28.9992 3804.4137,-25.6394"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;strconv -->
+<g id="edge147" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3992.7883,-110.4741C3526.8132,-103.3042 1584.8739,-71.8052 978,-36 922.1856,-32.707 857.4845,-26.0503 819.2436,-21.8198"/>
+<polygon fill="#000000" stroke="#000000" points="819.4188,-20.0786 814.256,-21.2652 819.032,-23.5572 819.4188,-20.0786"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;sync -->
+<g id="edge149" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M4195.0385,-105.6129C4494.8984,-86.6574 5369.986,-31.3391 5548.8464,-20.0326"/>
+<polygon fill="#000000" stroke="#000000" points="5549.0542,-21.773 5553.9338,-19.711 5548.8333,-18.28 5549.0542,-21.773"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;strings -->
+<g id="edge148" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M4195.2609,-106.3734C4265.5909,-102.5754 4361.4808,-97.6154 4446,-94 5194.6478,-61.9759 5382.1485,-62.8419 6131,-36 6300.5187,-29.9237 6503.4865,-22.22 6581.3769,-19.2478"/>
+<polygon fill="#000000" stroke="#000000" points="6581.8371,-20.9816 6586.7667,-19.0421 6581.7036,-17.4841 6581.8371,-20.9816"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;sort -->
+<g id="edge146" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M4195.2679,-106.531C4265.601,-102.8039 4361.4918,-97.8637 4446,-94 5085.4069,-64.7665 5246.2328,-76.8935 5885,-36 5946.0787,-32.0898 6017.2106,-25.3525 6057.5904,-21.3227"/>
+<polygon fill="#000000" stroke="#000000" points="6058.039,-23.0366 6062.8396,-20.7968 6057.69,-19.5541 6058.039,-23.0366"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;sync/atomic -->
+<g id="edge150" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;sync/atomic</title>
+<path fill="none" stroke="#000000" d="M4195.0543,-97.9273C4343.3062,-77.2818 4613.8806,-39.6018 4722.2721,-24.5073"/>
+<polygon fill="#000000" stroke="#000000" points="4722.5991,-26.2287 4727.3099,-23.8057 4722.1163,-22.7622 4722.5991,-26.2287"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;log -->
+<g id="edge142" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;log</title>
+<path fill="none" stroke="#000000" d="M4060.451,-93.8759C4029.0131,-76.8921 3982.372,-51.6952 3951.5869,-35.0642"/>
+<polygon fill="#000000" stroke="#000000" points="3952.3945,-33.5115 3947.1636,-32.6746 3950.7309,-36.5909 3952.3945,-33.5115"/>
+</g>
+<!-- math -->
+<g id="node53" class="node">
+<title>math</title>
+<g id="a_node53"><a xlink:href="https://godoc.org/math" xlink:title="math" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3492,-36C3492,-36 3462,-36 3462,-36 3456,-36 3450,-30 3450,-24 3450,-24 3450,-12 3450,-12 3450,-6 3456,0 3462,0 3462,0 3492,0 3492,0 3498,0 3504,-6 3504,-12 3504,-12 3504,-24 3504,-24 3504,-30 3498,-36 3492,-36"/>
+<text text-anchor="middle" x="3477" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">math</text>
+</a>
+</g>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;math -->
+<g id="edge143" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3992.8059,-96.5831C3850.3459,-74.8793 3598.1144,-36.4518 3509.1521,-22.8984"/>
+<polygon fill="#000000" stroke="#000000" points="3509.2388,-21.1415 3504.0322,-22.1184 3508.7116,-24.6015 3509.2388,-21.1415"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;reflect -->
+<g id="edge145" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M4116.9444,-93.8759C4136.6996,-78.2709 4165.2327,-55.7321 4186.0668,-39.275"/>
+<polygon fill="#000000" stroke="#000000" points="4187.2872,-40.5411 4190.126,-36.0685 4185.1177,-37.7946 4187.2872,-40.5411"/>
+</g>
+<!-- unicode/utf8 -->
+<g id="node55" class="node">
+<title>unicode/utf8</title>
+<g id="a_node55"><a xlink:href="https://godoc.org/unicode/utf8" xlink:title="unicode/utf8" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4144.5,-36C4144.5,-36 4083.5,-36 4083.5,-36 4077.5,-36 4071.5,-30 4071.5,-24 4071.5,-24 4071.5,-12 4071.5,-12 4071.5,-6 4077.5,0 4083.5,0 4083.5,0 4144.5,0 4144.5,0 4150.5,0 4156.5,-6 4156.5,-12 4156.5,-12 4156.5,-24 4156.5,-24 4156.5,-30 4150.5,-36 4144.5,-36"/>
+<text text-anchor="middle" x="4114" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">unicode/utf8</text>
+</a>
+</g>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;unicode/utf8 -->
+<g id="edge151" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M4097.8562,-93.8759C4101.0381,-78.9211 4105.5748,-57.5983 4109.0292,-41.3629"/>
+<polygon fill="#000000" stroke="#000000" points="4110.8267,-41.3233 4110.1556,-36.0685 4107.4033,-40.5948 4110.8267,-41.3233"/>
+</g>
+<!-- unsafe -->
+<g id="node56" class="node">
+<title>unsafe</title>
+<g id="a_node56"><a xlink:href="https://godoc.org/unsafe" xlink:title="unsafe" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5135,-36C5135,-36 5105,-36 5105,-36 5099,-36 5093,-30 5093,-24 5093,-24 5093,-12 5093,-12 5093,-6 5099,0 5105,0 5105,0 5135,0 5135,0 5141,0 5147,-6 5147,-12 5147,-12 5147,-24 5147,-24 5147,-30 5141,-36 5135,-36"/>
+<text text-anchor="middle" x="5120" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">unsafe</text>
+</a>
+</g>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;unsafe -->
+<g id="edge152" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;unsafe</title>
+<path fill="none" stroke="#000000" d="M4195.0417,-106.6925C4365.1633,-97.0829 4719.3732,-74.1794 5017,-36 5040.9022,-32.9338 5067.7118,-28.2074 5087.9862,-24.3671"/>
+<polygon fill="#000000" stroke="#000000" points="5088.4109,-26.0678 5092.9942,-23.4114 5087.7547,-22.6298 5088.4109,-26.0678"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes&#45;&gt;errors -->
+<g id="edge153" class="edge">
+<title>github.com/golang/protobuf/ptypes&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3695.5975,-284.3578C3701.1444,-283.554 3706.6384,-282.7638 3712,-282 3901.1704,-255.0527 3969.8887,-314.8306 4138,-224 4218.8394,-180.3224 4279.9242,-84.6004 4304.7921,-40.7527"/>
+<polygon fill="#000000" stroke="#000000" points="4306.4426,-41.3877 4307.3654,-36.1713 4303.391,-39.6737 4306.4426,-41.3877"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes&#45;&gt;fmt -->
+<g id="edge154" class="edge">
+<title>github.com/golang/protobuf/ptypes&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3486.3127,-291.2957C3363.0533,-279.7477 3166.7759,-256.9115 3101,-224 3027.0788,-187.0129 3027.6982,-151.171 2968,-94 2948.7416,-75.5569 2926.6548,-54.8634 2910.4142,-39.732"/>
+<polygon fill="#000000" stroke="#000000" points="2911.2684,-38.1361 2906.4165,-36.0096 2908.8833,-40.6977 2911.2684,-38.1361"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes&#45;&gt;time -->
+<g id="edge161" class="edge">
+<title>github.com/golang/protobuf/ptypes&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3486.4343,-288.7772C3375.768,-276.1355 3196.2766,-253.451 3043,-224 2806.0903,-178.4796 2754.5805,-129.7445 2516,-94 2246.4702,-53.6187 1362.4646,-24.5627 1182.2478,-18.9773"/>
+<polygon fill="#000000" stroke="#000000" points="1182.1742,-17.2243 1177.1225,-18.8189 1182.0661,-20.7226 1182.1742,-17.2243"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes&#45;&gt;strings -->
+<g id="edge160" class="edge">
+<title>github.com/golang/protobuf/ptypes&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3695.5238,-283.7344C3701.0898,-283.0924 3706.6081,-282.5075 3712,-282 4353.6799,-221.6062 4519.302,-284.2016 5161,-224 5269.7014,-213.8021 5295.3776,-199.0083 5404,-188 5637.6019,-164.3257 6237.6678,-208.3733 6459,-130 6513.2111,-110.804 6565.026,-66.306 6592.8665,-39.5363"/>
+<polygon fill="#000000" stroke="#000000" points="6594.0988,-40.7791 6596.4696,-36.0417 6591.662,-38.2667 6594.0988,-40.7791"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge155" class="edge">
+<title>github.com/golang/protobuf/ptypes&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3695.6064,-284.419C3701.1509,-283.5993 3706.6421,-282.7889 3712,-282 3800.4252,-268.9799 4051.534,-291.6038 4110,-224 4131.2,-199.4866 4118.4252,-160.0454 4106.6642,-135.1026"/>
+<polygon fill="#000000" stroke="#000000" points="4108.0968,-134.0469 4104.3359,-130.3161 4104.9494,-135.5779 4108.0968,-134.0469"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes&#45;&gt;reflect -->
+<g id="edge159" class="edge">
+<title>github.com/golang/protobuf/ptypes&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M3695.6636,-283.8957C3813.285,-265.2958 3989.2365,-235.7502 3999,-224 4036.404,-178.9853 3943.0747,-140.9639 3978,-94 4031.4484,-22.128 4086.8142,-66.5788 4171,-36 4174.1317,-34.8625 4177.3663,-33.6137 4180.5858,-32.3208"/>
+<polygon fill="#000000" stroke="#000000" points="4181.6508,-33.7759 4185.6155,-30.2624 4180.3251,-30.5366 4181.6508,-33.7759"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/any -->
+<g id="node58" class="node">
+<title>github.com/golang/protobuf/ptypes/any</title>
+<g id="a_node58"><a xlink:href="https://godoc.org/github.com/golang/protobuf/ptypes/any" xlink:title="github.com/golang/protobuf/ptypes/any" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3973,-224C3973,-224 3765,-224 3765,-224 3759,-224 3753,-218 3753,-212 3753,-212 3753,-200 3753,-200 3753,-194 3759,-188 3765,-188 3765,-188 3973,-188 3973,-188 3979,-188 3985,-194 3985,-200 3985,-200 3985,-212 3985,-212 3985,-218 3979,-224 3973,-224"/>
+<text text-anchor="middle" x="3869" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/ptypes/any</text>
+</a>
+</g>
+</g>
+<!-- github.com/golang/protobuf/ptypes&#45;&gt;github.com/golang/protobuf/ptypes/any -->
+<g id="edge156" class="edge">
+<title>github.com/golang/protobuf/ptypes&#45;&gt;github.com/golang/protobuf/ptypes/any</title>
+<path fill="none" stroke="#000000" d="M3644.2723,-281.9871C3691.829,-265.9068 3761.3507,-242.3994 3810.4501,-225.7975"/>
+<polygon fill="#000000" stroke="#000000" points="3811.2588,-227.3714 3815.4348,-224.112 3810.1377,-224.0558 3811.2588,-227.3714"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/duration -->
+<g id="node59" class="node">
+<title>github.com/golang/protobuf/ptypes/duration</title>
+<g id="a_node59"><a xlink:href="https://godoc.org/github.com/golang/protobuf/ptypes/duration" xlink:title="github.com/golang/protobuf/ptypes/duration" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3412.5,-224C3412.5,-224 3179.5,-224 3179.5,-224 3173.5,-224 3167.5,-218 3167.5,-212 3167.5,-212 3167.5,-200 3167.5,-200 3167.5,-194 3173.5,-188 3179.5,-188 3179.5,-188 3412.5,-188 3412.5,-188 3418.5,-188 3424.5,-194 3424.5,-200 3424.5,-200 3424.5,-212 3424.5,-212 3424.5,-218 3418.5,-224 3412.5,-224"/>
+<text text-anchor="middle" x="3296" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/ptypes/duration</text>
+</a>
+</g>
+</g>
+<!-- github.com/golang/protobuf/ptypes&#45;&gt;github.com/golang/protobuf/ptypes/duration -->
+<g id="edge157" class="edge">
+<title>github.com/golang/protobuf/ptypes&#45;&gt;github.com/golang/protobuf/ptypes/duration</title>
+<path fill="none" stroke="#000000" d="M3534.4701,-281.9871C3483.8223,-265.8484 3409.6969,-242.2288 3357.5644,-225.6171"/>
+<polygon fill="#000000" stroke="#000000" points="3357.8848,-223.8826 3352.5895,-224.0319 3356.8222,-227.2174 3357.8848,-223.8826"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/timestamp -->
+<g id="node60" class="node">
+<title>github.com/golang/protobuf/ptypes/timestamp</title>
+<g id="a_node60"><a xlink:href="https://godoc.org/github.com/golang/protobuf/ptypes/timestamp" xlink:title="github.com/golang/protobuf/ptypes/timestamp" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3712,-224C3712,-224 3466,-224 3466,-224 3460,-224 3454,-218 3454,-212 3454,-212 3454,-200 3454,-200 3454,-194 3460,-188 3466,-188 3466,-188 3712,-188 3712,-188 3718,-188 3724,-194 3724,-200 3724,-200 3724,-212 3724,-212 3724,-218 3718,-224 3712,-224"/>
+<text text-anchor="middle" x="3589" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/ptypes/timestamp</text>
+</a>
+</g>
+</g>
+<!-- github.com/golang/protobuf/ptypes&#45;&gt;github.com/golang/protobuf/ptypes/timestamp -->
+<g id="edge158" class="edge">
+<title>github.com/golang/protobuf/ptypes&#45;&gt;github.com/golang/protobuf/ptypes/timestamp</title>
+<path fill="none" stroke="#000000" d="M3590.6144,-281.8759C3590.2962,-266.9211 3589.8425,-245.5983 3589.4971,-229.3629"/>
+<polygon fill="#000000" stroke="#000000" points="3591.2405,-229.0301 3589.3844,-224.0685 3587.7413,-229.1046 3591.2405,-229.0301"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/any&#45;&gt;fmt -->
+<g id="edge162" class="edge">
+<title>github.com/golang/protobuf/ptypes/any&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3774.8417,-187.9738C3562.0347,-147.2327 3052.1674,-49.6207 2919.1708,-24.159"/>
+<polygon fill="#000000" stroke="#000000" points="2919.4687,-22.4343 2914.2288,-23.2128 2918.8105,-25.8719 2919.4687,-22.4343"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/any&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge163" class="edge">
+<title>github.com/golang/protobuf/ptypes/any&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3912.116,-187.9871C3950.3729,-172.0042 4006.1924,-148.6841 4045.8881,-132.1001"/>
+<polygon fill="#000000" stroke="#000000" points="4046.7079,-133.6542 4050.6469,-130.112 4045.3587,-130.4247 4046.7079,-133.6542"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/any&#45;&gt;math -->
+<g id="edge164" class="edge">
+<title>github.com/golang/protobuf/ptypes/any&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3758.1207,-187.9454C3660.9389,-171.205 3532.3543,-146.4691 3514,-130 3488.6713,-107.2729 3480.6352,-66.8571 3478.1141,-41.2943"/>
+<polygon fill="#000000" stroke="#000000" points="3479.8367,-40.9042 3477.6556,-36.0766 3476.3501,-41.2107 3479.8367,-40.9042"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/duration&#45;&gt;fmt -->
+<g id="edge165" class="edge">
+<title>github.com/golang/protobuf/ptypes/duration&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3276.5078,-187.9471C3249.0971,-163.4444 3196.7066,-119.768 3145,-94 3068.4328,-55.8428 2969.3189,-33.376 2919.1446,-23.6845"/>
+<polygon fill="#000000" stroke="#000000" points="2919.3908,-21.95 2914.1512,-22.7311 2918.7343,-25.3878 2919.3908,-21.95"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/duration&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge166" class="edge">
+<title>github.com/golang/protobuf/ptypes/duration&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3424.6181,-189.7505C3429.4732,-189.1561 3434.2802,-188.5709 3439,-188 3632.3658,-164.6125 3858.0264,-138.7421 3987.6421,-124.0198"/>
+<polygon fill="#000000" stroke="#000000" points="3988.2058,-125.7171 3992.9764,-123.414 3987.8108,-122.2395 3988.2058,-125.7171"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/duration&#45;&gt;math -->
+<g id="edge167" class="edge">
+<title>github.com/golang/protobuf/ptypes/duration&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3313.355,-187.9738C3346.6499,-153.3913 3419.3959,-77.8319 3455.6209,-40.2059"/>
+<polygon fill="#000000" stroke="#000000" points="3457.2244,-41.0636 3459.4316,-36.2479 3454.703,-38.6361 3457.2244,-41.0636"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/timestamp&#45;&gt;fmt -->
+<g id="edge168" class="edge">
+<title>github.com/golang/protobuf/ptypes/timestamp&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3528.2612,-187.9465C3451.2054,-165.2147 3314.1158,-125.3452 3196,-94 3095.4458,-67.3153 2975.622,-38.7643 2919.2907,-25.5325"/>
+<polygon fill="#000000" stroke="#000000" points="2919.6336,-23.8155 2914.366,-24.3768 2918.8339,-27.2229 2919.6336,-23.8155"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/timestamp&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge169" class="edge">
+<title>github.com/golang/protobuf/ptypes/timestamp&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3685.7716,-187.9871C3773.61,-171.637 3902.6991,-147.6085 3992.0991,-130.9677"/>
+<polygon fill="#000000" stroke="#000000" points="3992.5311,-132.6674 3997.1264,-130.0319 3991.8906,-129.2265 3992.5311,-132.6674"/>
+</g>
+<!-- github.com/golang/protobuf/ptypes/timestamp&#45;&gt;math -->
+<g id="edge170" class="edge">
+<title>github.com/golang/protobuf/ptypes/timestamp&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3529.4038,-187.9228C3501.6066,-176.1651 3471.1536,-157.7326 3455,-130 3438.8238,-102.2286 3451.9746,-64.6916 3463.7853,-40.9097"/>
+<polygon fill="#000000" stroke="#000000" points="3465.4018,-41.5922 3466.1205,-36.3437 3462.2857,-39.9985 3465.4018,-41.5922"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;bytes -->
+<g id="edge212" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M5184.5603,-93.9871C5132.5726,-75.0458 5052.3014,-45.7998 5008.038,-29.6728"/>
+<polygon fill="#000000" stroke="#000000" points="5008.3815,-27.9354 5003.0845,-27.868 5007.1833,-31.224 5008.3815,-27.9354"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;sync -->
+<g id="edge216" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M5300.4945,-93.9871C5374.9947,-73.8055 5492.6806,-41.9251 5548.9584,-26.6799"/>
+<polygon fill="#000000" stroke="#000000" points="5549.5148,-28.3423 5553.8833,-25.3457 5548.5996,-24.964 5549.5148,-28.3423"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;time -->
+<g id="edge218" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M5166.7587,-110.0986C5045.778,-106.6999 4783.3878,-99.4306 4562,-94 3161.8919,-59.6555 1438.439,-23.9405 1182.4022,-18.666"/>
+<polygon fill="#000000" stroke="#000000" points="1182.3594,-16.9149 1177.3244,-18.5615 1182.2873,-20.4142 1182.3594,-16.9149"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;strings -->
+<g id="edge215" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M5301.0266,-107.4344C5548.3659,-90.5867 6404.6596,-32.2594 6581.7837,-20.1944"/>
+<polygon fill="#000000" stroke="#000000" points="6581.9552,-21.9369 6586.8246,-19.8511 6581.7172,-18.445 6581.9552,-21.9369"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;sort -->
+<g id="edge214" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M5301.1689,-104.624C5474.5471,-85.5848 5932.3109,-35.3163 6057.7936,-21.5367"/>
+<polygon fill="#000000" stroke="#000000" points="6058.0395,-23.2703 6062.8186,-20.9849 6057.6574,-19.7912 6058.0395,-23.2703"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;runtime -->
+<g id="edge213" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M5301.2263,-108.4156C5593.2753,-92.8441 6746.8562,-31.3372 6962.26,-19.8523"/>
+<polygon fill="#000000" stroke="#000000" points="6962.571,-21.5883 6967.4707,-19.5744 6962.3846,-18.0932 6962.571,-21.5883"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;unsafe -->
+<g id="edge219" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;unsafe</title>
+<path fill="none" stroke="#000000" d="M5212.0197,-93.8759C5193.0945,-78.2709 5165.7603,-55.7321 5145.8016,-39.275"/>
+<polygon fill="#000000" stroke="#000000" points="5146.8839,-37.8992 5141.9129,-36.0685 5144.6573,-40.5997 5146.8839,-37.8992"/>
+</g>
+<!-- syscall -->
+<g id="node63" class="node">
+<title>syscall</title>
+<g id="a_node63"><a xlink:href="https://godoc.org/syscall" xlink:title="syscall" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5249,-36C5249,-36 5219,-36 5219,-36 5213,-36 5207,-30 5207,-24 5207,-24 5207,-12 5207,-12 5207,-6 5213,0 5219,0 5219,0 5249,0 5249,0 5255,0 5261,-6 5261,-12 5261,-12 5261,-24 5261,-24 5261,-30 5255,-36 5249,-36"/>
+<text text-anchor="middle" x="5234" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">syscall</text>
+</a>
+</g>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;syscall -->
+<g id="edge217" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;syscall</title>
+<path fill="none" stroke="#000000" d="M5234,-93.8759C5234,-78.9211 5234,-57.5983 5234,-41.3629"/>
+<polygon fill="#000000" stroke="#000000" points="5235.7501,-41.0685 5234,-36.0685 5232.2501,-41.0685 5235.7501,-41.0685"/>
+</g>
+<!-- google.golang.org/genproto/googleapis/rpc/status&#45;&gt;fmt -->
+<g id="edge220" class="edge">
+<title>google.golang.org/genproto/googleapis/rpc/status&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3726.416,-283.7958C3720.8731,-283.1857 3715.3855,-282.5852 3710,-282 3462.5626,-255.1119 3380.8743,-324.1076 3153,-224 3113.0746,-206.4603 3040.815,-120.2601 3006,-94 2977.6271,-72.5991 2943.2254,-51.0633 2918.7229,-36.409"/>
+<polygon fill="#000000" stroke="#000000" points="2919.4578,-34.8099 2914.2666,-33.7544 2917.6666,-37.8168 2919.4578,-34.8099"/>
+</g>
+<!-- google.golang.org/genproto/googleapis/rpc/status&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge221" class="edge">
+<title>google.golang.org/genproto/googleapis/rpc/status&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M4005.2046,-281.9926C4074.3404,-269.7648 4148.087,-250.8716 4169,-224 4192.5621,-193.7245 4155.1902,-156.2949 4125.2637,-133.2895"/>
+<polygon fill="#000000" stroke="#000000" points="4125.9553,-131.6189 4120.9105,-130.0025 4123.8462,-134.412 4125.9553,-131.6189"/>
+</g>
+<!-- google.golang.org/genproto/googleapis/rpc/status&#45;&gt;math -->
+<g id="edge223" class="edge">
+<title>google.golang.org/genproto/googleapis/rpc/status&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3918.4955,-281.9479C3967.3022,-261.4267 4030.3767,-225.8368 3999,-188 3910.0833,-80.7765 3822.5905,-175.6838 3691,-130 3622.5836,-106.2481 3549.0808,-63.5689 3508.4932,-38.3109"/>
+<polygon fill="#000000" stroke="#000000" points="3509.357,-36.787 3504.1895,-35.621 3507.5019,-39.755 3509.357,-36.787"/>
+</g>
+<!-- google.golang.org/genproto/googleapis/rpc/status&#45;&gt;github.com/golang/protobuf/ptypes/any -->
+<g id="edge222" class="edge">
+<title>google.golang.org/genproto/googleapis/rpc/status&#45;&gt;github.com/golang/protobuf/ptypes/any</title>
+<path fill="none" stroke="#000000" d="M3869,-281.8759C3869,-266.9211 3869,-245.5983 3869,-229.3629"/>
+<polygon fill="#000000" stroke="#000000" points="3870.7501,-229.0685 3869,-224.0685 3867.2501,-229.0685 3870.7501,-229.0685"/>
+</g>
+<!-- google.golang.org/grpc/connectivity -->
+<g id="node65" class="node">
+<title>google.golang.org/grpc/connectivity</title>
+<g id="a_node65"><a xlink:href="https://godoc.org/google.golang.org/grpc/connectivity" xlink:title="google.golang.org/grpc/connectivity" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1839,-224C1839,-224 1649,-224 1649,-224 1643,-224 1637,-218 1637,-212 1637,-212 1637,-200 1637,-200 1637,-194 1643,-188 1649,-188 1649,-188 1839,-188 1839,-188 1845,-188 1851,-194 1851,-200 1851,-200 1851,-212 1851,-212 1851,-218 1845,-224 1839,-224"/>
+<text text-anchor="middle" x="1744" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/connectivity</text>
+</a>
+</g>
+</g>
+<!-- google.golang.org/grpc/connectivity&#45;&gt;context -->
+<g id="edge226" class="edge">
+<title>google.golang.org/grpc/connectivity&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M1851.3309,-190.917C1858.6628,-189.9195 1865.9391,-188.939 1873,-188 2089.4046,-159.2204 2350.0059,-127.1449 2440.8522,-116.04"/>
+<polygon fill="#000000" stroke="#000000" points="2441.0704,-117.7764 2445.8212,-115.4328 2440.6459,-114.3022 2441.0704,-117.7764"/>
+</g>
+<!-- google.golang.org/grpc/grpclog -->
+<g id="node66" class="node">
+<title>google.golang.org/grpc/grpclog</title>
+<g id="a_node66"><a xlink:href="https://godoc.org/google.golang.org/grpc/grpclog" xlink:title="google.golang.org/grpc/grpclog" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1820,-130C1820,-130 1656,-130 1656,-130 1650,-130 1644,-124 1644,-118 1644,-118 1644,-106 1644,-106 1644,-100 1650,-94 1656,-94 1656,-94 1820,-94 1820,-94 1826,-94 1832,-100 1832,-106 1832,-106 1832,-118 1832,-118 1832,-124 1826,-130 1820,-130"/>
+<text text-anchor="middle" x="1738" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/grpclog</text>
+</a>
+</g>
+</g>
+<!-- google.golang.org/grpc/connectivity&#45;&gt;google.golang.org/grpc/grpclog -->
+<g id="edge227" class="edge">
+<title>google.golang.org/grpc/connectivity&#45;&gt;google.golang.org/grpc/grpclog</title>
+<path fill="none" stroke="#000000" d="M1742.8431,-187.8759C1741.8886,-172.9211 1740.5276,-151.5983 1739.4912,-135.3629"/>
+<polygon fill="#000000" stroke="#000000" points="1741.2183,-134.9468 1739.1533,-130.0685 1737.7255,-135.1699 1741.2183,-134.9468"/>
+</g>
+<!-- google.golang.org/grpc/grpclog&#45;&gt;io -->
+<g id="edge228" class="edge">
+<title>google.golang.org/grpc/grpclog&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M1643.6904,-97.8906C1634.0063,-96.5341 1624.3093,-95.2133 1615,-94 1390.0992,-64.6871 1331.9839,-71.6485 1108,-36 1089.4379,-33.0457 1068.8472,-28.8832 1052.2531,-25.305"/>
+<polygon fill="#000000" stroke="#000000" points="1052.4957,-23.567 1047.2381,-24.2153 1051.7525,-26.9871 1052.4957,-23.567"/>
+</g>
+<!-- google.golang.org/grpc/grpclog&#45;&gt;os -->
+<g id="edge231" class="edge">
+<title>google.golang.org/grpc/grpclog&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M1832.1241,-108.9128C2097.3981,-100.0108 2873.7408,-72.5747 3518,-36 3597.847,-31.4671 3691.6867,-24.3518 3739.8681,-20.5676"/>
+<polygon fill="#000000" stroke="#000000" points="3740.103,-22.3046 3744.9502,-20.1675 3739.8282,-18.8154 3740.103,-22.3046"/>
+</g>
+<!-- google.golang.org/grpc/grpclog&#45;&gt;strconv -->
+<g id="edge232" class="edge">
+<title>google.golang.org/grpc/grpclog&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M1643.6761,-102.6865C1436.8219,-82.2618 951.2631,-34.318 819.4957,-21.3073"/>
+<polygon fill="#000000" stroke="#000000" points="819.3696,-19.5365 814.2219,-20.7866 819.0257,-23.0195 819.3696,-19.5365"/>
+</g>
+<!-- google.golang.org/grpc/grpclog&#45;&gt;io/ioutil -->
+<g id="edge229" class="edge">
+<title>google.golang.org/grpc/grpclog&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M1643.9627,-106.6427C1329.5495,-88.7307 323.4217,-31.4119 122.9409,-19.9906"/>
+<polygon fill="#000000" stroke="#000000" points="122.8905,-18.235 117.799,-19.6976 122.6914,-21.7293 122.8905,-18.235"/>
+</g>
+<!-- google.golang.org/grpc/grpclog&#45;&gt;log -->
+<g id="edge230" class="edge">
+<title>google.golang.org/grpc/grpclog&#45;&gt;log</title>
+<path fill="none" stroke="#000000" d="M1832.3194,-109.2693C2211.6247,-98.1887 3614.804,-56.2137 3813,-36 3838.33,-33.4166 3866.7405,-28.5434 3887.9173,-24.5185"/>
+<polygon fill="#000000" stroke="#000000" points="3888.2853,-26.2299 3892.8657,-23.5685 3887.6254,-22.7927 3888.2853,-26.2299"/>
+</g>
+<!-- google.golang.org/grpc/internal&#45;&gt;context -->
+<g id="edge233" class="edge">
+<title>google.golang.org/grpc/internal&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M1834.5225,-281.6535C1850.3382,-255.9583 1882.822,-209.9441 1924,-188 2014.6619,-139.6855 2335.6434,-119.1387 2440.553,-113.6141"/>
+<polygon fill="#000000" stroke="#000000" points="2440.7772,-115.3549 2445.6793,-113.3471 2440.595,-111.8596 2440.7772,-115.3549"/>
+</g>
+<!-- google.golang.org/grpc/internal&#45;&gt;time -->
+<g id="edge235" class="edge">
+<title>google.golang.org/grpc/internal&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M1763.931,-281.8814C1722.8146,-268.4122 1667.6849,-248.2197 1622,-224 1533.5338,-177.1 1528.7388,-136.3361 1438,-94 1350.3828,-53.1204 1237.1265,-31.4145 1182.4175,-22.6808"/>
+<polygon fill="#000000" stroke="#000000" points="1182.5201,-20.9255 1177.3086,-21.8759 1181.9753,-24.3828 1182.5201,-20.9255"/>
+</g>
+<!-- google.golang.org/grpc/internal&#45;&gt;google.golang.org/grpc/connectivity -->
+<g id="edge234" class="edge">
+<title>google.golang.org/grpc/internal&#45;&gt;google.golang.org/grpc/connectivity</title>
+<path fill="none" stroke="#000000" d="M1808.5752,-281.8759C1795.5157,-266.531 1776.7502,-244.4815 1762.8109,-228.1029"/>
+<polygon fill="#000000" stroke="#000000" points="1763.9508,-226.742 1759.3775,-224.0685 1761.2854,-229.0104 1763.9508,-226.742"/>
+</g>
+</g>
+</svg>
diff --git a/images/containers.dot.svg b/images/containers.dot.svg
new file mode 100644
index 0000000..38135cf
--- /dev/null
+++ b/images/containers.dot.svg
@@ -0,0 +1,5365 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: godep Pages: 1 -->
+<svg width="4159pt" height="20510pt"
+ viewBox="0.00 0.00 4159.00 20509.79" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 20505.7905)">
+<title>godep</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-20505.7905 4155,-20505.7905 4155,4 -4,4"/>
+<!-- bufio -->
+<g id="node1" class="node">
+<title>bufio</title>
+<g id="a_node1"><a xlink:href="https://godoc.org/bufio" xlink:title="bufio" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-13964.5998C4111,-13964.5998 4081,-13964.5998 4081,-13964.5998 4075,-13964.5998 4069,-13958.5998 4069,-13952.5998 4069,-13952.5998 4069,-13940.5998 4069,-13940.5998 4069,-13934.5998 4075,-13928.5998 4081,-13928.5998 4081,-13928.5998 4111,-13928.5998 4111,-13928.5998 4117,-13928.5998 4123,-13934.5998 4123,-13940.5998 4123,-13940.5998 4123,-13952.5998 4123,-13952.5998 4123,-13958.5998 4117,-13964.5998 4111,-13964.5998"/>
+<text text-anchor="middle" x="4096" y="-13942.8998" font-family="Times,serif" font-size="14.00" fill="#000000">bufio</text>
+</a>
+</g>
+</g>
+<!-- bytes -->
+<g id="node2" class="node">
+<title>bytes</title>
+<g id="a_node2"><a xlink:href="https://godoc.org/bytes" xlink:title="bytes" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-13722.5998C4111,-13722.5998 4081,-13722.5998 4081,-13722.5998 4075,-13722.5998 4069,-13716.5998 4069,-13710.5998 4069,-13710.5998 4069,-13698.5998 4069,-13698.5998 4069,-13692.5998 4075,-13686.5998 4081,-13686.5998 4081,-13686.5998 4111,-13686.5998 4111,-13686.5998 4117,-13686.5998 4123,-13692.5998 4123,-13698.5998 4123,-13698.5998 4123,-13710.5998 4123,-13710.5998 4123,-13716.5998 4117,-13722.5998 4111,-13722.5998"/>
+<text text-anchor="middle" x="4096" y="-13700.8998" font-family="Times,serif" font-size="14.00" fill="#000000">bytes</text>
+</a>
+</g>
+</g>
+<!-- compress/bzip2 -->
+<g id="node3" class="node">
+<title>compress/bzip2</title>
+<g id="a_node3"><a xlink:href="https://godoc.org/compress/bzip2" xlink:title="compress/bzip2" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2996,-12242.5998C2996,-12242.5998 2918,-12242.5998 2918,-12242.5998 2912,-12242.5998 2906,-12236.5998 2906,-12230.5998 2906,-12230.5998 2906,-12218.5998 2906,-12218.5998 2906,-12212.5998 2912,-12206.5998 2918,-12206.5998 2918,-12206.5998 2996,-12206.5998 2996,-12206.5998 3002,-12206.5998 3008,-12212.5998 3008,-12218.5998 3008,-12218.5998 3008,-12230.5998 3008,-12230.5998 3008,-12236.5998 3002,-12242.5998 2996,-12242.5998"/>
+<text text-anchor="middle" x="2957" y="-12220.8998" font-family="Times,serif" font-size="14.00" fill="#000000">compress/bzip2</text>
+</a>
+</g>
+</g>
+<!-- compress/gzip -->
+<g id="node4" class="node">
+<title>compress/gzip</title>
+<g id="a_node4"><a xlink:href="https://godoc.org/compress/gzip" xlink:title="compress/gzip" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2612,-5544.5998C2612,-5544.5998 2541,-5544.5998 2541,-5544.5998 2535,-5544.5998 2529,-5538.5998 2529,-5532.5998 2529,-5532.5998 2529,-5520.5998 2529,-5520.5998 2529,-5514.5998 2535,-5508.5998 2541,-5508.5998 2541,-5508.5998 2612,-5508.5998 2612,-5508.5998 2618,-5508.5998 2624,-5514.5998 2624,-5520.5998 2624,-5520.5998 2624,-5532.5998 2624,-5532.5998 2624,-5538.5998 2618,-5544.5998 2612,-5544.5998"/>
+<text text-anchor="middle" x="2576.5" y="-5522.8998" font-family="Times,serif" font-size="14.00" fill="#000000">compress/gzip</text>
+</a>
+</g>
+</g>
+<!-- context -->
+<g id="node5" class="node">
+<title>context</title>
+<g id="a_node5"><a xlink:href="https://godoc.org/context" xlink:title="context" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4112,-952.5998C4112,-952.5998 4080,-952.5998 4080,-952.5998 4074,-952.5998 4068,-946.5998 4068,-940.5998 4068,-940.5998 4068,-928.5998 4068,-928.5998 4068,-922.5998 4074,-916.5998 4080,-916.5998 4080,-916.5998 4112,-916.5998 4112,-916.5998 4118,-916.5998 4124,-922.5998 4124,-928.5998 4124,-928.5998 4124,-940.5998 4124,-940.5998 4124,-946.5998 4118,-952.5998 4112,-952.5998"/>
+<text text-anchor="middle" x="4096" y="-930.8998" font-family="Times,serif" font-size="14.00" fill="#000000">context</text>
+</a>
+</g>
+</g>
+<!-- crypto -->
+<g id="node6" class="node">
+<title>crypto</title>
+<g id="a_node6"><a xlink:href="https://godoc.org/crypto" xlink:title="crypto" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-16453.5998C4111,-16453.5998 4081,-16453.5998 4081,-16453.5998 4075,-16453.5998 4069,-16447.5998 4069,-16441.5998 4069,-16441.5998 4069,-16429.5998 4069,-16429.5998 4069,-16423.5998 4075,-16417.5998 4081,-16417.5998 4081,-16417.5998 4111,-16417.5998 4111,-16417.5998 4117,-16417.5998 4123,-16423.5998 4123,-16429.5998 4123,-16429.5998 4123,-16441.5998 4123,-16441.5998 4123,-16447.5998 4117,-16453.5998 4111,-16453.5998"/>
+<text text-anchor="middle" x="4096" y="-16431.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto</text>
+</a>
+</g>
+</g>
+<!-- crypto/ecdsa -->
+<g id="node7" class="node">
+<title>crypto/ecdsa</title>
+<g id="a_node7"><a xlink:href="https://godoc.org/crypto/ecdsa" xlink:title="crypto/ecdsa" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4126.5,-16670.5998C4126.5,-16670.5998 4065.5,-16670.5998 4065.5,-16670.5998 4059.5,-16670.5998 4053.5,-16664.5998 4053.5,-16658.5998 4053.5,-16658.5998 4053.5,-16646.5998 4053.5,-16646.5998 4053.5,-16640.5998 4059.5,-16634.5998 4065.5,-16634.5998 4065.5,-16634.5998 4126.5,-16634.5998 4126.5,-16634.5998 4132.5,-16634.5998 4138.5,-16640.5998 4138.5,-16646.5998 4138.5,-16646.5998 4138.5,-16658.5998 4138.5,-16658.5998 4138.5,-16664.5998 4132.5,-16670.5998 4126.5,-16670.5998"/>
+<text text-anchor="middle" x="4096" y="-16648.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/ecdsa</text>
+</a>
+</g>
+</g>
+<!-- crypto/elliptic -->
+<g id="node8" class="node">
+<title>crypto/elliptic</title>
+<g id="a_node8"><a xlink:href="https://godoc.org/crypto/elliptic" xlink:title="crypto/elliptic" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4130,-17125.5998C4130,-17125.5998 4062,-17125.5998 4062,-17125.5998 4056,-17125.5998 4050,-17119.5998 4050,-17113.5998 4050,-17113.5998 4050,-17101.5998 4050,-17101.5998 4050,-17095.5998 4056,-17089.5998 4062,-17089.5998 4062,-17089.5998 4130,-17089.5998 4130,-17089.5998 4136,-17089.5998 4142,-17095.5998 4142,-17101.5998 4142,-17101.5998 4142,-17113.5998 4142,-17113.5998 4142,-17119.5998 4136,-17125.5998 4130,-17125.5998"/>
+<text text-anchor="middle" x="4096" y="-17103.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/elliptic</text>
+</a>
+</g>
+</g>
+<!-- crypto/rand -->
+<g id="node9" class="node">
+<title>crypto/rand</title>
+<g id="a_node9"><a xlink:href="https://godoc.org/crypto/rand" xlink:title="crypto/rand" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4123.5,-19192.5998C4123.5,-19192.5998 4068.5,-19192.5998 4068.5,-19192.5998 4062.5,-19192.5998 4056.5,-19186.5998 4056.5,-19180.5998 4056.5,-19180.5998 4056.5,-19168.5998 4056.5,-19168.5998 4056.5,-19162.5998 4062.5,-19156.5998 4068.5,-19156.5998 4068.5,-19156.5998 4123.5,-19156.5998 4123.5,-19156.5998 4129.5,-19156.5998 4135.5,-19162.5998 4135.5,-19168.5998 4135.5,-19168.5998 4135.5,-19180.5998 4135.5,-19180.5998 4135.5,-19186.5998 4129.5,-19192.5998 4123.5,-19192.5998"/>
+<text text-anchor="middle" x="4096" y="-19170.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/rand</text>
+</a>
+</g>
+</g>
+<!-- crypto/rsa -->
+<g id="node10" class="node">
+<title>crypto/rsa</title>
+<g id="a_node10"><a xlink:href="https://godoc.org/crypto/rsa" xlink:title="crypto/rsa" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4119.5,-17060.5998C4119.5,-17060.5998 4072.5,-17060.5998 4072.5,-17060.5998 4066.5,-17060.5998 4060.5,-17054.5998 4060.5,-17048.5998 4060.5,-17048.5998 4060.5,-17036.5998 4060.5,-17036.5998 4060.5,-17030.5998 4066.5,-17024.5998 4072.5,-17024.5998 4072.5,-17024.5998 4119.5,-17024.5998 4119.5,-17024.5998 4125.5,-17024.5998 4131.5,-17030.5998 4131.5,-17036.5998 4131.5,-17036.5998 4131.5,-17048.5998 4131.5,-17048.5998 4131.5,-17054.5998 4125.5,-17060.5998 4119.5,-17060.5998"/>
+<text text-anchor="middle" x="4096" y="-17038.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/rsa</text>
+</a>
+</g>
+</g>
+<!-- crypto/sha256 -->
+<g id="node11" class="node">
+<title>crypto/sha256</title>
+<g id="a_node11"><a xlink:href="https://godoc.org/crypto/sha256" xlink:title="crypto/sha256" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4130.5,-16605.5998C4130.5,-16605.5998 4061.5,-16605.5998 4061.5,-16605.5998 4055.5,-16605.5998 4049.5,-16599.5998 4049.5,-16593.5998 4049.5,-16593.5998 4049.5,-16581.5998 4049.5,-16581.5998 4049.5,-16575.5998 4055.5,-16569.5998 4061.5,-16569.5998 4061.5,-16569.5998 4130.5,-16569.5998 4130.5,-16569.5998 4136.5,-16569.5998 4142.5,-16575.5998 4142.5,-16581.5998 4142.5,-16581.5998 4142.5,-16593.5998 4142.5,-16593.5998 4142.5,-16599.5998 4136.5,-16605.5998 4130.5,-16605.5998"/>
+<text text-anchor="middle" x="4096" y="-16583.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/sha256</text>
+</a>
+</g>
+</g>
+<!-- crypto/sha512 -->
+<g id="node12" class="node">
+<title>crypto/sha512</title>
+<g id="a_node12"><a xlink:href="https://godoc.org/crypto/sha512" xlink:title="crypto/sha512" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4130.5,-16995.5998C4130.5,-16995.5998 4061.5,-16995.5998 4061.5,-16995.5998 4055.5,-16995.5998 4049.5,-16989.5998 4049.5,-16983.5998 4049.5,-16983.5998 4049.5,-16971.5998 4049.5,-16971.5998 4049.5,-16965.5998 4055.5,-16959.5998 4061.5,-16959.5998 4061.5,-16959.5998 4130.5,-16959.5998 4130.5,-16959.5998 4136.5,-16959.5998 4142.5,-16965.5998 4142.5,-16971.5998 4142.5,-16971.5998 4142.5,-16983.5998 4142.5,-16983.5998 4142.5,-16989.5998 4136.5,-16995.5998 4130.5,-16995.5998"/>
+<text text-anchor="middle" x="4096" y="-16973.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/sha512</text>
+</a>
+</g>
+</g>
+<!-- crypto/tls -->
+<g id="node13" class="node">
+<title>crypto/tls</title>
+<g id="a_node13"><a xlink:href="https://godoc.org/crypto/tls" xlink:title="crypto/tls" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4118,-19280.5998C4118,-19280.5998 4074,-19280.5998 4074,-19280.5998 4068,-19280.5998 4062,-19274.5998 4062,-19268.5998 4062,-19268.5998 4062,-19256.5998 4062,-19256.5998 4062,-19250.5998 4068,-19244.5998 4074,-19244.5998 4074,-19244.5998 4118,-19244.5998 4118,-19244.5998 4124,-19244.5998 4130,-19250.5998 4130,-19256.5998 4130,-19256.5998 4130,-19268.5998 4130,-19268.5998 4130,-19274.5998 4124,-19280.5998 4118,-19280.5998"/>
+<text text-anchor="middle" x="4096" y="-19258.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/tls</text>
+</a>
+</g>
+</g>
+<!-- crypto/x509 -->
+<g id="node14" class="node">
+<title>crypto/x509</title>
+<g id="a_node14"><a xlink:href="https://godoc.org/crypto/x509" xlink:title="crypto/x509" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4125,-18234.5998C4125,-18234.5998 4067,-18234.5998 4067,-18234.5998 4061,-18234.5998 4055,-18228.5998 4055,-18222.5998 4055,-18222.5998 4055,-18210.5998 4055,-18210.5998 4055,-18204.5998 4061,-18198.5998 4067,-18198.5998 4067,-18198.5998 4125,-18198.5998 4125,-18198.5998 4131,-18198.5998 4137,-18204.5998 4137,-18210.5998 4137,-18210.5998 4137,-18222.5998 4137,-18222.5998 4137,-18228.5998 4131,-18234.5998 4125,-18234.5998"/>
+<text text-anchor="middle" x="4096" y="-18212.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/x509</text>
+</a>
+</g>
+</g>
+<!-- crypto/x509/pkix -->
+<g id="node15" class="node">
+<title>crypto/x509/pkix</title>
+<g id="a_node15"><a xlink:href="https://godoc.org/crypto/x509/pkix" xlink:title="crypto/x509/pkix" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4139,-16865.5998C4139,-16865.5998 4053,-16865.5998 4053,-16865.5998 4047,-16865.5998 4041,-16859.5998 4041,-16853.5998 4041,-16853.5998 4041,-16841.5998 4041,-16841.5998 4041,-16835.5998 4047,-16829.5998 4053,-16829.5998 4053,-16829.5998 4139,-16829.5998 4139,-16829.5998 4145,-16829.5998 4151,-16835.5998 4151,-16841.5998 4151,-16841.5998 4151,-16853.5998 4151,-16853.5998 4151,-16859.5998 4145,-16865.5998 4139,-16865.5998"/>
+<text text-anchor="middle" x="4096" y="-16843.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/x509/pkix</text>
+</a>
+</g>
+</g>
+<!-- encoding -->
+<g id="node16" class="node">
+<title>encoding</title>
+<g id="a_node16"><a xlink:href="https://godoc.org/encoding" xlink:title="encoding" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4117,-4764.5998C4117,-4764.5998 4075,-4764.5998 4075,-4764.5998 4069,-4764.5998 4063,-4758.5998 4063,-4752.5998 4063,-4752.5998 4063,-4740.5998 4063,-4740.5998 4063,-4734.5998 4069,-4728.5998 4075,-4728.5998 4075,-4728.5998 4117,-4728.5998 4117,-4728.5998 4123,-4728.5998 4129,-4734.5998 4129,-4740.5998 4129,-4740.5998 4129,-4752.5998 4129,-4752.5998 4129,-4758.5998 4123,-4764.5998 4117,-4764.5998"/>
+<text text-anchor="middle" x="4096" y="-4742.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding</text>
+</a>
+</g>
+</g>
+<!-- encoding/base32 -->
+<g id="node17" class="node">
+<title>encoding/base32</title>
+<g id="a_node17"><a xlink:href="https://godoc.org/encoding/base32" xlink:title="encoding/base32" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4137.5,-16800.5998C4137.5,-16800.5998 4054.5,-16800.5998 4054.5,-16800.5998 4048.5,-16800.5998 4042.5,-16794.5998 4042.5,-16788.5998 4042.5,-16788.5998 4042.5,-16776.5998 4042.5,-16776.5998 4042.5,-16770.5998 4048.5,-16764.5998 4054.5,-16764.5998 4054.5,-16764.5998 4137.5,-16764.5998 4137.5,-16764.5998 4143.5,-16764.5998 4149.5,-16770.5998 4149.5,-16776.5998 4149.5,-16776.5998 4149.5,-16788.5998 4149.5,-16788.5998 4149.5,-16794.5998 4143.5,-16800.5998 4137.5,-16800.5998"/>
+<text text-anchor="middle" x="4096" y="-16778.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/base32</text>
+</a>
+</g>
+</g>
+<!-- encoding/base64 -->
+<g id="node18" class="node">
+<title>encoding/base64</title>
+<g id="a_node18"><a xlink:href="https://godoc.org/encoding/base64" xlink:title="encoding/base64" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4137.5,-16930.5998C4137.5,-16930.5998 4054.5,-16930.5998 4054.5,-16930.5998 4048.5,-16930.5998 4042.5,-16924.5998 4042.5,-16918.5998 4042.5,-16918.5998 4042.5,-16906.5998 4042.5,-16906.5998 4042.5,-16900.5998 4048.5,-16894.5998 4054.5,-16894.5998 4054.5,-16894.5998 4137.5,-16894.5998 4137.5,-16894.5998 4143.5,-16894.5998 4149.5,-16900.5998 4149.5,-16906.5998 4149.5,-16906.5998 4149.5,-16918.5998 4149.5,-16918.5998 4149.5,-16924.5998 4143.5,-16930.5998 4137.5,-16930.5998"/>
+<text text-anchor="middle" x="4096" y="-16908.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/base64</text>
+</a>
+</g>
+</g>
+<!-- encoding/binary -->
+<g id="node19" class="node">
+<title>encoding/binary</title>
+<g id="a_node19"><a xlink:href="https://godoc.org/encoding/binary" xlink:title="encoding/binary" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4136,-12132.5998C4136,-12132.5998 4056,-12132.5998 4056,-12132.5998 4050,-12132.5998 4044,-12126.5998 4044,-12120.5998 4044,-12120.5998 4044,-12108.5998 4044,-12108.5998 4044,-12102.5998 4050,-12096.5998 4056,-12096.5998 4056,-12096.5998 4136,-12096.5998 4136,-12096.5998 4142,-12096.5998 4148,-12102.5998 4148,-12108.5998 4148,-12108.5998 4148,-12120.5998 4148,-12120.5998 4148,-12126.5998 4142,-12132.5998 4136,-12132.5998"/>
+<text text-anchor="middle" x="4096" y="-12110.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/binary</text>
+</a>
+</g>
+</g>
+<!-- encoding/hex -->
+<g id="node20" class="node">
+<title>encoding/hex</title>
+<g id="a_node20"><a xlink:href="https://godoc.org/encoding/hex" xlink:title="encoding/hex" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3814,-9299.5998C3814,-9299.5998 3749,-9299.5998 3749,-9299.5998 3743,-9299.5998 3737,-9293.5998 3737,-9287.5998 3737,-9287.5998 3737,-9275.5998 3737,-9275.5998 3737,-9269.5998 3743,-9263.5998 3749,-9263.5998 3749,-9263.5998 3814,-9263.5998 3814,-9263.5998 3820,-9263.5998 3826,-9269.5998 3826,-9275.5998 3826,-9275.5998 3826,-9287.5998 3826,-9287.5998 3826,-9293.5998 3820,-9299.5998 3814,-9299.5998"/>
+<text text-anchor="middle" x="3781.5" y="-9277.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/hex</text>
+</a>
+</g>
+</g>
+<!-- encoding/json -->
+<g id="node21" class="node">
+<title>encoding/json</title>
+<g id="a_node21"><a xlink:href="https://godoc.org/encoding/json" xlink:title="encoding/json" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4130,-17255.5998C4130,-17255.5998 4062,-17255.5998 4062,-17255.5998 4056,-17255.5998 4050,-17249.5998 4050,-17243.5998 4050,-17243.5998 4050,-17231.5998 4050,-17231.5998 4050,-17225.5998 4056,-17219.5998 4062,-17219.5998 4062,-17219.5998 4130,-17219.5998 4130,-17219.5998 4136,-17219.5998 4142,-17225.5998 4142,-17231.5998 4142,-17231.5998 4142,-17243.5998 4142,-17243.5998 4142,-17249.5998 4136,-17255.5998 4130,-17255.5998"/>
+<text text-anchor="middle" x="4096" y="-17233.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/json</text>
+</a>
+</g>
+</g>
+<!-- encoding/pem -->
+<g id="node22" class="node">
+<title>encoding/pem</title>
+<g id="a_node22"><a xlink:href="https://godoc.org/encoding/pem" xlink:title="encoding/pem" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4131,-18509.5998C4131,-18509.5998 4061,-18509.5998 4061,-18509.5998 4055,-18509.5998 4049,-18503.5998 4049,-18497.5998 4049,-18497.5998 4049,-18485.5998 4049,-18485.5998 4049,-18479.5998 4055,-18473.5998 4061,-18473.5998 4061,-18473.5998 4131,-18473.5998 4131,-18473.5998 4137,-18473.5998 4143,-18479.5998 4143,-18485.5998 4143,-18485.5998 4143,-18497.5998 4143,-18497.5998 4143,-18503.5998 4137,-18509.5998 4131,-18509.5998"/>
+<text text-anchor="middle" x="4096" y="-18487.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/pem</text>
+</a>
+</g>
+</g>
+<!-- errors -->
+<g id="node23" class="node">
+<title>errors</title>
+<g id="a_node23"><a xlink:href="https://godoc.org/errors" xlink:title="errors" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-6800.5998C4111,-6800.5998 4081,-6800.5998 4081,-6800.5998 4075,-6800.5998 4069,-6794.5998 4069,-6788.5998 4069,-6788.5998 4069,-6776.5998 4069,-6776.5998 4069,-6770.5998 4075,-6764.5998 4081,-6764.5998 4081,-6764.5998 4111,-6764.5998 4111,-6764.5998 4117,-6764.5998 4123,-6770.5998 4123,-6776.5998 4123,-6776.5998 4123,-6788.5998 4123,-6788.5998 4123,-6794.5998 4117,-6800.5998 4111,-6800.5998"/>
+<text text-anchor="middle" x="4096" y="-6778.8998" font-family="Times,serif" font-size="14.00" fill="#000000">errors</text>
+</a>
+</g>
+</g>
+<!-- expvar -->
+<g id="node24" class="node">
+<title>expvar</title>
+<g id="a_node24"><a xlink:href="https://godoc.org/expvar" xlink:title="expvar" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2972,-6835.5998C2972,-6835.5998 2942,-6835.5998 2942,-6835.5998 2936,-6835.5998 2930,-6829.5998 2930,-6823.5998 2930,-6823.5998 2930,-6811.5998 2930,-6811.5998 2930,-6805.5998 2936,-6799.5998 2942,-6799.5998 2942,-6799.5998 2972,-6799.5998 2972,-6799.5998 2978,-6799.5998 2984,-6805.5998 2984,-6811.5998 2984,-6811.5998 2984,-6823.5998 2984,-6823.5998 2984,-6829.5998 2978,-6835.5998 2972,-6835.5998"/>
+<text text-anchor="middle" x="2957" y="-6813.8998" font-family="Times,serif" font-size="14.00" fill="#000000">expvar</text>
+</a>
+</g>
+</g>
+<!-- fmt -->
+<g id="node25" class="node">
+<title>fmt</title>
+<g id="a_node25"><a xlink:href="https://godoc.org/fmt" xlink:title="fmt" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-8759.5998C4111,-8759.5998 4081,-8759.5998 4081,-8759.5998 4075,-8759.5998 4069,-8753.5998 4069,-8747.5998 4069,-8747.5998 4069,-8735.5998 4069,-8735.5998 4069,-8729.5998 4075,-8723.5998 4081,-8723.5998 4081,-8723.5998 4111,-8723.5998 4111,-8723.5998 4117,-8723.5998 4123,-8729.5998 4123,-8735.5998 4123,-8735.5998 4123,-8747.5998 4123,-8747.5998 4123,-8753.5998 4117,-8759.5998 4111,-8759.5998"/>
+<text text-anchor="middle" x="4096" y="-8737.8998" font-family="Times,serif" font-size="14.00" fill="#000000">fmt</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml -->
+<g id="node26" class="node">
+<title>github.com/BurntSushi/toml</title>
+<g id="a_node26"><a xlink:href="https://godoc.org/github.com/BurntSushi/toml" xlink:title="github.com/BurntSushi/toml" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3856,-5068.5998C3856,-5068.5998 3707,-5068.5998 3707,-5068.5998 3701,-5068.5998 3695,-5062.5998 3695,-5056.5998 3695,-5056.5998 3695,-5044.5998 3695,-5044.5998 3695,-5038.5998 3701,-5032.5998 3707,-5032.5998 3707,-5032.5998 3856,-5032.5998 3856,-5032.5998 3862,-5032.5998 3868,-5038.5998 3868,-5044.5998 3868,-5044.5998 3868,-5056.5998 3868,-5056.5998 3868,-5062.5998 3862,-5068.5998 3856,-5068.5998"/>
+<text text-anchor="middle" x="3781.5" y="-5046.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/BurntSushi/toml</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;bufio -->
+<g id="edge1" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3801.8355,-5068.6028C3845.2465,-5108.5584 3946.0347,-5209.8307 3983,-5319.5998 4059.1976,-5545.8698 3921.2706,-13708.035 4041,-13914.5998 4046.2845,-13923.717 4055.2673,-13930.4827 4064.4585,-13935.3836"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7174,-13936.9695 4068.972,-13937.6417 4065.2835,-13933.8394 4063.7174,-13936.9695"/>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;encoding -->
+<g id="edge2" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;encoding</title>
+<path fill="none" stroke="#000000" d="M3848.6679,-5032.5935C3891.2434,-5018.4258 3945.2123,-4995.0587 3983,-4959.5998 4041.52,-4904.6864 4075.7626,-4812.5703 4089.2712,-4769.765"/>
+<polygon fill="#000000" stroke="#000000" points="4091.003,-4770.0892 4090.8161,-4764.7951 4087.6607,-4769.0502 4091.003,-4770.0892"/>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;errors -->
+<g id="edge3" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3868.0246,-5045.2629C3908.9066,-5047.6916 3954.8139,-5058.5122 3983,-5090.5998 4040.7474,-5156.3404 4088.7051,-6559.6055 4095.2413,-6759.0902"/>
+<polygon fill="#000000" stroke="#000000" points="4093.5016,-6759.4353 4095.414,-6764.3755 4096.9997,-6759.321 4093.5016,-6759.4353"/>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;fmt -->
+<g id="edge4" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3801.7125,-5068.6447C3844.8814,-5108.6829 3945.222,-5210.1077 3983,-5319.5998 4031.7181,-5460.7996 4033.8058,-7858.4051 4041,-8007.5998 4054.5175,-8287.9281 4085.322,-8627.4946 4093.803,-8718.3329"/>
+<polygon fill="#000000" stroke="#000000" points="4092.0747,-8718.6477 4094.283,-8723.4629 4095.5595,-8718.3216 4092.0747,-8718.6477"/>
+</g>
+<!-- io -->
+<g id="node27" class="node">
+<title>io</title>
+<g id="a_node27"><a xlink:href="https://godoc.org/io" xlink:title="io" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-13899.5998C4111,-13899.5998 4081,-13899.5998 4081,-13899.5998 4075,-13899.5998 4069,-13893.5998 4069,-13887.5998 4069,-13887.5998 4069,-13875.5998 4069,-13875.5998 4069,-13869.5998 4075,-13863.5998 4081,-13863.5998 4081,-13863.5998 4111,-13863.5998 4111,-13863.5998 4117,-13863.5998 4123,-13869.5998 4123,-13875.5998 4123,-13875.5998 4123,-13887.5998 4123,-13887.5998 4123,-13893.5998 4117,-13899.5998 4111,-13899.5998"/>
+<text text-anchor="middle" x="4096" y="-13877.8998" font-family="Times,serif" font-size="14.00" fill="#000000">io</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;io -->
+<g id="edge5" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3801.8343,-5068.6032C3845.243,-5108.5596 3946.0269,-5209.8333 3983,-5319.5998 4057.6442,-5541.205 3997.7868,-13507.7885 4041,-13737.5998 4049.4151,-13782.3518 4070.9421,-13831.2307 4084.433,-13858.9794"/>
+<polygon fill="#000000" stroke="#000000" points="4082.9159,-13859.8605 4086.6896,-13863.5783 4086.058,-13858.3186 4082.9159,-13859.8605"/>
+</g>
+<!-- io/ioutil -->
+<g id="node28" class="node">
+<title>io/ioutil</title>
+<g id="a_node28"><a xlink:href="https://godoc.org/io/ioutil" xlink:title="io/ioutil" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4113.5,-16318.5998C4113.5,-16318.5998 4078.5,-16318.5998 4078.5,-16318.5998 4072.5,-16318.5998 4066.5,-16312.5998 4066.5,-16306.5998 4066.5,-16306.5998 4066.5,-16294.5998 4066.5,-16294.5998 4066.5,-16288.5998 4072.5,-16282.5998 4078.5,-16282.5998 4078.5,-16282.5998 4113.5,-16282.5998 4113.5,-16282.5998 4119.5,-16282.5998 4125.5,-16288.5998 4125.5,-16294.5998 4125.5,-16294.5998 4125.5,-16306.5998 4125.5,-16306.5998 4125.5,-16312.5998 4119.5,-16318.5998 4113.5,-16318.5998"/>
+<text text-anchor="middle" x="4096" y="-16296.8998" font-family="Times,serif" font-size="14.00" fill="#000000">io/ioutil</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;io/ioutil -->
+<g id="edge6" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M3801.8363,-5068.6025C3845.249,-5108.5576 3946.0402,-5209.8288 3983,-5319.5998 4021.6692,-5434.4478 4039.1204,-13923.4312 4041,-14044.5998 4055.3143,-14967.3713 4089.8535,-16102.0781 4095.2778,-16277.3974"/>
+<polygon fill="#000000" stroke="#000000" points="4093.533,-16277.5928 4095.437,-16282.5362 4097.0313,-16277.4844 4093.533,-16277.5928"/>
+</g>
+<!-- math -->
+<g id="node29" class="node">
+<title>math</title>
+<g id="a_node29"><a xlink:href="https://godoc.org/math" xlink:title="math" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-3861.5998C4111,-3861.5998 4081,-3861.5998 4081,-3861.5998 4075,-3861.5998 4069,-3855.5998 4069,-3849.5998 4069,-3849.5998 4069,-3837.5998 4069,-3837.5998 4069,-3831.5998 4075,-3825.5998 4081,-3825.5998 4081,-3825.5998 4111,-3825.5998 4111,-3825.5998 4117,-3825.5998 4123,-3831.5998 4123,-3837.5998 4123,-3837.5998 4123,-3849.5998 4123,-3849.5998 4123,-3855.5998 4117,-3861.5998 4111,-3861.5998"/>
+<text text-anchor="middle" x="4096" y="-3839.8998" font-family="Times,serif" font-size="14.00" fill="#000000">math</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;math -->
+<g id="edge7" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3868.1396,-5033.2736C3909.8421,-5020.2512 3956.4446,-4997.8217 3983,-4959.5998 4037.266,-4881.4933 4027.9766,-4199.8114 4041,-4105.5998 4053.3176,-4016.494 4078.4466,-3912.3695 4089.9754,-3866.8649"/>
+<polygon fill="#000000" stroke="#000000" points="4091.7095,-3867.1463 4091.2462,-3861.8692 4088.3175,-3866.2834 4091.7095,-3867.1463"/>
+</g>
+<!-- reflect -->
+<g id="node30" class="node">
+<title>reflect</title>
+<g id="a_node30"><a xlink:href="https://godoc.org/reflect" xlink:title="reflect" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-5037.5998C4111,-5037.5998 4081,-5037.5998 4081,-5037.5998 4075,-5037.5998 4069,-5031.5998 4069,-5025.5998 4069,-5025.5998 4069,-5013.5998 4069,-5013.5998 4069,-5007.5998 4075,-5001.5998 4081,-5001.5998 4081,-5001.5998 4111,-5001.5998 4111,-5001.5998 4117,-5001.5998 4123,-5007.5998 4123,-5013.5998 4123,-5013.5998 4123,-5025.5998 4123,-5025.5998 4123,-5031.5998 4117,-5037.5998 4111,-5037.5998"/>
+<text text-anchor="middle" x="4096" y="-5015.8998" font-family="Times,serif" font-size="14.00" fill="#000000">reflect</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;reflect -->
+<g id="edge8" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M3868.271,-5042.0469C3933.0468,-5035.662 4017.9401,-5027.2941 4063.5899,-5022.7945"/>
+<polygon fill="#000000" stroke="#000000" points="4063.8854,-5024.5239 4068.6896,-5022.2918 4063.542,-5021.0408 4063.8854,-5024.5239"/>
+</g>
+<!-- sort -->
+<g id="node31" class="node">
+<title>sort</title>
+<g id="a_node31"><a xlink:href="https://godoc.org/sort" xlink:title="sort" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-4460.5998C4111,-4460.5998 4081,-4460.5998 4081,-4460.5998 4075,-4460.5998 4069,-4454.5998 4069,-4448.5998 4069,-4448.5998 4069,-4436.5998 4069,-4436.5998 4069,-4430.5998 4075,-4424.5998 4081,-4424.5998 4081,-4424.5998 4111,-4424.5998 4111,-4424.5998 4117,-4424.5998 4123,-4430.5998 4123,-4436.5998 4123,-4436.5998 4123,-4448.5998 4123,-4448.5998 4123,-4454.5998 4117,-4460.5998 4111,-4460.5998"/>
+<text text-anchor="middle" x="4096" y="-4438.8998" font-family="Times,serif" font-size="14.00" fill="#000000">sort</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;sort -->
+<g id="edge9" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3862.821,-5032.5467C3904.6318,-5019.1799 3952.9779,-4996.6577 3983,-4959.5998 3991.0611,-4949.6496 4070.83,-4564.6991 4091.2555,-4465.6487"/>
+<polygon fill="#000000" stroke="#000000" points="4092.9985,-4465.8606 4092.2942,-4460.6102 4089.5706,-4465.1539 4092.9985,-4465.8606"/>
+</g>
+<!-- strconv -->
+<g id="node32" class="node">
+<title>strconv</title>
+<g id="a_node32"><a xlink:href="https://godoc.org/strconv" xlink:title="strconv" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4112,-5369.5998C4112,-5369.5998 4080,-5369.5998 4080,-5369.5998 4074,-5369.5998 4068,-5363.5998 4068,-5357.5998 4068,-5357.5998 4068,-5345.5998 4068,-5345.5998 4068,-5339.5998 4074,-5333.5998 4080,-5333.5998 4080,-5333.5998 4112,-5333.5998 4112,-5333.5998 4118,-5333.5998 4124,-5339.5998 4124,-5345.5998 4124,-5345.5998 4124,-5357.5998 4124,-5357.5998 4124,-5363.5998 4118,-5369.5998 4112,-5369.5998"/>
+<text text-anchor="middle" x="4096" y="-5347.8998" font-family="Times,serif" font-size="14.00" fill="#000000">strconv</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;strconv -->
+<g id="edge10" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3868.2478,-5050.1661C3906.9897,-5054.1043 3951.0149,-5064.783 3983,-5090.5998 4059.7081,-5152.515 4085.4975,-5276.5946 4093.1347,-5328.2243"/>
+<polygon fill="#000000" stroke="#000000" points="4091.4178,-5328.5804 4093.8587,-5333.282 4094.8825,-5328.0844 4091.4178,-5328.5804"/>
+</g>
+<!-- strings -->
+<g id="node33" class="node">
+<title>strings</title>
+<g id="a_node33"><a xlink:href="https://godoc.org/strings" xlink:title="strings" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-7095.5998C4111,-7095.5998 4081,-7095.5998 4081,-7095.5998 4075,-7095.5998 4069,-7089.5998 4069,-7083.5998 4069,-7083.5998 4069,-7071.5998 4069,-7071.5998 4069,-7065.5998 4075,-7059.5998 4081,-7059.5998 4081,-7059.5998 4111,-7059.5998 4111,-7059.5998 4117,-7059.5998 4123,-7065.5998 4123,-7071.5998 4123,-7071.5998 4123,-7083.5998 4123,-7083.5998 4123,-7089.5998 4117,-7095.5998 4111,-7095.5998"/>
+<text text-anchor="middle" x="4096" y="-7073.8998" font-family="Times,serif" font-size="14.00" fill="#000000">strings</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;strings -->
+<g id="edge11" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3801.5704,-5068.6945C3844.4596,-5108.8304 3944.283,-5210.4362 3983,-5319.5998 4094.209,-5633.1568 4000.2362,-6485.4124 4041,-6815.5998 4052.0216,-6904.8752 4077.8142,-7008.9129 4089.7439,-7054.365"/>
+<polygon fill="#000000" stroke="#000000" points="4088.0926,-7054.9664 4091.0598,-7059.3548 4091.4769,-7054.0738 4088.0926,-7054.9664"/>
+</g>
+<!-- sync -->
+<g id="node34" class="node">
+<title>sync</title>
+<g id="a_node34"><a xlink:href="https://godoc.org/sync" xlink:title="sync" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-3206.5998C4111,-3206.5998 4081,-3206.5998 4081,-3206.5998 4075,-3206.5998 4069,-3200.5998 4069,-3194.5998 4069,-3194.5998 4069,-3182.5998 4069,-3182.5998 4069,-3176.5998 4075,-3170.5998 4081,-3170.5998 4081,-3170.5998 4111,-3170.5998 4111,-3170.5998 4117,-3170.5998 4123,-3176.5998 4123,-3182.5998 4123,-3182.5998 4123,-3194.5998 4123,-3194.5998 4123,-3200.5998 4117,-3206.5998 4111,-3206.5998"/>
+<text text-anchor="middle" x="4096" y="-3184.8998" font-family="Times,serif" font-size="14.00" fill="#000000">sync</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;sync -->
+<g id="edge12" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3868.1574,-5033.5438C3909.9997,-5020.5844 3956.7228,-4998.1297 3983,-4959.5998 4054.9613,-4854.0843 4032.1604,-3939.0118 4041,-3811.5998 4057.2698,-3577.0907 4085.3567,-3293.6614 4093.6375,-3211.7734"/>
+<polygon fill="#000000" stroke="#000000" points="4095.3912,-3211.8246 4094.154,-3206.6737 4091.909,-3211.4719 4095.3912,-3211.8246"/>
+</g>
+<!-- time -->
+<g id="node35" class="node">
+<title>time</title>
+<g id="a_node35"><a xlink:href="https://godoc.org/time" xlink:title="time" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-11403.5998C4111,-11403.5998 4081,-11403.5998 4081,-11403.5998 4075,-11403.5998 4069,-11397.5998 4069,-11391.5998 4069,-11391.5998 4069,-11379.5998 4069,-11379.5998 4069,-11373.5998 4075,-11367.5998 4081,-11367.5998 4081,-11367.5998 4111,-11367.5998 4111,-11367.5998 4117,-11367.5998 4123,-11373.5998 4123,-11379.5998 4123,-11379.5998 4123,-11391.5998 4123,-11391.5998 4123,-11397.5998 4117,-11403.5998 4111,-11403.5998"/>
+<text text-anchor="middle" x="4096" y="-11381.8998" font-family="Times,serif" font-size="14.00" fill="#000000">time</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;time -->
+<g id="edge13" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3801.8117,-5068.6108C3845.1759,-5108.5823 3945.8775,-5209.8837 3983,-5319.5998 4036.7221,-5478.3764 3956.7361,-11208.701 4041,-11353.5998 4046.2976,-11362.7094 4055.2837,-11369.4732 4064.4739,-11374.3746"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7321,-11375.9603 4068.9866,-11376.6333 4065.2987,-11372.8304 4063.7321,-11375.9603"/>
+</g>
+<!-- unicode -->
+<g id="node36" class="node">
+<title>unicode</title>
+<g id="a_node36"><a xlink:href="https://godoc.org/unicode" xlink:title="unicode" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4113.5,-4155.5998C4113.5,-4155.5998 4078.5,-4155.5998 4078.5,-4155.5998 4072.5,-4155.5998 4066.5,-4149.5998 4066.5,-4143.5998 4066.5,-4143.5998 4066.5,-4131.5998 4066.5,-4131.5998 4066.5,-4125.5998 4072.5,-4119.5998 4078.5,-4119.5998 4078.5,-4119.5998 4113.5,-4119.5998 4113.5,-4119.5998 4119.5,-4119.5998 4125.5,-4125.5998 4125.5,-4131.5998 4125.5,-4131.5998 4125.5,-4143.5998 4125.5,-4143.5998 4125.5,-4149.5998 4119.5,-4155.5998 4113.5,-4155.5998"/>
+<text text-anchor="middle" x="4096" y="-4133.8998" font-family="Times,serif" font-size="14.00" fill="#000000">unicode</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;unicode -->
+<g id="edge14" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;unicode</title>
+<path fill="none" stroke="#000000" d="M3868.2374,-5032.8172C3909.6983,-5019.694 3956.0545,-4997.3121 3983,-4959.5998 4062.6757,-4848.0875 4016.9567,-4480.5262 4041,-4345.5998 4053.1662,-4277.3257 4076.6146,-4198.6777 4088.5812,-4160.6146"/>
+<polygon fill="#000000" stroke="#000000" points="4090.2843,-4161.0327 4090.1215,-4155.7377 4086.9468,-4159.9785 4090.2843,-4161.0327"/>
+</g>
+<!-- unicode/utf8 -->
+<g id="node37" class="node">
+<title>unicode/utf8</title>
+<g id="a_node37"><a xlink:href="https://godoc.org/unicode/utf8" xlink:title="unicode/utf8" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4126.5,-4395.5998C4126.5,-4395.5998 4065.5,-4395.5998 4065.5,-4395.5998 4059.5,-4395.5998 4053.5,-4389.5998 4053.5,-4383.5998 4053.5,-4383.5998 4053.5,-4371.5998 4053.5,-4371.5998 4053.5,-4365.5998 4059.5,-4359.5998 4065.5,-4359.5998 4065.5,-4359.5998 4126.5,-4359.5998 4126.5,-4359.5998 4132.5,-4359.5998 4138.5,-4365.5998 4138.5,-4371.5998 4138.5,-4371.5998 4138.5,-4383.5998 4138.5,-4383.5998 4138.5,-4389.5998 4132.5,-4395.5998 4126.5,-4395.5998"/>
+<text text-anchor="middle" x="4096" y="-4373.8998" font-family="Times,serif" font-size="14.00" fill="#000000">unicode/utf8</text>
+</a>
+</g>
+</g>
+<!-- github.com/BurntSushi/toml&#45;&gt;unicode/utf8 -->
+<g id="edge15" class="edge">
+<title>github.com/BurntSushi/toml&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M3868.0629,-5032.6918C3909.4781,-5019.5357 3955.8461,-4997.1623 3983,-4959.5998 4126.743,-4760.7571 3909.0484,-4617.4555 4041,-4410.5998 4043.7847,-4406.2344 4047.3724,-4402.366 4051.3795,-4398.9582"/>
+<polygon fill="#000000" stroke="#000000" points="4052.6675,-4400.1714 4055.5122,-4395.7026 4050.5016,-4397.422 4052.6675,-4400.1714"/>
+</g>
+<!-- github.com/beorn7/perks/quantile -->
+<g id="node38" class="node">
+<title>github.com/beorn7/perks/quantile</title>
+<g id="a_node38"><a xlink:href="https://godoc.org/github.com/beorn7/perks/quantile" xlink:title="github.com/beorn7/perks/quantile" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3869.5,-3622.5998C3869.5,-3622.5998 3693.5,-3622.5998 3693.5,-3622.5998 3687.5,-3622.5998 3681.5,-3616.5998 3681.5,-3610.5998 3681.5,-3610.5998 3681.5,-3598.5998 3681.5,-3598.5998 3681.5,-3592.5998 3687.5,-3586.5998 3693.5,-3586.5998 3693.5,-3586.5998 3869.5,-3586.5998 3869.5,-3586.5998 3875.5,-3586.5998 3881.5,-3592.5998 3881.5,-3598.5998 3881.5,-3598.5998 3881.5,-3610.5998 3881.5,-3610.5998 3881.5,-3616.5998 3875.5,-3622.5998 3869.5,-3622.5998"/>
+<text text-anchor="middle" x="3781.5" y="-3600.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/beorn7/perks/quantile</text>
+</a>
+</g>
+</g>
+<!-- github.com/beorn7/perks/quantile&#45;&gt;math -->
+<g id="edge16" class="edge">
+<title>github.com/beorn7/perks/quantile&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3881.5317,-3607.6496C3916.2652,-3612.8458 3953.791,-3623.5188 3983,-3644.5998 4044.76,-3689.1739 4077.39,-3778.585 4089.8843,-3820.6838"/>
+<polygon fill="#000000" stroke="#000000" points="4088.2309,-3821.2655 4091.3088,-3825.5771 4091.5914,-3820.2872 4088.2309,-3821.2655"/>
+</g>
+<!-- github.com/beorn7/perks/quantile&#45;&gt;sort -->
+<g id="edge17" class="edge">
+<title>github.com/beorn7/perks/quantile&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3820.5599,-3622.7671C3868.0913,-3647.1815 3946.5072,-3695.4081 3983,-3762.5998 4121.0022,-4016.6941 3885.8028,-4166.6278 4041,-4410.5998 4046.4532,-4419.1723 4055.0867,-4425.7154 4063.9116,-4430.5809"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5695,-4432.3738 4068.815,-4433.1135 4065.1757,-4429.2641 4063.5695,-4432.3738"/>
+</g>
+<!-- github.com/cespare/xxhash/v2 -->
+<g id="node39" class="node">
+<title>github.com/cespare/xxhash/v2</title>
+<g id="a_node39"><a xlink:href="https://godoc.org/github.com/cespare/xxhash/v2" xlink:title="github.com/cespare/xxhash/v2" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3860.5,-10739.5998C3860.5,-10739.5998 3702.5,-10739.5998 3702.5,-10739.5998 3696.5,-10739.5998 3690.5,-10733.5998 3690.5,-10727.5998 3690.5,-10727.5998 3690.5,-10715.5998 3690.5,-10715.5998 3690.5,-10709.5998 3696.5,-10703.5998 3702.5,-10703.5998 3702.5,-10703.5998 3860.5,-10703.5998 3860.5,-10703.5998 3866.5,-10703.5998 3872.5,-10709.5998 3872.5,-10715.5998 3872.5,-10715.5998 3872.5,-10727.5998 3872.5,-10727.5998 3872.5,-10733.5998 3866.5,-10739.5998 3860.5,-10739.5998"/>
+<text text-anchor="middle" x="3781.5" y="-10717.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/cespare/xxhash/v2</text>
+</a>
+</g>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;encoding/binary -->
+<g id="edge18" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M3801.3418,-10739.7773C3843.7811,-10780.0764 3942.7726,-10881.9837 3983,-10990.5998 4050.3723,-11172.5087 4013.8778,-11669.521 4041,-11861.5998 4053.1029,-11947.3125 4078.1209,-12047.2398 4089.7741,-12091.4915"/>
+<polygon fill="#000000" stroke="#000000" points="4088.0902,-12091.9693 4091.0603,-12096.3557 4091.4739,-12091.0745 4088.0902,-12091.9693"/>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;errors -->
+<g id="edge19" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3825.1474,-10703.5215C3873.6984,-10680.7615 3949.6787,-10636.6064 3983,-10571.5998 4072.3666,-10397.254 4019.2394,-7240.303 4041,-7045.5998 4051.0278,-6955.8765 4077.3293,-6851.4966 4089.5664,-6805.9042"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3039,-6806.1823 4090.9169,-6800.899 4087.9248,-6805.2705 4091.3039,-6806.1823"/>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;reflect -->
+<g id="edge21" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M3825.2105,-10703.5537C3873.82,-10680.8236 3949.8545,-10636.6963 3983,-10571.5998 4049.2,-10441.5855 4026.989,-5464.8233 4041,-5319.5998 4051.0783,-5215.1387 4078.5171,-5092.6113 4090.395,-5042.6026"/>
+<polygon fill="#000000" stroke="#000000" points="4092.104,-5042.9801 4091.5624,-5037.7104 4088.6995,-5042.1676 4092.104,-5042.9801"/>
+</g>
+<!-- math/bits -->
+<g id="node40" class="node">
+<title>math/bits</title>
+<g id="a_node40"><a xlink:href="https://godoc.org/math/bits" xlink:title="math/bits" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4118,-10679.5998C4118,-10679.5998 4074,-10679.5998 4074,-10679.5998 4068,-10679.5998 4062,-10673.5998 4062,-10667.5998 4062,-10667.5998 4062,-10655.5998 4062,-10655.5998 4062,-10649.5998 4068,-10643.5998 4074,-10643.5998 4074,-10643.5998 4118,-10643.5998 4118,-10643.5998 4124,-10643.5998 4130,-10649.5998 4130,-10655.5998 4130,-10655.5998 4130,-10667.5998 4130,-10667.5998 4130,-10673.5998 4124,-10679.5998 4118,-10679.5998"/>
+<text text-anchor="middle" x="4096" y="-10657.8998" font-family="Times,serif" font-size="14.00" fill="#000000">math/bits</text>
+</a>
+</g>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;math/bits -->
+<g id="edge20" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;math/bits</title>
+<path fill="none" stroke="#000000" d="M3872.8889,-10704.1647C3933.8351,-10692.5375 4010.8917,-10677.8367 4056.6557,-10669.1059"/>
+<polygon fill="#000000" stroke="#000000" points="4057.2213,-10670.7796 4061.8047,-10668.1236 4056.5653,-10667.3416 4057.2213,-10670.7796"/>
+</g>
+<!-- unsafe -->
+<g id="node41" class="node">
+<title>unsafe</title>
+<g id="a_node41"><a xlink:href="https://godoc.org/unsafe" xlink:title="unsafe" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-14029.5998C4111,-14029.5998 4081,-14029.5998 4081,-14029.5998 4075,-14029.5998 4069,-14023.5998 4069,-14017.5998 4069,-14017.5998 4069,-14005.5998 4069,-14005.5998 4069,-13999.5998 4075,-13993.5998 4081,-13993.5998 4081,-13993.5998 4111,-13993.5998 4111,-13993.5998 4117,-13993.5998 4123,-13999.5998 4123,-14005.5998 4123,-14005.5998 4123,-14017.5998 4123,-14017.5998 4123,-14023.5998 4117,-14029.5998 4111,-14029.5998"/>
+<text text-anchor="middle" x="4096" y="-14007.8998" font-family="Times,serif" font-size="14.00" fill="#000000">unsafe</text>
+</a>
+</g>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;unsafe -->
+<g id="edge22" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;unsafe</title>
+<path fill="none" stroke="#000000" d="M3801.7305,-10739.6385C3844.9349,-10779.6645 3945.3409,-10881.0668 3983,-10990.5998 4037.0006,-11147.6628 3956.8045,-13836.4358 4041,-13979.5998 4046.294,-13988.6015 4055.1777,-13995.3186 4064.2783,-14000.2109"/>
+<polygon fill="#000000" stroke="#000000" points="4063.4963,-14001.7765 4068.7484,-14002.468 4065.0739,-13998.6521 4063.4963,-14001.7765"/>
+</g>
+<!-- github.com/containers/image/docker -->
+<g id="node42" class="node">
+<title>github.com/containers/image/docker</title>
+<g id="a_node42"><a xlink:href="https://godoc.org/github.com/containers/image/docker" xlink:title="github.com/containers/image/docker" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M204,-9015.5998C204,-9015.5998 12,-9015.5998 12,-9015.5998 6,-9015.5998 0,-9009.5998 0,-9003.5998 0,-9003.5998 0,-8991.5998 0,-8991.5998 0,-8985.5998 6,-8979.5998 12,-8979.5998 12,-8979.5998 204,-8979.5998 204,-8979.5998 210,-8979.5998 216,-8985.5998 216,-8991.5998 216,-8991.5998 216,-9003.5998 216,-9003.5998 216,-9009.5998 210,-9015.5998 204,-9015.5998"/>
+<text text-anchor="middle" x="108" y="-8993.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/docker</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;bytes -->
+<g id="edge23" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M108.2994,-9015.6083C112.2985,-9251.1292 156.8864,-11729.3631 274,-12451.5998 405.9589,-13265.3875 -49.4171,-14237.5998 775,-14237.5998 775,-14237.5998 775,-14237.5998 1848.5,-14237.5998 2295.8667,-14237.5998 2352.5907,-14491.8866 2785,-14606.5998 3102.5829,-14690.8509 3193.4348,-14666.0252 3522,-14664.5998 3726.8947,-14663.7109 3833.6448,-14800.8697 3983,-14660.5998 4048.8524,-14598.7532 4021.5521,-13937.823 4041,-13849.5998 4050.8621,-13804.8615 4071.9125,-13755.5363 4084.9211,-13727.4868"/>
+<polygon fill="#000000" stroke="#000000" points="4086.5625,-13728.1079 4087.0943,-13722.8373 4083.3917,-13726.6259 4086.5625,-13728.1079"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;context -->
+<g id="edge24" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M108.1721,-8979.5338C112.2011,-8558.9319 186.4301,-933.1535 274,-727.5998 397.4197,-437.8957 460.1018,-226.5998 775,-226.5998 775,-226.5998 775,-226.5998 3354.5,-226.5998 3661.9493,-226.5998 3784.5568,-280.7693 3983,-515.5998 4035.3575,-577.5578 4079.5844,-831.7349 4092.3149,-910.9282"/>
+<polygon fill="#000000" stroke="#000000" points="4090.6597,-911.6613 4093.1767,-916.3226 4094.1159,-911.1091 4090.6597,-911.6613"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;crypto/rand -->
+<g id="edge25" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;crypto/rand</title>
+<path fill="none" stroke="#000000" d="M108.1297,-9015.806C111.5759,-9496.8615 183.2608,-19352.2153 274,-19625.5998 378.9915,-19941.9244 441.7067,-20183.5998 775,-20183.5998 775,-20183.5998 775,-20183.5998 3354.5,-20183.5998 3641.0713,-20183.5998 3796.5144,-20257.1917 3983,-20039.5998 4100.2903,-19902.7453 3966.8511,-19394.8804 4041,-19230.5998 4046.9915,-19217.3254 4057.5663,-19205.3743 4067.8532,-19195.9553"/>
+<polygon fill="#000000" stroke="#000000" points="4069.0495,-19197.2334 4071.6211,-19192.6019 4066.7226,-19194.6188 4069.0495,-19197.2334"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;crypto/tls -->
+<g id="edge26" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;crypto/tls</title>
+<path fill="none" stroke="#000000" d="M108.0767,-9015.9112C110.1478,-9504.7959 154.4374,-19621.5528 274,-19891.5998 397.4799,-20170.4945 469.9926,-20360.5998 775,-20360.5998 775,-20360.5998 775,-20360.5998 3354.5,-20360.5998 3705.1724,-20360.5998 3810.3455,-20188.8236 3983,-19883.5998 4100.7565,-19675.4264 3961.581,-19574.1999 4041,-19348.5998 4049.3006,-19325.0208 4065.1505,-19301.3178 4077.7111,-19284.7852"/>
+<polygon fill="#000000" stroke="#000000" points="4079.1616,-19285.7698 4080.827,-19280.741 4076.3891,-19283.6337 4079.1616,-19285.7698"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;encoding/json -->
+<g id="edge27" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M108.0966,-9015.6621C110.3777,-9436.2375 153.3824,-17064.8233 274,-17508.5998 393.3606,-17947.7516 319.9162,-18401.5998 775,-18401.5998 775,-18401.5998 775,-18401.5998 2576.5,-18401.5998 2581.5534,-18401.5998 3979.6433,-18031.3773 3983,-18027.5998 4009.3524,-17997.9441 4079.0234,-17388.802 4093.3874,-17260.9947"/>
+<polygon fill="#000000" stroke="#000000" points="4095.1338,-17261.1243 4093.9526,-17255.9602 4091.6556,-17260.7338 4095.1338,-17261.1243"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;errors -->
+<g id="edge28" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M108.1072,-8979.2185C109.9132,-8681.6996 135.7309,-4869.8585 274,-4685.5998 414.8757,-4497.8675 540.2887,-4518.5998 775,-4518.5998 775,-4518.5998 775,-4518.5998 1848.5,-4518.5998 2611.1132,-4518.5998 2961.7325,-4380.2225 3522,-4897.5998 3585.6168,-4956.3465 3513.4112,-5028.2446 3580,-5083.5998 3718.7083,-5198.9077 3858.6961,-5000.8923 3983,-5131.5998 4041.8152,-5193.445 4088.8126,-6563.2577 4095.248,-6759.3198"/>
+<polygon fill="#000000" stroke="#000000" points="4093.5054,-6759.5768 4095.4181,-6764.5169 4097.0035,-6759.4623 4093.5054,-6759.5768"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;fmt -->
+<g id="edge29" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M114.1774,-8979.4412C131.1964,-8932.4778 183.5937,-8806.7662 274,-8750.5998 396.6406,-8674.4073 452.2222,-8720.7897 596,-8707.5998 1744.117,-8602.2738 2034.3194,-8604.9615 3187,-8580.5998 3363.8583,-8576.8619 3825.7673,-8507.5398 3983,-8588.5998 4036.8519,-8616.3627 4071.5562,-8682.921 4086.8974,-8718.404"/>
+<polygon fill="#000000" stroke="#000000" points="4085.4558,-8719.4867 4089.0203,-8723.4054 4088.6775,-8718.1191 4085.4558,-8719.4867"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;io -->
+<g id="edge49" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M108.2346,-9015.7509C113.0578,-9387.1364 191.5189,-15341.1846 274,-16101.5998 362.1176,-16913.9797 -42.1449,-17870.5998 775,-17870.5998 775,-17870.5998 775,-17870.5998 1848.5,-17870.5998 2129.1558,-17870.5998 2165.7332,-17706.1655 2368,-17511.5998 2590.6142,-17297.4614 2592.7057,-17197.3336 2785,-16955.5998 3299.2459,-16309.1397 3685.7345,-16305.3091 3983,-15534.5998 4047.8169,-15366.5513 3949.4672,-14069.7232 4041,-13914.5998 4046.3495,-13905.5339 4055.2478,-13898.6637 4064.3445,-13893.6038"/>
+<polygon fill="#000000" stroke="#000000" points="4065.1946,-13895.1341 4068.8115,-13891.2635 4063.5703,-13892.0338 4065.1946,-13895.1341"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;io/ioutil -->
+<g id="edge50" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M108.0157,-9015.8874C108.4032,-9412.6285 117.276,-16140.2335 274,-16987.5998 368.769,-17499.9912 253.9183,-18047.5998 775,-18047.5998 775,-18047.5998 775,-18047.5998 2195.5,-18047.5998 2250.2529,-18047.5998 3531.182,-16669.3926 3580,-16644.5998 3741.4568,-16562.6023 3844.9948,-16701.8451 3983,-16584.5998 4047.3774,-16529.9066 4008.0824,-16481.3959 4041,-16403.5998 4053.1176,-16374.9616 4070.4612,-16343.6614 4082.417,-16323.1721"/>
+<polygon fill="#000000" stroke="#000000" points="4084.0114,-16323.9127 4085.0326,-16318.7146 4080.9927,-16322.1413 4084.0114,-16323.9127"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;strconv -->
+<g id="edge57" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M108.1139,-8979.2729C110.0062,-8686.3457 136.6448,-4974.0993 274,-4515.5998 353.2054,-4251.2075 452.0744,-4220.104 596,-3984.5998 1007.0122,-3312.0646 737.8157,-2474.5998 1526,-2474.5998 1526,-2474.5998 1526,-2474.5998 1848.5,-2474.5998 2909.975,-2474.5998 2305.4453,-3861.338 3187,-4452.5998 3335.1846,-4551.9878 3404.4668,-4502.5881 3580,-4534.5998 3669.1725,-4550.8621 3919.8183,-4532.6054 3983,-4597.5998 4054.0484,-4670.6867 4025.3295,-4951.8823 4041,-5052.5998 4057.0498,-5155.7551 4081.131,-5277.9985 4091.2521,-5328.2549"/>
+<polygon fill="#000000" stroke="#000000" points="4089.5974,-5328.9023 4092.3018,-5333.4574 4093.0282,-5328.21 4089.5974,-5328.9023"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;strings -->
+<g id="edge58" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M108.2736,-8979.2299C112.4407,-8703.7582 164.3605,-5420.2252 274,-5253.5998 663.9815,-4660.9225 1139.027,-4990.5998 1848.5,-4990.5998 1848.5,-4990.5998 1848.5,-4990.5998 2195.5,-4990.5998 2452.8914,-4990.5998 2523.8118,-5062.597 2727,-5220.5998 2859.0727,-5323.3019 3058.8379,-5709.0573 3187,-5816.5998 3224.7672,-5848.2908 3534.4124,-6003.8262 3580,-6022.5998 3753.2799,-6093.9588 3865.2545,-6000.8122 3983,-6146.5998 4076.7603,-6262.6899 4019.1873,-6667.9783 4041,-6815.5998 4054.1488,-6904.5867 4078.8522,-7008.7721 4090.1238,-7054.3135"/>
+<polygon fill="#000000" stroke="#000000" points="4088.4619,-7054.8827 4091.3657,-7059.3133 4091.8587,-7054.0389 4088.4619,-7054.8827"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;sync -->
+<g id="edge59" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M108.1876,-8979.303C112.3165,-8579.1193 184.1005,-1746.6952 274,-1567.5998 400.7129,-1315.1657 492.548,-1176.5998 775,-1176.5998 775,-1176.5998 775,-1176.5998 2957,-1176.5998 3229.6735,-1176.5998 3854.6897,-1609.002 3983,-1849.5998 4080.0213,-2031.5269 4020.9347,-2570.3975 4041,-2775.5998 4055.5841,-2924.7478 4082.5844,-3102.9788 4092.3528,-3165.5379"/>
+<polygon fill="#000000" stroke="#000000" points="4090.633,-3165.8673 4093.1352,-3170.5365 4094.0909,-3165.3261 4090.633,-3165.8673"/>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;time -->
+<g id="edge60" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M108.3298,-9015.8248C111.9426,-9207.6293 146.5066,-10858.201 274,-11330.5998 405.7052,-11818.6044 269.5351,-12351.5998 775,-12351.5998 775,-12351.5998 775,-12351.5998 1848.5,-12351.5998 2492.161,-12351.5998 2590.9467,-12041.5401 3187,-11798.5998 3230.5825,-11780.8364 3534.7788,-11653.6391 3580,-11640.5998 3754.9003,-11590.1684 3832.4785,-11669.9556 3983,-11567.5998 4040.8369,-11528.2703 4074.6786,-11448.5105 4088.5681,-11408.9437"/>
+<polygon fill="#000000" stroke="#000000" points="4090.3438,-11409.162 4090.3186,-11403.8646 4087.0348,-11408.0215 4090.3438,-11409.162"/>
+</g>
+<!-- github.com/containers/image/v5/docker/policyconfiguration -->
+<g id="node43" class="node">
+<title>github.com/containers/image/v5/docker/policyconfiguration</title>
+<g id="a_node43"><a xlink:href="https://godoc.org/github.com/containers/image/v5/docker/policyconfiguration" xlink:title="github.com/containers/image/v5/docker/policyconfiguration" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3117,-7806.5998C3117,-7806.5998 2797,-7806.5998 2797,-7806.5998 2791,-7806.5998 2785,-7800.5998 2785,-7794.5998 2785,-7794.5998 2785,-7782.5998 2785,-7782.5998 2785,-7776.5998 2791,-7770.5998 2797,-7770.5998 2797,-7770.5998 3117,-7770.5998 3117,-7770.5998 3123,-7770.5998 3129,-7776.5998 3129,-7782.5998 3129,-7782.5998 3129,-7794.5998 3129,-7794.5998 3129,-7800.5998 3123,-7806.5998 3117,-7806.5998"/>
+<text text-anchor="middle" x="2957" y="-7784.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/docker/policyconfiguration</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/docker/policyconfiguration -->
+<g id="edge30" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/docker/policyconfiguration</title>
+<path fill="none" stroke="#000000" d="M110.078,-8979.5352C118.2169,-8917.3259 153.6629,-8710.0201 274,-8604.5998 759.3146,-8179.4442 1059.3606,-8416.8293 1674,-8220.5998 2174.8116,-8060.711 2275.653,-7944.7624 2785,-7814.5998 2794.8067,-7812.0937 2804.9937,-7809.7982 2815.2933,-7807.7011"/>
+<polygon fill="#000000" stroke="#000000" points="2815.7982,-7809.3848 2820.3574,-7806.6874 2815.1112,-7805.9529 2815.7982,-7809.3848"/>
+</g>
+<!-- github.com/containers/image/v5/docker/reference -->
+<g id="node44" class="node">
+<title>github.com/containers/image/v5/docker/reference</title>
+<g id="a_node44"><a xlink:href="https://godoc.org/github.com/containers/image/v5/docker/reference" xlink:title="github.com/containers/image/v5/docker/reference" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3486,-6679.5998C3486,-6679.5998 3223,-6679.5998 3223,-6679.5998 3217,-6679.5998 3211,-6673.5998 3211,-6667.5998 3211,-6667.5998 3211,-6655.5998 3211,-6655.5998 3211,-6649.5998 3217,-6643.5998 3223,-6643.5998 3223,-6643.5998 3486,-6643.5998 3486,-6643.5998 3492,-6643.5998 3498,-6649.5998 3498,-6655.5998 3498,-6655.5998 3498,-6667.5998 3498,-6667.5998 3498,-6673.5998 3492,-6679.5998 3486,-6679.5998"/>
+<text text-anchor="middle" x="3354.5" y="-6657.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/docker/reference</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/docker/reference -->
+<g id="edge31" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/docker/reference</title>
+<path fill="none" stroke="#000000" d="M108.3552,-8979.4036C114.2215,-8681.0041 191.8455,-4811.6189 274,-4732.5998 358.5652,-4651.262 421.5999,-4717.8303 538,-4732.5998 954.635,-4785.4648 1981.3579,-5040.6263 2368,-5204.5998 2540.2796,-5277.6629 2630.619,-5264.1967 2727,-5424.5998 2835.7051,-5605.5135 2641.654,-6217.6853 2785,-6372.5998 2889.8435,-6485.9046 3008.0242,-6324.7093 3129,-6420.5998 3185.0145,-6464.9993 3139.8133,-6516.9123 3187,-6570.5998 3215.959,-6603.5485 3259.834,-6626.7733 3295.4155,-6641.5106"/>
+<polygon fill="#000000" stroke="#000000" points="3294.8652,-6643.1761 3300.1559,-6643.4444 3296.1873,-6639.9353 3294.8652,-6643.1761"/>
+</g>
+<!-- github.com/containers/image/v5/image -->
+<g id="node45" class="node">
+<title>github.com/containers/image/v5/image</title>
+<g id="a_node45"><a xlink:href="https://godoc.org/github.com/containers/image/v5/image" xlink:title="github.com/containers/image/v5/image" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1951.5,-10920.5998C1951.5,-10920.5998 1745.5,-10920.5998 1745.5,-10920.5998 1739.5,-10920.5998 1733.5,-10914.5998 1733.5,-10908.5998 1733.5,-10908.5998 1733.5,-10896.5998 1733.5,-10896.5998 1733.5,-10890.5998 1739.5,-10884.5998 1745.5,-10884.5998 1745.5,-10884.5998 1951.5,-10884.5998 1951.5,-10884.5998 1957.5,-10884.5998 1963.5,-10890.5998 1963.5,-10896.5998 1963.5,-10896.5998 1963.5,-10908.5998 1963.5,-10908.5998 1963.5,-10914.5998 1957.5,-10920.5998 1951.5,-10920.5998"/>
+<text text-anchor="middle" x="1848.5" y="-10898.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/image</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/image -->
+<g id="edge32" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/image</title>
+<path fill="none" stroke="#000000" d="M108.3103,-9015.6884C111.6199,-9199.0476 142.9189,-10717.6288 274,-10862.5998 426.0548,-11030.767 548.2824,-10958.5998 775,-10958.5998 775,-10958.5998 775,-10958.5998 1166,-10958.5998 1363.4893,-10958.5998 1592.0496,-10935.0211 1728.2055,-10918.4633"/>
+<polygon fill="#000000" stroke="#000000" points="1728.6604,-10920.1708 1733.4117,-10917.8281 1728.2365,-10916.6966 1728.6604,-10920.1708"/>
+</g>
+<!-- github.com/containers/image/v5/manifest -->
+<g id="node46" class="node">
+<title>github.com/containers/image/v5/manifest</title>
+<g id="a_node46"><a xlink:href="https://godoc.org/github.com/containers/image/v5/manifest" xlink:title="github.com/containers/image/v5/manifest" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2305.5,-11204.5998C2305.5,-11204.5998 2085.5,-11204.5998 2085.5,-11204.5998 2079.5,-11204.5998 2073.5,-11198.5998 2073.5,-11192.5998 2073.5,-11192.5998 2073.5,-11180.5998 2073.5,-11180.5998 2073.5,-11174.5998 2079.5,-11168.5998 2085.5,-11168.5998 2085.5,-11168.5998 2305.5,-11168.5998 2305.5,-11168.5998 2311.5,-11168.5998 2317.5,-11174.5998 2317.5,-11180.5998 2317.5,-11180.5998 2317.5,-11192.5998 2317.5,-11192.5998 2317.5,-11198.5998 2311.5,-11204.5998 2305.5,-11204.5998"/>
+<text text-anchor="middle" x="2195.5" y="-11182.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/manifest</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/manifest -->
+<g id="edge33" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/manifest</title>
+<path fill="none" stroke="#000000" d="M108.7042,-9015.8208C116.0162,-9202.1923 179.7214,-10759.1907 274,-10939.5998 408.0461,-11196.1071 485.5793,-11355.5998 775,-11355.5998 775,-11355.5998 775,-11355.5998 1526,-11355.5998 1766.97,-11355.5998 2042.2948,-11251.4042 2149.4552,-11206.6422"/>
+<polygon fill="#000000" stroke="#000000" points="2150.1876,-11208.2328 2154.1223,-11204.6858 2148.8345,-11205.0049 2150.1876,-11208.2328"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/blobinfocache/none -->
+<g id="node47" class="node">
+<title>github.com/containers/image/v5/pkg/blobinfocache/none</title>
+<g id="a_node47"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/blobinfocache/none" xlink:title="github.com/containers/image/v5/pkg/blobinfocache/none" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2347,-10680.5998C2347,-10680.5998 2044,-10680.5998 2044,-10680.5998 2038,-10680.5998 2032,-10674.5998 2032,-10668.5998 2032,-10668.5998 2032,-10656.5998 2032,-10656.5998 2032,-10650.5998 2038,-10644.5998 2044,-10644.5998 2044,-10644.5998 2347,-10644.5998 2347,-10644.5998 2353,-10644.5998 2359,-10650.5998 2359,-10656.5998 2359,-10656.5998 2359,-10668.5998 2359,-10668.5998 2359,-10674.5998 2353,-10680.5998 2347,-10680.5998"/>
+<text text-anchor="middle" x="2195.5" y="-10658.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/blobinfocache/none</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/pkg/blobinfocache/none -->
+<g id="edge34" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/pkg/blobinfocache/none</title>
+<path fill="none" stroke="#000000" d="M108.8666,-9016.0789C113.2303,-9089.8803 138.3683,-9367.5627 274,-9534.5998 434.5189,-9732.2867 520.3504,-9812.5998 775,-9812.5998 775,-9812.5998 775,-9812.5998 1526,-9812.5998 1959.5196,-9812.5998 2155.5055,-10503.3876 2189.9104,-10639.481"/>
+<polygon fill="#000000" stroke="#000000" points="2188.2436,-10640.0292 2191.1576,-10644.4531 2191.6385,-10639.1776 2188.2436,-10640.0292"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config -->
+<g id="node48" class="node">
+<title>github.com/containers/image/v5/pkg/docker/config</title>
+<g id="a_node48"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/docker/config" xlink:title="github.com/containers/image/v5/pkg/docker/config" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1662,-15988.5998C1662,-15988.5998 1390,-15988.5998 1390,-15988.5998 1384,-15988.5998 1378,-15982.5998 1378,-15976.5998 1378,-15976.5998 1378,-15964.5998 1378,-15964.5998 1378,-15958.5998 1384,-15952.5998 1390,-15952.5998 1390,-15952.5998 1662,-15952.5998 1662,-15952.5998 1668,-15952.5998 1674,-15958.5998 1674,-15964.5998 1674,-15964.5998 1674,-15976.5998 1674,-15976.5998 1674,-15982.5998 1668,-15988.5998 1662,-15988.5998"/>
+<text text-anchor="middle" x="1526" y="-15966.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/docker/config</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/pkg/docker/config -->
+<g id="edge35" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/pkg/docker/config</title>
+<path fill="none" stroke="#000000" d="M108.2604,-9015.6641C113.0066,-9342.3197 182.1231,-13992.7789 274,-14259.5998 551.6618,-15065.9614 1352.279,-15814.2505 1501.7963,-15949.0664"/>
+<polygon fill="#000000" stroke="#000000" points="1500.7322,-15950.4631 1505.6192,-15952.5075 1503.0738,-15947.8617 1500.7322,-15950.4631"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2 -->
+<g id="node49" class="node">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2</title>
+<g id="a_node49"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/sysregistriesv2" xlink:title="github.com/containers/image/v5/pkg/sysregistriesv2" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2715,-8750.5998C2715,-8750.5998 2438,-8750.5998 2438,-8750.5998 2432,-8750.5998 2426,-8744.5998 2426,-8738.5998 2426,-8738.5998 2426,-8726.5998 2426,-8726.5998 2426,-8720.5998 2432,-8714.5998 2438,-8714.5998 2438,-8714.5998 2715,-8714.5998 2715,-8714.5998 2721,-8714.5998 2727,-8720.5998 2727,-8726.5998 2727,-8726.5998 2727,-8738.5998 2727,-8738.5998 2727,-8744.5998 2721,-8750.5998 2715,-8750.5998"/>
+<text text-anchor="middle" x="2576.5" y="-8728.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/sysregistriesv2</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/pkg/sysregistriesv2 -->
+<g id="edge36" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/pkg/sysregistriesv2</title>
+<path fill="none" stroke="#000000" d="M118.676,-8979.4546C141.6117,-8942.4559 199.5985,-8858.9825 274,-8825.5998 658.1473,-8653.2394 1967.1679,-8701.4421 2420.4742,-8724.0295"/>
+<polygon fill="#000000" stroke="#000000" points="2420.6864,-8725.7922 2425.7676,-8724.294 2420.8612,-8722.2965 2420.6864,-8725.7922"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig -->
+<g id="node50" class="node">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig</title>
+<g id="a_node50"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/tlsclientconfig" xlink:title="github.com/containers/image/v5/pkg/tlsclientconfig" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2713.5,-16696.5998C2713.5,-16696.5998 2439.5,-16696.5998 2439.5,-16696.5998 2433.5,-16696.5998 2427.5,-16690.5998 2427.5,-16684.5998 2427.5,-16684.5998 2427.5,-16672.5998 2427.5,-16672.5998 2427.5,-16666.5998 2433.5,-16660.5998 2439.5,-16660.5998 2439.5,-16660.5998 2713.5,-16660.5998 2713.5,-16660.5998 2719.5,-16660.5998 2725.5,-16666.5998 2725.5,-16672.5998 2725.5,-16672.5998 2725.5,-16684.5998 2725.5,-16684.5998 2725.5,-16690.5998 2719.5,-16696.5998 2713.5,-16696.5998"/>
+<text text-anchor="middle" x="2576.5" y="-16674.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/tlsclientconfig</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/pkg/tlsclientconfig -->
+<g id="edge37" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/pkg/tlsclientconfig</title>
+<path fill="none" stroke="#000000" d="M108.2282,-9015.9753C112.0749,-9321.9297 164.6577,-13361.0434 274,-14541.5998 340.6035,-15260.7108 52.8112,-17752.5998 775,-17752.5998 775,-17752.5998 775,-17752.5998 1848.5,-17752.5998 2167.4195,-17752.5998 2219.4798,-17539.8256 2368,-17257.5998 2443.6769,-17113.7947 2365.458,-17047.4029 2426,-16896.5998 2457.9064,-16817.1245 2523.0268,-16737.9235 2556.1593,-16700.6491"/>
+<polygon fill="#000000" stroke="#000000" points="2557.6229,-16701.6377 2559.6505,-16696.7436 2555.0135,-16699.305 2557.6229,-16701.6377"/>
+</g>
+<!-- github.com/containers/image/v5/transports -->
+<g id="node51" class="node">
+<title>github.com/containers/image/v5/transports</title>
+<g id="a_node51"><a xlink:href="https://godoc.org/github.com/containers/image/v5/transports" xlink:title="github.com/containers/image/v5/transports" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2689.5,-2551.5998C2689.5,-2551.5998 2463.5,-2551.5998 2463.5,-2551.5998 2457.5,-2551.5998 2451.5,-2545.5998 2451.5,-2539.5998 2451.5,-2539.5998 2451.5,-2527.5998 2451.5,-2527.5998 2451.5,-2521.5998 2457.5,-2515.5998 2463.5,-2515.5998 2463.5,-2515.5998 2689.5,-2515.5998 2689.5,-2515.5998 2695.5,-2515.5998 2701.5,-2521.5998 2701.5,-2527.5998 2701.5,-2527.5998 2701.5,-2539.5998 2701.5,-2539.5998 2701.5,-2545.5998 2695.5,-2551.5998 2689.5,-2551.5998"/>
+<text text-anchor="middle" x="2576.5" y="-2529.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/transports</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/transports -->
+<g id="edge38" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/transports</title>
+<path fill="none" stroke="#000000" d="M108.1152,-8979.3183C110.5292,-8602.4073 151.3587,-2522.3495 274,-2393.5998 427.9962,-2231.9334 551.7269,-2356.5998 775,-2356.5998 775,-2356.5998 775,-2356.5998 1848.5,-2356.5998 2083.298,-2356.5998 2142.1292,-2388.4711 2368,-2452.5998 2425.9129,-2469.0423 2490.145,-2495.2744 2531.8055,-2513.4099"/>
+<polygon fill="#000000" stroke="#000000" points="2531.2375,-2515.0715 2536.52,-2515.4696 2532.6388,-2511.8642 2531.2375,-2515.0715"/>
+</g>
+<!-- github.com/containers/image/v5/types -->
+<g id="node52" class="node">
+<title>github.com/containers/image/v5/types</title>
+<g id="a_node52"><a xlink:href="https://godoc.org/github.com/containers/image/v5/types" xlink:title="github.com/containers/image/v5/types" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3057.5,-10920.5998C3057.5,-10920.5998 2856.5,-10920.5998 2856.5,-10920.5998 2850.5,-10920.5998 2844.5,-10914.5998 2844.5,-10908.5998 2844.5,-10908.5998 2844.5,-10896.5998 2844.5,-10896.5998 2844.5,-10890.5998 2850.5,-10884.5998 2856.5,-10884.5998 2856.5,-10884.5998 3057.5,-10884.5998 3057.5,-10884.5998 3063.5,-10884.5998 3069.5,-10890.5998 3069.5,-10896.5998 3069.5,-10896.5998 3069.5,-10908.5998 3069.5,-10908.5998 3069.5,-10914.5998 3063.5,-10920.5998 3057.5,-10920.5998"/>
+<text text-anchor="middle" x="2957" y="-10898.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/types</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/types -->
+<g id="edge39" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/containers/image/v5/types</title>
+<path fill="none" stroke="#000000" d="M110.3783,-9015.8406C135.8235,-9207.2489 364.9686,-10840.5998 775,-10840.5998 775,-10840.5998 775,-10840.5998 2195.5,-10840.5998 2423.9207,-10840.5998 2689.6151,-10868.6553 2839.0738,-10887.0212"/>
+<polygon fill="#000000" stroke="#000000" points="2839.1647,-10888.7956 2844.3412,-10887.6704 2839.5929,-10885.3218 2839.1647,-10888.7956"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode -->
+<g id="node53" class="node">
+<title>github.com/docker/distribution/registry/api/errcode</title>
+<g id="a_node53"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/errcode" xlink:title="github.com/docker/distribution/registry/api/errcode" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3917,-4519.5998C3917,-4519.5998 3646,-4519.5998 3646,-4519.5998 3640,-4519.5998 3634,-4513.5998 3634,-4507.5998 3634,-4507.5998 3634,-4495.5998 3634,-4495.5998 3634,-4489.5998 3640,-4483.5998 3646,-4483.5998 3646,-4483.5998 3917,-4483.5998 3917,-4483.5998 3923,-4483.5998 3929,-4489.5998 3929,-4495.5998 3929,-4495.5998 3929,-4507.5998 3929,-4507.5998 3929,-4513.5998 3923,-4519.5998 3917,-4519.5998"/>
+<text text-anchor="middle" x="3781.5" y="-4497.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/errcode</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/docker/distribution/registry/api/errcode -->
+<g id="edge40" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/docker/distribution/registry/api/errcode</title>
+<path fill="none" stroke="#000000" d="M108.1213,-8979.4687C110.7423,-8592.9779 156.1523,-2155.5427 274,-2008.5998 417.5137,-1829.6543 545.6146,-1884.5998 775,-1884.5998 775,-1884.5998 775,-1884.5998 2195.5,-1884.5998 2680.6938,-1884.5998 2881.715,-2033.1515 3129,-2450.5998 3203.1437,-2575.7638 3089.2949,-3650.8176 3187,-3758.5998 3288.0185,-3870.0371 3418.5542,-3697.412 3522,-3806.5998 3622.4991,-3912.6775 3482.3821,-4352.8648 3580,-4461.5998 3593.3435,-4476.4629 3610.4922,-4486.7372 3629.1426,-4493.7192"/>
+<polygon fill="#000000" stroke="#000000" points="3628.6179,-4495.39 3633.9152,-4495.4242 3629.7954,-4492.094 3628.6179,-4495.39"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2 -->
+<g id="node54" class="node">
+<title>github.com/docker/distribution/registry/api/v2</title>
+<g id="a_node54"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/v2" xlink:title="github.com/docker/distribution/registry/api/v2" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2698.5,-2256.5998C2698.5,-2256.5998 2454.5,-2256.5998 2454.5,-2256.5998 2448.5,-2256.5998 2442.5,-2250.5998 2442.5,-2244.5998 2442.5,-2244.5998 2442.5,-2232.5998 2442.5,-2232.5998 2442.5,-2226.5998 2448.5,-2220.5998 2454.5,-2220.5998 2454.5,-2220.5998 2698.5,-2220.5998 2698.5,-2220.5998 2704.5,-2220.5998 2710.5,-2226.5998 2710.5,-2232.5998 2710.5,-2232.5998 2710.5,-2244.5998 2710.5,-2244.5998 2710.5,-2250.5998 2704.5,-2256.5998 2698.5,-2256.5998"/>
+<text text-anchor="middle" x="2576.5" y="-2234.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/v2</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/docker/distribution/registry/api/v2 -->
+<g id="edge41" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/docker/distribution/registry/api/v2</title>
+<path fill="none" stroke="#000000" d="M108.0949,-8979.3845C110.1401,-8594.3635 145.8592,-2233.2677 274,-2098.5998 427.9094,-1936.8507 551.7269,-2061.5998 775,-2061.5998 775,-2061.5998 775,-2061.5998 1848.5,-2061.5998 2111.8205,-2061.5998 2413.9364,-2172.2883 2528.8194,-2218.6012"/>
+<polygon fill="#000000" stroke="#000000" points="2528.1968,-2220.2371 2533.4882,-2220.4895 2529.5092,-2216.9924 2528.1968,-2220.2371"/>
+</g>
+<!-- github.com/docker/distribution/registry/client -->
+<g id="node55" class="node">
+<title>github.com/docker/distribution/registry/client</title>
+<g id="a_node55"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client" xlink:title="github.com/docker/distribution/registry/client" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M526,-5303.5998C526,-5303.5998 286,-5303.5998 286,-5303.5998 280,-5303.5998 274,-5297.5998 274,-5291.5998 274,-5291.5998 274,-5279.5998 274,-5279.5998 274,-5273.5998 280,-5267.5998 286,-5267.5998 286,-5267.5998 526,-5267.5998 526,-5267.5998 532,-5267.5998 538,-5273.5998 538,-5279.5998 538,-5279.5998 538,-5291.5998 538,-5291.5998 538,-5297.5998 532,-5303.5998 526,-5303.5998"/>
+<text text-anchor="middle" x="406" y="-5281.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/docker/distribution/registry/client -->
+<g id="edge42" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/docker/distribution/registry/client</title>
+<path fill="none" stroke="#000000" d="M109.4628,-8979.3782C130.8519,-8712.9475 379.9683,-5609.8599 404.1202,-5309.0152"/>
+<polygon fill="#000000" stroke="#000000" points="405.8679,-5309.1129 404.5237,-5303.9889 402.3791,-5308.8328 405.8679,-5309.1129"/>
+</g>
+<!-- github.com/docker/go&#45;connections/tlsconfig -->
+<g id="node56" class="node">
+<title>github.com/docker/go&#45;connections/tlsconfig</title>
+<g id="a_node56"><a xlink:href="https://godoc.org/github.com/docker/go-connections/tlsconfig" xlink:title="github.com/docker/go&#45;connections/tlsconfig" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3471,-18573.5998C3471,-18573.5998 3238,-18573.5998 3238,-18573.5998 3232,-18573.5998 3226,-18567.5998 3226,-18561.5998 3226,-18561.5998 3226,-18549.5998 3226,-18549.5998 3226,-18543.5998 3232,-18537.5998 3238,-18537.5998 3238,-18537.5998 3471,-18537.5998 3471,-18537.5998 3477,-18537.5998 3483,-18543.5998 3483,-18549.5998 3483,-18549.5998 3483,-18561.5998 3483,-18561.5998 3483,-18567.5998 3477,-18573.5998 3471,-18573.5998"/>
+<text text-anchor="middle" x="3354.5" y="-18551.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go&#45;connections/tlsconfig</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/docker/go&#45;connections/tlsconfig -->
+<g id="edge43" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/docker/go&#45;connections/tlsconfig</title>
+<path fill="none" stroke="#000000" d="M108.168,-9015.8644C112.285,-9461.5843 191.4447,-17920.3886 274,-18423.5998 393.0573,-19149.3071 39.5915,-20000.5998 775,-20000.5998 775,-20000.5998 775,-20000.5998 2576.5,-20000.5998 3251.1579,-20000.5998 3343.3713,-18764.3283 3353.4117,-18578.789"/>
+<polygon fill="#000000" stroke="#000000" points="3355.1691,-18578.6938 3353.6853,-18573.6084 3351.674,-18578.5091 3355.1691,-18578.6938"/>
+</g>
+<!-- github.com/ghodss/yaml -->
+<g id="node57" class="node">
+<title>github.com/ghodss/yaml</title>
+<g id="a_node57"><a xlink:href="https://godoc.org/github.com/ghodss/yaml" xlink:title="github.com/ghodss/yaml" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3418.5,-3743.5998C3418.5,-3743.5998 3290.5,-3743.5998 3290.5,-3743.5998 3284.5,-3743.5998 3278.5,-3737.5998 3278.5,-3731.5998 3278.5,-3731.5998 3278.5,-3719.5998 3278.5,-3719.5998 3278.5,-3713.5998 3284.5,-3707.5998 3290.5,-3707.5998 3290.5,-3707.5998 3418.5,-3707.5998 3418.5,-3707.5998 3424.5,-3707.5998 3430.5,-3713.5998 3430.5,-3719.5998 3430.5,-3719.5998 3430.5,-3731.5998 3430.5,-3731.5998 3430.5,-3737.5998 3424.5,-3743.5998 3418.5,-3743.5998"/>
+<text text-anchor="middle" x="3354.5" y="-3721.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ghodss/yaml</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/ghodss/yaml -->
+<g id="edge44" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/ghodss/yaml</title>
+<path fill="none" stroke="#000000" d="M108.1856,-8979.4355C112.2039,-8588.9265 181.0419,-2030.9884 274,-1862.5998 403.0971,-1628.7473 507.88,-1530.5998 775,-1530.5998 775,-1530.5998 775,-1530.5998 2576.5,-1530.5998 2988.7282,-1530.5998 2969.9163,-1895.3046 3129,-2275.5998 3244.2526,-2551.1152 3337.5397,-3538.1731 3352.4261,-3702.3703"/>
+<polygon fill="#000000" stroke="#000000" points="3350.69,-3702.6041 3352.8832,-3707.4262 3354.1757,-3702.2889 3350.69,-3702.6041"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest -->
+<g id="node58" class="node">
+<title>github.com/opencontainers/go&#45;digest</title>
+<g id="a_node58"><a xlink:href="https://godoc.org/github.com/opencontainers/go-digest" xlink:title="github.com/opencontainers/go&#45;digest" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3878.5,-11040.5998C3878.5,-11040.5998 3684.5,-11040.5998 3684.5,-11040.5998 3678.5,-11040.5998 3672.5,-11034.5998 3672.5,-11028.5998 3672.5,-11028.5998 3672.5,-11016.5998 3672.5,-11016.5998 3672.5,-11010.5998 3678.5,-11004.5998 3684.5,-11004.5998 3684.5,-11004.5998 3878.5,-11004.5998 3878.5,-11004.5998 3884.5,-11004.5998 3890.5,-11010.5998 3890.5,-11016.5998 3890.5,-11016.5998 3890.5,-11028.5998 3890.5,-11028.5998 3890.5,-11034.5998 3884.5,-11040.5998 3878.5,-11040.5998"/>
+<text text-anchor="middle" x="3781.5" y="-11018.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/go&#45;digest</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge45" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M118.7683,-9015.627C174.7273,-9106.7783 442.5354,-9512.5998 775,-9512.5998 775,-9512.5998 775,-9512.5998 1526,-9512.5998 1775.8086,-9512.5998 1774.0857,-9702.4909 1965,-9863.5998 2337.9031,-10178.2857 2357.0559,-10359.1903 2785,-10593.5998 2924.3193,-10669.913 3009.1382,-10586.3563 3129,-10690.5998 3177.3521,-10732.6515 3137.1138,-10782.3799 3187,-10822.5998 3305.2732,-10917.9557 3396.1119,-10805.55 3522,-10890.5998 3560.8142,-10916.8226 3541.3799,-10952.092 3580,-10978.5998 3605.5353,-10996.1266 3636.8183,-11006.8327 3667.0499,-11013.3263"/>
+<polygon fill="#000000" stroke="#000000" points="3667.0337,-11015.1101 3672.2848,-11014.4115 3667.7442,-11011.6829 3667.0337,-11015.1101"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="node59" class="node">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<g id="a_node59"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" xlink:title="github.com/opencontainers/image&#45;spec/specs&#45;go/v1" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3492,-11187.5998C3492,-11187.5998 3217,-11187.5998 3217,-11187.5998 3211,-11187.5998 3205,-11181.5998 3205,-11175.5998 3205,-11175.5998 3205,-11163.5998 3205,-11163.5998 3205,-11157.5998 3211,-11151.5998 3217,-11151.5998 3217,-11151.5998 3492,-11151.5998 3492,-11151.5998 3498,-11151.5998 3504,-11157.5998 3504,-11163.5998 3504,-11163.5998 3504,-11175.5998 3504,-11175.5998 3504,-11181.5998 3498,-11187.5998 3492,-11187.5998"/>
+<text text-anchor="middle" x="3354.5" y="-11165.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image&#45;spec/specs&#45;go/v1</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge46" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M108.5234,-9015.8308C114.2885,-9212.6764 167.7791,-10941.1649 274,-11139.5998 407.5569,-11389.1022 492.0002,-11532.5998 775,-11532.5998 775,-11532.5998 775,-11532.5998 2195.5,-11532.5998 2657.5235,-11532.5998 3177.7401,-11266.654 3318.0365,-11190.0296"/>
+<polygon fill="#000000" stroke="#000000" points="3318.9267,-11191.5374 3322.4719,-11187.601 3317.2458,-11188.4675 3318.9267,-11191.5374"/>
+</g>
+<!-- github.com/pkg/errors -->
+<g id="node60" class="node">
+<title>github.com/pkg/errors</title>
+<g id="a_node60"><a xlink:href="https://godoc.org/github.com/pkg/errors" xlink:title="github.com/pkg/errors" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3838.5,-8821.5998C3838.5,-8821.5998 3724.5,-8821.5998 3724.5,-8821.5998 3718.5,-8821.5998 3712.5,-8815.5998 3712.5,-8809.5998 3712.5,-8809.5998 3712.5,-8797.5998 3712.5,-8797.5998 3712.5,-8791.5998 3718.5,-8785.5998 3724.5,-8785.5998 3724.5,-8785.5998 3838.5,-8785.5998 3838.5,-8785.5998 3844.5,-8785.5998 3850.5,-8791.5998 3850.5,-8797.5998 3850.5,-8797.5998 3850.5,-8809.5998 3850.5,-8809.5998 3850.5,-8815.5998 3844.5,-8821.5998 3838.5,-8821.5998"/>
+<text text-anchor="middle" x="3781.5" y="-8799.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/pkg/errors</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/pkg/errors -->
+<g id="edge47" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M154.9007,-8979.5063C262.673,-8939.6109 537.0251,-8847.5998 775,-8847.5998 775,-8847.5998 775,-8847.5998 1848.5,-8847.5998 2443.5808,-8847.5998 2592.0518,-8826.1597 3187,-8813.5998 3374.1812,-8809.6483 3594.5401,-8806.2765 3707.1106,-8804.6464"/>
+<polygon fill="#000000" stroke="#000000" points="3707.4149,-8806.3923 3712.389,-8804.5701 3707.3642,-8802.8926 3707.4149,-8806.3923"/>
+</g>
+<!-- github.com/sirupsen/logrus -->
+<g id="node61" class="node">
+<title>github.com/sirupsen/logrus</title>
+<g id="a_node61"><a xlink:href="https://godoc.org/github.com/sirupsen/logrus" xlink:title="github.com/sirupsen/logrus" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3425,-12323.5998C3425,-12323.5998 3284,-12323.5998 3284,-12323.5998 3278,-12323.5998 3272,-12317.5998 3272,-12311.5998 3272,-12311.5998 3272,-12299.5998 3272,-12299.5998 3272,-12293.5998 3278,-12287.5998 3284,-12287.5998 3284,-12287.5998 3425,-12287.5998 3425,-12287.5998 3431,-12287.5998 3437,-12293.5998 3437,-12299.5998 3437,-12299.5998 3437,-12311.5998 3437,-12311.5998 3437,-12317.5998 3431,-12323.5998 3425,-12323.5998"/>
+<text text-anchor="middle" x="3354.5" y="-12301.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/sirupsen/logrus</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge48" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M108.2514,-9015.8007C111.9207,-9275.8874 156.256,-12238.0736 274,-12373.5998 422.6919,-12544.7478 548.2824,-12469.5998 775,-12469.5998 775,-12469.5998 775,-12469.5998 2195.5,-12469.5998 2604.9138,-12469.5998 3087.7261,-12368.0317 3274.8553,-12324.7571"/>
+<polygon fill="#000000" stroke="#000000" points="3275.2872,-12326.4535 3279.7631,-12323.6198 3274.4971,-12323.0438 3275.2872,-12326.4535"/>
+</g>
+<!-- mime -->
+<g id="node62" class="node">
+<title>mime</title>
+<g id="a_node62"><a xlink:href="https://godoc.org/mime" xlink:title="mime" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3369.5,-5067.5998C3369.5,-5067.5998 3339.5,-5067.5998 3339.5,-5067.5998 3333.5,-5067.5998 3327.5,-5061.5998 3327.5,-5055.5998 3327.5,-5055.5998 3327.5,-5043.5998 3327.5,-5043.5998 3327.5,-5037.5998 3333.5,-5031.5998 3339.5,-5031.5998 3339.5,-5031.5998 3369.5,-5031.5998 3369.5,-5031.5998 3375.5,-5031.5998 3381.5,-5037.5998 3381.5,-5043.5998 3381.5,-5043.5998 3381.5,-5055.5998 3381.5,-5055.5998 3381.5,-5061.5998 3375.5,-5067.5998 3369.5,-5067.5998"/>
+<text text-anchor="middle" x="3354.5" y="-5045.8998" font-family="Times,serif" font-size="14.00" fill="#000000">mime</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;mime -->
+<g id="edge51" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;mime</title>
+<path fill="none" stroke="#000000" d="M108.0952,-8979.3436C110.1164,-8599.8414 144.9524,-2429.4869 274,-2303.5998 607.2088,-1978.5516 1913.6521,-2257.3451 2368,-2358.5998 2535.6353,-2395.9586 2623.626,-2364.4463 2727,-2501.5998 2804.6983,-2604.6875 2760.5299,-2952.8508 2785,-3079.5998 2942.8909,-3897.4368 3287.8458,-4865.8096 3346.0737,-5026.4931"/>
+<polygon fill="#000000" stroke="#000000" points="3344.5183,-5027.3372 3347.8686,-5031.4406 3347.8085,-5026.1436 3344.5183,-5027.3372"/>
+</g>
+<!-- net/http -->
+<g id="node63" class="node">
+<title>net/http</title>
+<g id="a_node63"><a xlink:href="https://godoc.org/net/http" xlink:title="net/http" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4113,-598.5998C4113,-598.5998 4079,-598.5998 4079,-598.5998 4073,-598.5998 4067,-592.5998 4067,-586.5998 4067,-586.5998 4067,-574.5998 4067,-574.5998 4067,-568.5998 4073,-562.5998 4079,-562.5998 4079,-562.5998 4113,-562.5998 4113,-562.5998 4119,-562.5998 4125,-568.5998 4125,-574.5998 4125,-574.5998 4125,-586.5998 4125,-586.5998 4125,-592.5998 4119,-598.5998 4113,-598.5998"/>
+<text text-anchor="middle" x="4096" y="-576.8998" font-family="Times,serif" font-size="14.00" fill="#000000">net/http</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;net/http -->
+<g id="edge52" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M108.0768,-8979.5222C109.9464,-8546.7436 146.4675,-486.5344 274,-288.5998 407.6219,-81.2142 528.2945,-49.5998 775,-49.5998 775,-49.5998 775,-49.5998 3354.5,-49.5998 3634.5484,-49.5998 3770.0591,87.2883 3983,-94.5998 4055.2503,-156.314 4086.654,-469.1587 4094.16,-557.3991"/>
+<polygon fill="#000000" stroke="#000000" points="4092.4177,-557.5654 4094.5802,-562.4013 4095.9054,-557.2723 4092.4177,-557.5654"/>
+</g>
+<!-- net/url -->
+<g id="node64" class="node">
+<title>net/url</title>
+<g id="a_node64"><a xlink:href="https://godoc.org/net/url" xlink:title="net/url" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-1542.5998C4111,-1542.5998 4081,-1542.5998 4081,-1542.5998 4075,-1542.5998 4069,-1536.5998 4069,-1530.5998 4069,-1530.5998 4069,-1518.5998 4069,-1518.5998 4069,-1512.5998 4075,-1506.5998 4081,-1506.5998 4081,-1506.5998 4111,-1506.5998 4111,-1506.5998 4117,-1506.5998 4123,-1512.5998 4123,-1518.5998 4123,-1518.5998 4123,-1530.5998 4123,-1530.5998 4123,-1536.5998 4117,-1542.5998 4111,-1542.5998"/>
+<text text-anchor="middle" x="4096" y="-1520.8998" font-family="Times,serif" font-size="14.00" fill="#000000">net/url</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;net/url -->
+<g id="edge53" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M108.1147,-8979.3969C110.7666,-8563.2628 159.6001,-1149.8696 274,-965.5998 406.4266,-752.2934 523.9294,-704.5998 775,-704.5998 775,-704.5998 775,-704.5998 2957,-704.5998 3437.948,-704.5998 3658.6364,-693.4953 3983,-1048.5998 4045.5604,-1117.0893 4083.6281,-1415.2326 4093.4549,-1501.2745"/>
+<polygon fill="#000000" stroke="#000000" points="4091.7333,-1501.6247 4094.035,-1506.3959 4095.211,-1501.2307 4091.7333,-1501.6247"/>
+</g>
+<!-- os -->
+<g id="node65" class="node">
+<title>os</title>
+<g id="a_node65"><a xlink:href="https://godoc.org/os" xlink:title="os" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-18838.5998C4111,-18838.5998 4081,-18838.5998 4081,-18838.5998 4075,-18838.5998 4069,-18832.5998 4069,-18826.5998 4069,-18826.5998 4069,-18814.5998 4069,-18814.5998 4069,-18808.5998 4075,-18802.5998 4081,-18802.5998 4081,-18802.5998 4111,-18802.5998 4111,-18802.5998 4117,-18802.5998 4123,-18808.5998 4123,-18814.5998 4123,-18814.5998 4123,-18826.5998 4123,-18826.5998 4123,-18832.5998 4117,-18838.5998 4111,-18838.5998"/>
+<text text-anchor="middle" x="4096" y="-18816.8998" font-family="Times,serif" font-size="14.00" fill="#000000">os</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;os -->
+<g id="edge54" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M108.1028,-9015.6188C110.761,-9477.4607 164.9863,-18667.3085 274,-19209.5998 360.8766,-19641.7698 334.1844,-20065.5998 775,-20065.5998 775,-20065.5998 775,-20065.5998 1848.5,-20065.5998 2323.6722,-20065.5998 3646.0034,-20273.5952 3983,-19938.5998 4045.8919,-19876.0814 4030.2999,-19230.6308 4041,-19142.5998 4054.6946,-19029.9322 4080.5898,-18896.6367 4091.2409,-18843.819"/>
+<polygon fill="#000000" stroke="#000000" points="4093.0071,-18843.9138 4092.2832,-18838.6661 4089.5766,-18843.2198 4093.0071,-18843.9138"/>
+</g>
+<!-- path -->
+<g id="node66" class="node">
+<title>path</title>
+<g id="a_node66"><a xlink:href="https://godoc.org/path" xlink:title="path" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-2760.5998C4111,-2760.5998 4081,-2760.5998 4081,-2760.5998 4075,-2760.5998 4069,-2754.5998 4069,-2748.5998 4069,-2748.5998 4069,-2736.5998 4069,-2736.5998 4069,-2730.5998 4075,-2724.5998 4081,-2724.5998 4081,-2724.5998 4111,-2724.5998 4111,-2724.5998 4117,-2724.5998 4123,-2730.5998 4123,-2736.5998 4123,-2736.5998 4123,-2748.5998 4123,-2748.5998 4123,-2754.5998 4117,-2760.5998 4111,-2760.5998"/>
+<text text-anchor="middle" x="4096" y="-2738.8998" font-family="Times,serif" font-size="14.00" fill="#000000">path</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;path -->
+<g id="edge55" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M108.1342,-8979.3688C111.1784,-8569.9911 166.0057,-1401.5608 274,-1221.5998 405.3662,-1002.6921 519.7008,-940.5998 775,-940.5998 775,-940.5998 775,-940.5998 2957,-940.5998 3468.8266,-940.5998 3701.1853,-1036.3447 3983,-1463.5998 4036.1603,-1544.1953 4031.3848,-2234.5311 4041,-2330.5998 4055.8888,-2479.3594 4082.7006,-2657.1757 4092.3861,-2719.5908"/>
+<polygon fill="#000000" stroke="#000000" points="4090.664,-2719.9063 4093.1617,-2724.5779 4094.1224,-2719.3683 4090.664,-2719.9063"/>
+</g>
+<!-- path/filepath -->
+<g id="node67" class="node">
+<title>path/filepath</title>
+<g id="a_node67"><a xlink:href="https://godoc.org/path/filepath" xlink:title="path/filepath" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4126.5,-19398.5998C4126.5,-19398.5998 4065.5,-19398.5998 4065.5,-19398.5998 4059.5,-19398.5998 4053.5,-19392.5998 4053.5,-19386.5998 4053.5,-19386.5998 4053.5,-19374.5998 4053.5,-19374.5998 4053.5,-19368.5998 4059.5,-19362.5998 4065.5,-19362.5998 4065.5,-19362.5998 4126.5,-19362.5998 4126.5,-19362.5998 4132.5,-19362.5998 4138.5,-19368.5998 4138.5,-19374.5998 4138.5,-19374.5998 4138.5,-19386.5998 4138.5,-19386.5998 4138.5,-19392.5998 4132.5,-19398.5998 4126.5,-19398.5998"/>
+<text text-anchor="middle" x="4096" y="-19376.8998" font-family="Times,serif" font-size="14.00" fill="#000000">path/filepath</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/docker&#45;&gt;path/filepath -->
+<g id="edge56" class="edge">
+<title>github.com/containers/image/docker&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M108.0503,-9015.9265C109.4324,-9510.3739 139.9208,-19846.548 274,-20116.5998 396.1636,-20362.6521 500.2898,-20478.5998 775,-20478.5998 775,-20478.5998 775,-20478.5998 3354.5,-20478.5998 3638.003,-20478.5998 3788.4257,-20575.791 3983,-20369.5998 4051.6518,-20296.8492 4088.5525,-19546.77 4094.9813,-19403.9997"/>
+<polygon fill="#000000" stroke="#000000" points="4096.7387,-19403.8695 4095.2141,-19398.7963 4093.2422,-19403.713 4096.7387,-19403.8695"/>
+</g>
+<!-- github.com/containers/image/v5/docker/policyconfiguration&#45;&gt;strings -->
+<g id="edge63" class="edge">
+<title>github.com/containers/image/v5/docker/policyconfiguration&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2968.6847,-7770.4044C3000.1751,-7720.4445 3087.2395,-7576.0453 3129,-7443.5998 3155.2159,-7360.4546 3120.9544,-7112.5065 3187,-7055.5998 3321.0067,-6940.1361 3806.928,-7038.6195 3983,-7055.5998 4010.558,-7058.2575 4041.3784,-7064.4805 4063.8617,-7069.6323"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5487,-7071.3561 4068.8148,-7070.7808 4064.3394,-7067.9465 4063.5487,-7071.3561"/>
+</g>
+<!-- github.com/containers/image/v5/docker/policyconfiguration&#45;&gt;github.com/containers/image/v5/docker/reference -->
+<g id="edge61" class="edge">
+<title>github.com/containers/image/v5/docker/policyconfiguration&#45;&gt;github.com/containers/image/v5/docker/reference</title>
+<path fill="none" stroke="#000000" d="M2960.5065,-7770.5879C2982.6087,-7657.8489 3104.3608,-7048.6054 3187,-6878.5998 3225.6437,-6799.1019 3296.3819,-6720.7348 3332.3033,-6683.7033"/>
+<polygon fill="#000000" stroke="#000000" points="3333.8503,-6684.6236 3336.088,-6679.822 3331.3444,-6682.1801 3333.8503,-6684.6236"/>
+</g>
+<!-- github.com/containers/image/v5/docker/policyconfiguration&#45;&gt;github.com/pkg/errors -->
+<g id="edge62" class="edge">
+<title>github.com/containers/image/v5/docker/policyconfiguration&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M2971.6607,-7806.6478C3071.6871,-7929.7852 3652.9872,-8645.3943 3763.3966,-8781.3137"/>
+<polygon fill="#000000" stroke="#000000" points="3762.3147,-8782.7573 3766.8256,-8785.5349 3765.0313,-8780.5505 3762.3147,-8782.7573"/>
+</g>
+<!-- github.com/containers/image/v5/docker/reference&#45;&gt;errors -->
+<g id="edge64" class="edge">
+<title>github.com/containers/image/v5/docker/reference&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3414.8302,-6643.4473C3451.3364,-6629.3641 3495.7599,-6606.1443 3522,-6570.5998 3599.5486,-6465.5536 3478.9696,-6365.3122 3580,-6282.5998 3718.5899,-6169.1379 3843.0845,-6170.7766 3983,-6282.5998 4059.5117,-6343.7495 4087.9909,-6669.3928 4094.4716,-6759.4852"/>
+<polygon fill="#000000" stroke="#000000" points="4092.7338,-6759.7211 4094.8329,-6764.5849 4096.2251,-6759.4737 4092.7338,-6759.7211"/>
+</g>
+<!-- github.com/containers/image/v5/docker/reference&#45;&gt;fmt -->
+<g id="edge65" class="edge">
+<title>github.com/containers/image/v5/docker/reference&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3357.9512,-6679.7581C3384.7706,-6820.6805 3559.3827,-7734.7656 3580,-7754.5998 3710.85,-7880.4799 3857.3701,-7690.5096 3983,-7821.5998 4047.1771,-7888.5662 4087.4107,-8581.1102 4094.7748,-8718.1231"/>
+<polygon fill="#000000" stroke="#000000" points="4093.0476,-8718.5979 4095.0621,-8723.4973 4096.5426,-8718.411 4093.0476,-8718.5979"/>
+</g>
+<!-- github.com/containers/image/v5/docker/reference&#45;&gt;strings -->
+<g id="edge69" class="edge">
+<title>github.com/containers/image/v5/docker/reference&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3498.2613,-6645.2014C3637.7676,-6637.436 3847.6966,-6648.4041 3983,-6760.5998 4030.2308,-6799.7643 4076.0228,-6988.0367 4090.946,-7054.3925"/>
+<polygon fill="#000000" stroke="#000000" points="4089.2501,-7054.8279 4092.049,-7059.3256 4092.6657,-7054.0642 4089.2501,-7054.8279"/>
+</g>
+<!-- github.com/containers/image/v5/docker/reference&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge66" class="edge">
+<title>github.com/containers/image/v5/docker/reference&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M3359.2972,-6679.8553C3382.7744,-6770.1929 3485.8837,-7178.7365 3522,-7520.5998 3540.879,-7699.3009 3521.5477,-10584.6768 3580,-10754.5998 3616.467,-10860.6108 3712.1032,-10958.7915 3757.2458,-11000.9275"/>
+<polygon fill="#000000" stroke="#000000" points="3756.0754,-11002.2289 3760.9309,-11004.3472 3758.4562,-10999.6633 3756.0754,-11002.2289"/>
+</g>
+<!-- github.com/containers/image/v5/docker/reference&#45;&gt;path -->
+<g id="edge67" class="edge">
+<title>github.com/containers/image/v5/docker/reference&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M3420.9174,-6643.5807C3457.4949,-6629.9198 3499.8538,-6607.0981 3522,-6570.5998 3616.7152,-6414.5039 3510.7123,-3453.5262 3580,-3284.5998 3684.3451,-3030.2021 3968.831,-2825.9968 4064.2888,-2762.8662"/>
+<polygon fill="#000000" stroke="#000000" points="4065.4772,-2764.179 4068.69,-2759.967 4063.5518,-2761.2561 4065.4772,-2764.179"/>
+</g>
+<!-- regexp -->
+<g id="node68" class="node">
+<title>regexp</title>
+<g id="a_node68"><a xlink:href="https://godoc.org/regexp" xlink:title="regexp" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-2315.5998C4111,-2315.5998 4081,-2315.5998 4081,-2315.5998 4075,-2315.5998 4069,-2309.5998 4069,-2303.5998 4069,-2303.5998 4069,-2291.5998 4069,-2291.5998 4069,-2285.5998 4075,-2279.5998 4081,-2279.5998 4081,-2279.5998 4111,-2279.5998 4111,-2279.5998 4117,-2279.5998 4123,-2285.5998 4123,-2291.5998 4123,-2291.5998 4123,-2303.5998 4123,-2303.5998 4123,-2309.5998 4117,-2315.5998 4111,-2315.5998"/>
+<text text-anchor="middle" x="4096" y="-2293.8998" font-family="Times,serif" font-size="14.00" fill="#000000">regexp</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/docker/reference&#45;&gt;regexp -->
+<g id="edge68" class="edge">
+<title>github.com/containers/image/v5/docker/reference&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M3421.3991,-6643.4554C3457.8981,-6629.7881 3500.015,-6607.0023 3522,-6570.5998 3583.6837,-6468.4649 3495.0628,-2359.3977 3580,-2275.5998 3713.5662,-2143.8251 3973.5433,-2242.1765 4064.0308,-2282.4868"/>
+<polygon fill="#000000" stroke="#000000" points="4063.4453,-2284.1422 4068.7235,-2284.5932 4064.8786,-2280.9491 4063.4453,-2284.1422"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;bytes -->
+<g id="edge70" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M1853.2909,-10920.9917C1925.1103,-11196.6936 2780.3594,-14479.5545 2785,-14483.5998 2885.6542,-14571.3421 3884.908,-14669.1975 3983,-14578.5998 4042.6916,-14523.4687 4023.1907,-13928.8801 4041,-13849.5998 4051.0409,-13804.9013 4072.0324,-13755.563 4084.9814,-13727.5002"/>
+<polygon fill="#000000" stroke="#000000" points="4086.6231,-13728.1201 4087.1444,-13722.8484 4083.4494,-13726.6444 4086.6231,-13728.1201"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;context -->
+<g id="edge71" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M1848.585,-10884.4439C1849.8683,-10632.3596 1867.5275,-7805.057 2023,-5529.5998 2030.3525,-5421.9904 2370.4442,-1760.0519 2426,-1667.5998 2505.6188,-1535.1037 3432.1576,-935.7332 3580,-890.5998 3751.3064,-838.3034 3806.967,-857.5369 3983,-890.5998 4011.0096,-895.8607 4040.994,-907.8611 4063.0325,-918.0024"/>
+<polygon fill="#000000" stroke="#000000" points="4062.3397,-919.6102 4067.6111,-920.1351 4063.8176,-916.4375 4062.3397,-919.6102"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;crypto/sha256 -->
+<g id="edge72" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;crypto/sha256</title>
+<path fill="none" stroke="#000000" d="M1848.8658,-10921.0037C1854.8325,-11219.0464 1932.9638,-15043.4878 2023,-15529.5998 2124.0814,-16075.3456 1974.6312,-16388.6105 2426,-16711.5998 2960.5583,-17094.1177 3853.5105,-16704.248 4054.8406,-16607.9408"/>
+<polygon fill="#000000" stroke="#000000" points="4055.7163,-16609.4617 4059.4673,-16605.7209 4054.2022,-16606.3061 4055.7163,-16609.4617"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;encoding/hex -->
+<g id="edge73" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;encoding/hex</title>
+<path fill="none" stroke="#000000" d="M1855.3103,-10884.5502C1874.3511,-10836.4114 1932.7297,-10703.6682 2023,-10630.5998 2431.3295,-10300.0815 2803.6678,-10626.0732 3129,-10213.5998 3256.6122,-10051.8063 3079.5923,-9929.4566 3187,-9753.5998 3281.0058,-9599.6858 3417.7717,-9671.7837 3522,-9524.5998 3579.6935,-9443.1291 3504.8796,-9373.349 3580,-9307.5998 3621.2748,-9271.4741 3686.7572,-9269.6436 3731.8073,-9273.7733"/>
+<polygon fill="#000000" stroke="#000000" points="3731.7752,-9275.5284 3736.9223,-9274.2757 3732.1174,-9272.0452 3731.7752,-9275.5284"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;encoding/json -->
+<g id="edge74" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M1848.7302,-10920.8754C1852.7685,-11237.6297 1909.974,-15572.3999 2023,-16119.5998 2167.7245,-16820.2638 2250.5784,-17061.9227 2785,-17537.5998 2949.7549,-17684.2446 2977.4319,-17759.8241 3187,-17828.5998 3355.0708,-17883.757 3846.7323,-17944.3883 3983,-17831.5998 4073.1709,-17756.9655 4091.9927,-17361.5946 4095.3463,-17260.9327"/>
+<polygon fill="#000000" stroke="#000000" points="4097.0992,-17260.8699 4095.5118,-17255.8159 4093.601,-17260.7567 4097.0992,-17260.8699"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;fmt -->
+<g id="edge75" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1849.7096,-10884.522C1856.9136,-10782.4914 1898.1711,-10269.0913 2023,-9871.5998 2148.7466,-9471.1863 2134.1248,-9319.1824 2426,-9017.5998 2897.2503,-8530.6764 3874.4881,-8697.0103 4063.8372,-8734.8263"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7013,-8736.5839 4068.9482,-8735.8542 4064.3914,-8733.1526 4063.7013,-8736.5839"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;io/ioutil -->
+<g id="edge84" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M1848.6494,-10920.9724C1851.0537,-11207.1675 1883.5415,-14746.8655 2023,-15181.5998 2242.3903,-15865.5056 2513.6547,-16052.6692 3187,-16302.5998 3519.2952,-16425.9403 3636.4863,-16426.1737 3983,-16351.5998 4010.6987,-16345.6387 4039.7898,-16332.3233 4061.5176,-16320.793"/>
+<polygon fill="#000000" stroke="#000000" points="4062.4634,-16322.2715 4066.0392,-16318.3629 4060.8065,-16319.1885 4062.4634,-16322.2715"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;strings -->
+<g id="edge85" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M1848.9611,-10884.5107C1853.4159,-10717.0107 1891.539,-9434.722 2023,-9077.5998 2227.1904,-8522.9037 2334.8771,-8370.7067 2785,-7987.5998 2914.275,-7877.5718 3033.0747,-7961.6588 3129,-7821.5998 3220.3295,-7688.2512 3069.3959,-7207.4699 3187,-7096.5998 3251.7708,-7035.5378 3908.1712,-7067.3336 4063.2787,-7075.7537"/>
+<polygon fill="#000000" stroke="#000000" points="4063.4951,-7077.518 4068.583,-7076.0432 4063.6859,-7074.0232 4063.4951,-7077.518"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;github.com/containers/image/v5/docker/reference -->
+<g id="edge76" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;github.com/containers/image/v5/docker/reference</title>
+<path fill="none" stroke="#000000" d="M1848.8119,-10884.4322C1853.8486,-10594.0444 1919.5673,-6913.7894 2023,-6449.5998 2123.1953,-5999.9391 2042.1066,-5749.2755 2426,-5494.5998 2537.4775,-5420.6454 2629.5022,-5402.9992 2727,-5494.5998 2877.7505,-5636.2321 2665.0425,-6255.0898 2785,-6423.5998 2881.8655,-6559.6715 3071.9006,-6618.2753 3205.7387,-6643.302"/>
+<polygon fill="#000000" stroke="#000000" points="3205.6386,-6645.0631 3210.8734,-6644.2509 3206.2747,-6641.6213 3205.6386,-6645.0631"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;github.com/containers/image/v5/manifest -->
+<g id="edge77" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;github.com/containers/image/v5/manifest</title>
+<path fill="none" stroke="#000000" d="M1870.7343,-10920.7973C1932.0631,-10970.9915 2103.4247,-11111.2413 2169.2687,-11165.131"/>
+<polygon fill="#000000" stroke="#000000" points="2168.4732,-11166.7413 2173.4509,-11168.5539 2170.69,-11164.0328 2168.4732,-11166.7413"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;github.com/containers/image/v5/pkg/blobinfocache/none -->
+<g id="edge78" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;github.com/containers/image/v5/pkg/blobinfocache/none</title>
+<path fill="none" stroke="#000000" d="M1868.2805,-10884.4444C1899.7289,-10856.0385 1963.655,-10800.1459 2023,-10759.5998 2066.0326,-10730.1987 2118.6332,-10701.5834 2154.3542,-10683.1454"/>
+<polygon fill="#000000" stroke="#000000" points="2155.4153,-10684.5676 2159.0611,-10680.7243 2153.8143,-10681.4552 2155.4153,-10684.5676"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;github.com/containers/image/v5/types -->
+<g id="edge79" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;github.com/containers/image/v5/types</title>
+<path fill="none" stroke="#000000" d="M1963.732,-10902.5998C2175.7003,-10902.5998 2625.2907,-10902.5998 2839.178,-10902.5998"/>
+<polygon fill="#000000" stroke="#000000" points="2839.1864,-10904.3499 2844.1864,-10902.5998 2839.1864,-10900.8499 2839.1864,-10904.3499"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge80" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M1963.9488,-10897.8497C1983.6499,-10897.0732 2003.9114,-10896.2958 2023,-10895.5998 2514.3546,-10877.6845 2692.335,-10644.6042 3129,-10870.5998 3168.6447,-10891.1179 3148.6251,-10929.795 3187,-10952.5998 3227.2959,-10976.5462 3506.0663,-11001.5072 3667.1677,-11014.1593"/>
+<polygon fill="#000000" stroke="#000000" points="3667.2523,-11015.9212 3672.3737,-11014.567 3667.5257,-11012.4318 3667.2523,-11015.9212"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge81" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M1950.2397,-10920.6373C2223.0461,-10969.0034 2968.9862,-11101.2518 3247.6278,-11150.6524"/>
+<polygon fill="#000000" stroke="#000000" points="3247.6816,-11152.4391 3252.9104,-11151.5889 3248.2927,-11148.9929 3247.6816,-11152.4391"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;github.com/pkg/errors -->
+<g id="edge82" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M1854.8032,-10884.4508C1875.9502,-10824.1611 1947.3096,-10625.4906 2023,-10468.5998 2298.2011,-9898.1635 2468.1826,-9807.6541 2727,-9229.5998 2759.9671,-9155.9696 2723.5823,-9109.9077 2785,-9057.5998 2911.4102,-8949.9395 3386.4522,-9030.5011 3522,-8934.5998 3565.5216,-8903.8079 3535.9156,-8859.5804 3580,-8829.5998 3616.7065,-8804.6368 3666.0198,-8797.934 3706.674,-8797.5708"/>
+<polygon fill="#000000" stroke="#000000" points="3707.0994,-8799.32 3712.0956,-8797.5591 3707.0918,-8795.82 3707.0994,-8799.32"/>
+</g>
+<!-- github.com/containers/image/v5/image&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge83" class="edge">
+<title>github.com/containers/image/v5/image&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M1855.8957,-10920.6842C1877.075,-10971.2145 1941.6911,-11116.9076 2023,-11219.5998 2439.4659,-11745.5915 3151.8673,-12185.5863 3318.7761,-12284.708"/>
+<polygon fill="#000000" stroke="#000000" points="3318.3608,-12286.4962 3323.5542,-12287.5409 3320.1458,-12283.4856 3318.3608,-12286.4962"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;encoding/json -->
+<g id="edge88" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M2196.367,-11204.7558C2206.5234,-11417.7322 2303.6499,-13462.5544 2368,-15116.5998 2371.6953,-15211.5826 2376.6984,-16745.3303 2426,-16826.5998 2505.6876,-16957.9582 2608.6825,-16898.5891 2727,-16996.5998 2753.6557,-17018.6806 3108.7557,-17444.5239 3129,-17472.5998 3157.9331,-17512.7257 3148.8917,-17536.0567 3187,-17567.5998 3330.7781,-17686.6083 3396.3764,-17682.1703 3580,-17715.5998 3668.1074,-17731.6401 3912.7671,-17771.1654 3983,-17715.5998 4056.2818,-17657.6221 4086.909,-17348.5848 4094.2061,-17260.8439"/>
+<polygon fill="#000000" stroke="#000000" points="4095.9692,-17260.7545 4094.6339,-17255.6281 4092.4809,-17260.4683 4095.9692,-17260.7545"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;fmt -->
+<g id="edge89" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2204.1127,-11168.4193C2233.6514,-11105.0339 2331.0609,-10887.2297 2368,-10695.5998 2396.3759,-10548.3934 2358.485,-9481.4528 2426,-9347.5998 2748.517,-8708.1886 3859.8042,-8730.1435 4063.7212,-8739.7579"/>
+<polygon fill="#000000" stroke="#000000" points="4063.8516,-8741.5161 4068.9304,-8740.0102 4064.021,-8738.0202 4063.8516,-8741.5161"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;strings -->
+<g id="edge104" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2204.2158,-11168.4388C2234.091,-11105.117 2332.4679,-10887.4955 2368,-10695.5998 2388.7136,-10583.7335 2362.5595,-8743.0373 2426,-8648.5998 2507.5565,-8527.1949 2634.8464,-8629.1706 2727,-8515.5998 2805.4309,-8418.941 2717.3554,-8346.0916 2785,-8241.5998 2887.8508,-8082.7245 3036.5111,-8155.7224 3129,-7990.5998 3207.1181,-7851.1337 3086.4961,-7397.9067 3187,-7273.5998 3414.6808,-6991.9962 3625.7976,-7159.1448 3983,-7099.5998 4010.309,-7095.0475 4041.1499,-7088.9802 4063.7044,-7084.3704"/>
+<polygon fill="#000000" stroke="#000000" points="4064.1282,-7086.07 4068.6745,-7083.3508 4063.4248,-7082.6414 4064.1282,-7086.07"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;time -->
+<g id="edge105" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2317.6508,-11190.176C2335.6683,-11194.7761 2353.1393,-11201.8808 2368,-11212.5998 2414.1051,-11245.8553 2386.4788,-11285.7381 2426,-11326.5998 2532.2107,-11436.413 2623.4613,-11380.2638 2727,-11492.5998 2769.6032,-11538.8229 2732.2172,-11587.4592 2785,-11621.5998 2913.3753,-11704.6348 2977.0542,-11638.5555 3129,-11621.5998 3322.3636,-11600.0223 3796.7246,-11484.783 3983,-11428.5998 4010.955,-11420.1682 4041.8942,-11408.2035 4064.3205,-11399.0348"/>
+<polygon fill="#000000" stroke="#000000" points="4065.0132,-11400.6422 4068.9727,-11397.123 4063.6828,-11397.4049 4065.0132,-11400.6422"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/image/v5/docker/reference -->
+<g id="edge90" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/image/v5/docker/reference</title>
+<path fill="none" stroke="#000000" d="M2204.3073,-11168.4554C2234.4817,-11105.188 2333.7179,-10887.7228 2368,-10695.5998 2386.4908,-10591.9743 2363.9422,-6991.6233 2426,-6906.5998 2517.5308,-6781.1965 2967.3479,-6708.5828 3205.6657,-6678.3235"/>
+<polygon fill="#000000" stroke="#000000" points="3206.1652,-6680.0244 3210.9061,-6677.6606 3205.726,-6676.552 3206.1652,-6680.0244"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/image/v5/types -->
+<g id="edge93" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/image/v5/types</title>
+<path fill="none" stroke="#000000" d="M2224.0787,-11168.5409C2266.7846,-11142.0957 2350.3432,-11092.4216 2426,-11059.5998 2579.9523,-10992.8115 2623.3441,-10987.6482 2785,-10942.5998 2811.6902,-10935.1621 2840.9717,-10927.9907 2867.6886,-10921.8245"/>
+<polygon fill="#000000" stroke="#000000" points="2868.1653,-10923.5107 2872.6462,-10920.685 2867.3812,-10920.0996 2868.1653,-10923.5107"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge97" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M2317.5029,-11195.9989C2626.9012,-11218.7278 3424.5812,-11270.0086 3522,-11202.5998 3582.1434,-11160.9837 3522.6486,-11093.9866 3580,-11048.5998 3604.4471,-11029.2529 3636.0327,-11020.1661 3666.9134,-11016.5608"/>
+<polygon fill="#000000" stroke="#000000" points="3667.4784,-11018.2605 3672.2642,-11015.9892 3667.1066,-11014.7803 3667.4784,-11018.2605"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge99" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M2317.6157,-11184.8086C2529.9102,-11181.6948 2966.5936,-11175.2896 3199.7627,-11171.8695"/>
+<polygon fill="#000000" stroke="#000000" points="3199.9167,-11173.6175 3204.8904,-11171.7943 3199.8653,-11170.1179 3199.9167,-11173.6175"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/pkg/errors -->
+<g id="edge100" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M2202.3281,-11168.4554C2226.3709,-11104.3072 2308.8454,-10882.0502 2368,-10695.5998 2419.3106,-10533.8732 2673.5923,-9354.5709 2785,-9226.5998 2913.1823,-9079.3602 2999.944,-9105.4594 3187,-9049.5998 3331.6431,-9006.4058 3406.3733,-9090.6453 3522,-8993.5998 3581.2193,-8943.8971 3519.8639,-8878.1893 3580,-8829.5998 3615.1964,-8801.1615 3665.7177,-8794.6958 3707.3665,-8795.3225"/>
+<polygon fill="#000000" stroke="#000000" points="3707.3627,-8797.0728 3712.4,-8795.4331 3707.4397,-8793.5736 3707.3627,-8797.0728"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge101" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M2204.6998,-11204.8305C2231.8005,-11256.9091 2316.3579,-11408.2029 2426,-11495.5998 2540.3672,-11586.7632 2619.4963,-11529.4355 2727,-11628.5998 2766.7604,-11665.2759 2744.2054,-11700.0776 2785,-11735.5998 2908.1434,-11842.8277 3024.487,-11739.144 3129,-11864.5998 3222.6343,-11976.9969 3108.1681,-12065.3683 3187,-12188.5998 3215.7462,-12233.5365 3268.5221,-12266.0534 3307.1751,-12285.235"/>
+<polygon fill="#000000" stroke="#000000" points="3306.5561,-12286.8804 3311.8163,-12287.5081 3308.0956,-12283.7372 3306.5561,-12286.8804"/>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;regexp -->
+<g id="edge102" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M2204.3691,-11168.4663C2234.7454,-11105.2345 2334.562,-10887.8715 2368,-10695.5998 2389.1608,-10573.9235 2358.2528,-1906.8627 2426,-1803.5998 2506.2281,-1681.313 2597.3688,-1738.3225 2727,-1670.5998 2939.1361,-1559.7745 2969.0712,-1489.5492 3187,-1390.5998 3353.7695,-1314.8791 3398.9797,-1294.4803 3580,-1266.5998 3757.0238,-1239.3349 3849.6284,-1147.0473 3983,-1266.5998 4022.0078,-1301.5659 4083.4872,-2125.1148 4094.337,-2274.4789"/>
+<polygon fill="#000000" stroke="#000000" points="4092.5944,-2274.6451 4094.7015,-2279.5054 4096.0852,-2274.3919 4092.5944,-2274.6451"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression -->
+<g id="node71" class="node">
+<title>github.com/containers/image/v5/pkg/compression</title>
+<g id="a_node71"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/compression" xlink:title="github.com/containers/image/v5/pkg/compression" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2709.5,-12366.5998C2709.5,-12366.5998 2443.5,-12366.5998 2443.5,-12366.5998 2437.5,-12366.5998 2431.5,-12360.5998 2431.5,-12354.5998 2431.5,-12354.5998 2431.5,-12342.5998 2431.5,-12342.5998 2431.5,-12336.5998 2437.5,-12330.5998 2443.5,-12330.5998 2443.5,-12330.5998 2709.5,-12330.5998 2709.5,-12330.5998 2715.5,-12330.5998 2721.5,-12336.5998 2721.5,-12342.5998 2721.5,-12342.5998 2721.5,-12354.5998 2721.5,-12354.5998 2721.5,-12360.5998 2715.5,-12366.5998 2709.5,-12366.5998"/>
+<text text-anchor="middle" x="2576.5" y="-12344.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/compression</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/image/v5/pkg/compression -->
+<g id="edge91" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/image/v5/pkg/compression</title>
+<path fill="none" stroke="#000000" d="M2201.4846,-11204.8521C2245.5519,-11339.2517 2519.457,-12174.6261 2568.8664,-12325.3183"/>
+<polygon fill="#000000" stroke="#000000" points="2567.3078,-12326.1819 2570.5286,-12330.3878 2570.6336,-12325.0914 2567.3078,-12326.1819"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/strslice -->
+<g id="node72" class="node">
+<title>github.com/containers/image/v5/pkg/strslice</title>
+<g id="a_node72"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/strslice" xlink:title="github.com/containers/image/v5/pkg/strslice" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3074.5,-17522.5998C3074.5,-17522.5998 2839.5,-17522.5998 2839.5,-17522.5998 2833.5,-17522.5998 2827.5,-17516.5998 2827.5,-17510.5998 2827.5,-17510.5998 2827.5,-17498.5998 2827.5,-17498.5998 2827.5,-17492.5998 2833.5,-17486.5998 2839.5,-17486.5998 2839.5,-17486.5998 3074.5,-17486.5998 3074.5,-17486.5998 3080.5,-17486.5998 3086.5,-17492.5998 3086.5,-17498.5998 3086.5,-17498.5998 3086.5,-17510.5998 3086.5,-17510.5998 3086.5,-17516.5998 3080.5,-17522.5998 3074.5,-17522.5998"/>
+<text text-anchor="middle" x="2957" y="-17500.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/strslice</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/image/v5/pkg/strslice -->
+<g id="edge92" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/image/v5/pkg/strslice</title>
+<path fill="none" stroke="#000000" d="M2196.3889,-11204.755C2206.7975,-11417.7218 2306.2245,-13462.4563 2368,-15116.5998 2371.9059,-15221.1876 2380.169,-16905.5074 2426,-16999.5998 2502.2766,-17156.1981 2617.6598,-17115.0054 2727,-17250.5998 2761.9401,-17293.9295 2750.0281,-17318.2958 2785,-17361.5998 2826.0298,-17412.4049 2887.3743,-17458.1685 2924.6399,-17483.5913"/>
+<polygon fill="#000000" stroke="#000000" points="2923.9416,-17485.2322 2929.0622,-17486.5898 2925.9059,-17482.3353 2923.9416,-17485.2322"/>
+</g>
+<!-- github.com/containers/libtrust -->
+<g id="node73" class="node">
+<title>github.com/containers/libtrust</title>
+<g id="a_node73"><a xlink:href="https://godoc.org/github.com/containers/libtrust" xlink:title="github.com/containers/libtrust" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3859.5,-16882.5998C3859.5,-16882.5998 3703.5,-16882.5998 3703.5,-16882.5998 3697.5,-16882.5998 3691.5,-16876.5998 3691.5,-16870.5998 3691.5,-16870.5998 3691.5,-16858.5998 3691.5,-16858.5998 3691.5,-16852.5998 3697.5,-16846.5998 3703.5,-16846.5998 3703.5,-16846.5998 3859.5,-16846.5998 3859.5,-16846.5998 3865.5,-16846.5998 3871.5,-16852.5998 3871.5,-16858.5998 3871.5,-16858.5998 3871.5,-16870.5998 3871.5,-16870.5998 3871.5,-16876.5998 3865.5,-16882.5998 3859.5,-16882.5998"/>
+<text text-anchor="middle" x="3781.5" y="-16860.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/libtrust</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/libtrust -->
+<g id="edge94" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/libtrust</title>
+<path fill="none" stroke="#000000" d="M2196.3499,-11204.7565C2206.3084,-11417.7407 2301.63,-13462.6342 2368,-15116.5998 2371.5553,-15205.1982 2365.7299,-16646.5627 2426,-16711.5998 2769.9562,-17082.7615 3484.0615,-16940.9232 3710.7964,-16883.8743"/>
+<polygon fill="#000000" stroke="#000000" points="3711.3167,-16885.5479 3715.7352,-16882.6256 3710.4588,-16882.1547 3711.3167,-16885.5479"/>
+</g>
+<!-- github.com/containers/ocicrypt/spec -->
+<g id="node74" class="node">
+<title>github.com/containers/ocicrypt/spec</title>
+<g id="a_node74"><a xlink:href="https://godoc.org/github.com/containers/ocicrypt/spec" xlink:title="github.com/containers/ocicrypt/spec" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2671.5,-11311.5998C2671.5,-11311.5998 2481.5,-11311.5998 2481.5,-11311.5998 2475.5,-11311.5998 2469.5,-11305.5998 2469.5,-11299.5998 2469.5,-11299.5998 2469.5,-11287.5998 2469.5,-11287.5998 2469.5,-11281.5998 2475.5,-11275.5998 2481.5,-11275.5998 2481.5,-11275.5998 2671.5,-11275.5998 2671.5,-11275.5998 2677.5,-11275.5998 2683.5,-11281.5998 2683.5,-11287.5998 2683.5,-11287.5998 2683.5,-11299.5998 2683.5,-11299.5998 2683.5,-11305.5998 2677.5,-11311.5998 2671.5,-11311.5998"/>
+<text text-anchor="middle" x="2576.5" y="-11289.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/ocicrypt/spec</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/ocicrypt/spec -->
+<g id="edge95" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/containers/ocicrypt/spec</title>
+<path fill="none" stroke="#000000" d="M2256.0549,-11204.616C2302.4806,-11218.3278 2368.1854,-11237.5166 2426,-11253.5998 2450.6816,-11260.4659 2477.7119,-11267.7296 2501.9704,-11274.1557"/>
+<polygon fill="#000000" stroke="#000000" points="2501.8217,-11275.9265 2507.103,-11275.5138 2502.717,-11272.5429 2501.8217,-11275.9265"/>
+</g>
+<!-- github.com/docker/docker/api/types/versions -->
+<g id="node75" class="node">
+<title>github.com/docker/docker/api/types/versions</title>
+<g id="a_node75"><a xlink:href="https://godoc.org/github.com/docker/docker/api/types/versions" xlink:title="github.com/docker/docker/api/types/versions" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3900,-5847.5998C3900,-5847.5998 3663,-5847.5998 3663,-5847.5998 3657,-5847.5998 3651,-5841.5998 3651,-5835.5998 3651,-5835.5998 3651,-5823.5998 3651,-5823.5998 3651,-5817.5998 3657,-5811.5998 3663,-5811.5998 3663,-5811.5998 3900,-5811.5998 3900,-5811.5998 3906,-5811.5998 3912,-5817.5998 3912,-5823.5998 3912,-5823.5998 3912,-5835.5998 3912,-5835.5998 3912,-5841.5998 3906,-5847.5998 3900,-5847.5998"/>
+<text text-anchor="middle" x="3781.5" y="-5825.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/api/types/versions</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/docker/docker/api/types/versions -->
+<g id="edge96" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/docker/docker/api/types/versions</title>
+<path fill="none" stroke="#000000" d="M2204.3113,-11168.4562C2234.4987,-11105.1911 2333.7724,-10887.7325 2368,-10695.5998 2444.6902,-10265.1078 2309.1557,-7181.9693 2426,-6760.5998 2510.8796,-6454.5031 2536.8991,-6340.9573 2785,-6142.5998 3043.2689,-5936.113 3439.203,-5864.9801 3645.6466,-5841.1414"/>
+<polygon fill="#000000" stroke="#000000" points="3646.0219,-5842.86 3650.7905,-5840.5527 3645.6239,-5839.3826 3646.0219,-5842.86"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go -->
+<g id="node76" class="node">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go</title>
+<g id="a_node76"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go" xlink:title="github.com/opencontainers/image&#45;spec/specs&#45;go" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3910,-9541.5998C3910,-9541.5998 3653,-9541.5998 3653,-9541.5998 3647,-9541.5998 3641,-9535.5998 3641,-9529.5998 3641,-9529.5998 3641,-9517.5998 3641,-9517.5998 3641,-9511.5998 3647,-9505.5998 3653,-9505.5998 3653,-9505.5998 3910,-9505.5998 3910,-9505.5998 3916,-9505.5998 3922,-9511.5998 3922,-9517.5998 3922,-9517.5998 3922,-9529.5998 3922,-9529.5998 3922,-9535.5998 3916,-9541.5998 3910,-9541.5998"/>
+<text text-anchor="middle" x="3781.5" y="-9519.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image&#45;spec/specs&#45;go</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go -->
+<g id="edge98" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go</title>
+<path fill="none" stroke="#000000" d="M2317.8182,-11171.4229C2546.8752,-11140.2948 3026.6579,-11061.3828 3129,-10935.5998 3269.6257,-10762.7645 3094.7667,-10137.4315 3187,-9934.5998 3237.6073,-9823.3085 3470.0053,-9602.9668 3580,-9549.5998 3597.407,-9541.1543 3616.6053,-9535.1343 3636.0281,-9530.8882"/>
+<polygon fill="#000000" stroke="#000000" points="3636.4385,-9532.5902 3640.97,-9529.8465 3635.7165,-9529.1655 3636.4385,-9532.5902"/>
+</g>
+<!-- runtime -->
+<g id="node77" class="node">
+<title>runtime</title>
+<g id="a_node77"><a xlink:href="https://godoc.org/runtime" xlink:title="runtime" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4113.5,-17802.5998C4113.5,-17802.5998 4078.5,-17802.5998 4078.5,-17802.5998 4072.5,-17802.5998 4066.5,-17796.5998 4066.5,-17790.5998 4066.5,-17790.5998 4066.5,-17778.5998 4066.5,-17778.5998 4066.5,-17772.5998 4072.5,-17766.5998 4078.5,-17766.5998 4078.5,-17766.5998 4113.5,-17766.5998 4113.5,-17766.5998 4119.5,-17766.5998 4125.5,-17772.5998 4125.5,-17778.5998 4125.5,-17778.5998 4125.5,-17790.5998 4125.5,-17790.5998 4125.5,-17796.5998 4119.5,-17802.5998 4113.5,-17802.5998"/>
+<text text-anchor="middle" x="4096" y="-17780.8998" font-family="Times,serif" font-size="14.00" fill="#000000">runtime</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/manifest&#45;&gt;runtime -->
+<g id="edge103" class="edge">
+<title>github.com/containers/image/v5/manifest&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M2196.4046,-11204.7544C2206.9937,-11417.7145 2308.0677,-13462.3885 2368,-15116.5998 2376.1699,-15342.0995 2376.4136,-16926.468 2426,-17146.5998 2498.9868,-17470.6144 2596.5794,-17525.1445 2727,-17830.5998 2910.4101,-18260.1609 3012.2884,-18349.3384 3129,-18801.5998 3157.6673,-18912.6868 3120.7613,-19220.9272 3187,-19314.5998 3292.028,-19463.127 3851.5795,-19631.3768 3983,-19505.5998 4036.0708,-19454.808 4034.3785,-18257.7604 4041,-18184.5998 4054.0027,-18040.9334 4081.6414,-17869.7647 4091.9831,-17808.1467"/>
+<polygon fill="#000000" stroke="#000000" points="4093.7669,-17808.0919 4092.8714,-17802.8707 4090.3155,-17807.5108 4093.7669,-17808.0919"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/blobinfocache/none&#45;&gt;github.com/containers/image/v5/types -->
+<g id="edge106" class="edge">
+<title>github.com/containers/image/v5/pkg/blobinfocache/none&#45;&gt;github.com/containers/image/v5/types</title>
+<path fill="none" stroke="#000000" d="M2295.5294,-10680.6285C2405.4873,-10702.9824 2585.3326,-10747.1282 2727,-10818.5998 2755.8879,-10833.1738 2755.7086,-10848.8548 2785,-10862.5998 2803.0381,-10871.0642 2823.0032,-10877.8089 2842.7543,-10883.1615"/>
+<polygon fill="#000000" stroke="#000000" points="2842.4882,-10884.9014 2847.7698,-10884.4925 2843.386,-10881.5185 2842.4882,-10884.9014"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/blobinfocache/none&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge107" class="edge">
+<title>github.com/containers/image/v5/pkg/blobinfocache/none&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M2359.1467,-10673.1056C2464.3601,-10680.4589 2603.9119,-10691.3273 2727,-10704.5998 2906.2918,-10723.9328 2985.3475,-10650.5894 3129,-10759.5998 3180.6956,-10798.8289 3136.9441,-10852.299 3187,-10893.5998 3304.6121,-10990.6409 3373.7214,-10932.0608 3522,-10967.5998 3547.8926,-10973.8057 3554.0424,-10976.6719 3580,-10982.5998 3612.3443,-10989.9863 3647.7886,-10997.2863 3679.8092,-11003.5873"/>
+<polygon fill="#000000" stroke="#000000" points="3679.6744,-11005.3442 3684.9179,-11004.5899 3680.3485,-11001.9097 3679.6744,-11005.3442"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;encoding/base64 -->
+<g id="edge122" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;encoding/base64</title>
+<path fill="none" stroke="#000000" d="M1529.5775,-15988.7881C1548.449,-16083.0788 1639.0822,-16515.5515 1732,-16610.5998 2388.664,-17282.32 3739.9327,-17000.1237 4037.285,-16927.6718"/>
+<polygon fill="#000000" stroke="#000000" points="4037.9915,-16929.3007 4042.4325,-16926.4128 4037.1599,-16925.9009 4037.9915,-16929.3007"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;encoding/json -->
+<g id="edge123" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M1527.2953,-15988.6831C1534.2668,-16076.1234 1573.1513,-16461.5427 1732,-16722.5998 2062.9156,-17266.4372 2255.7123,-17339.8786 2785,-17693.5998 2948.6778,-17802.9851 2995.012,-17836.0548 3187,-17879.5998 3357.354,-17918.238 3405.3227,-17883.4887 3580,-17884.5998 3759.1075,-17885.7391 3845.8865,-17999.8416 3983,-17884.5998 4032.3001,-17843.1639 4082.5883,-17370.9993 4093.7274,-17260.591"/>
+<polygon fill="#000000" stroke="#000000" points="4095.4692,-17260.7601 4094.2281,-17255.6101 4091.9867,-17260.41 4095.4692,-17260.7601"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;fmt -->
+<g id="edge124" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1526.121,-15952.1931C1528.2689,-15637.5838 1560.3923,-11389.018 1732,-10870.5998 1846.8228,-10523.7259 1830.1155,-10191.5998 2195.5,-10191.5998 2195.5,-10191.5998 2195.5,-10191.5998 2576.5,-10191.5998 2979.3565,-10191.5998 2844.4167,-9733.567 3187,-9521.5998 3318.6055,-9440.1713 3417.5451,-9540.7917 3522,-9426.5998 3615.4273,-9324.4635 3479.7411,-9216.039 3580,-9120.5998 3710.977,-8995.9192 3843.0937,-9179.1707 3983,-9064.5998 4031.3803,-9024.9807 4076.6048,-8832.1019 4091.1479,-8764.8137"/>
+<polygon fill="#000000" stroke="#000000" points="4092.8824,-8765.0714 4092.2215,-8759.8154 4089.4604,-8764.3363 4092.8824,-8765.0714"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;io/ioutil -->
+<g id="edge132" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M1527.109,-15988.7095C1532.8013,-16065.3515 1565.3738,-16366.0774 1732,-16518.5998 1968.1537,-16734.7647 2108.7901,-16668.3134 2426,-16711.5998 2558.5494,-16729.6875 2593.5835,-16721.425 2727,-16711.5998 2797.3483,-16706.4192 3926.2958,-16567.5563 3983,-16525.5998 4031.2628,-16489.8893 4013.9639,-16457.2057 4041,-16403.5998 4055.0881,-16375.6666 4072.1785,-16343.9797 4083.5315,-16323.2165"/>
+<polygon fill="#000000" stroke="#000000" points="4085.1385,-16323.9254 4086.0051,-16318.6993 4082.0686,-16322.2443 4085.1385,-16323.9254"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;strings -->
+<g id="edge135" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M1526.2952,-15952.5325C1532.2242,-15591.8263 1626.6013,-9947.4987 1732,-9622.5998 1972.1394,-8882.3535 2231.8013,-8770.9597 2785,-8223.5998 2928.5826,-8081.5327 3027.4104,-8101.1809 3129,-7926.5998 3195.7661,-7811.8627 3090.6729,-7724.9421 3187,-7633.5998 3296.1421,-7530.1057 3402.5957,-7677.0636 3522,-7585.5998 3573.8416,-7545.8891 3528.0743,-7490.2005 3580,-7450.5998 3723.7885,-7340.9408 3842.7571,-7508.7584 3983,-7394.5998 4030.5845,-7355.8658 4076.1498,-7167.3175 4090.9802,-7100.8488"/>
+<polygon fill="#000000" stroke="#000000" points="4092.702,-7101.1675 4092.0762,-7095.9072 4089.285,-7100.4096 4092.702,-7101.1675"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/containers/image/v5/types -->
+<g id="edge126" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/containers/image/v5/types</title>
+<path fill="none" stroke="#000000" d="M1527.3977,-15952.5127C1537.9473,-15818.1211 1608.0477,-14961.2735 1732,-14274.5998 1966.0416,-12978.051 1484.9064,-12314.6395 2426,-11392.5998 2523.8278,-11296.7527 2633.4027,-11426.5822 2727,-11326.5998 2844.9585,-11200.5943 2667.8404,-11069.3485 2785,-10942.5998 2799.5455,-10926.8638 2818.9703,-10916.7388 2839.5257,-10910.3339"/>
+<polygon fill="#000000" stroke="#000000" points="2840.1919,-10911.9618 2844.4893,-10908.8642 2839.1981,-10908.6058 2840.1919,-10911.9618"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/pkg/errors -->
+<g id="edge130" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M1526.3321,-15952.5528C1532.6953,-15608.7261 1629.5326,-10463.9705 1732,-10169.5998 1963.6182,-9504.2016 2134.1115,-9254.3143 2785,-8984.5998 3110.5682,-8849.6912 3537.1531,-8815.0817 3707.0281,-8806.4128"/>
+<polygon fill="#000000" stroke="#000000" points="3707.319,-8808.1505 3712.2248,-8806.1517 3707.1433,-8804.6549 3707.319,-8808.1505"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge131" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M1528.0912,-15952.4036C1552.9554,-15743.5434 1808.7882,-13786.3616 2785,-12664.5998 2899.5182,-12533.0073 2982.5303,-12570.3501 3129,-12475.5998 3204.4185,-12426.8121 3287.3825,-12360.787 3328.4654,-12327.1839"/>
+<polygon fill="#000000" stroke="#000000" points="3329.7898,-12328.3612 3332.548,-12323.8385 3327.5714,-12325.654 3329.7898,-12328.3612"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;os -->
+<g id="edge133" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M1529.263,-15988.7038C1546.7104,-16084.7001 1631.9417,-16543.6622 1732,-16911.5998 1797.207,-17151.3812 2320.7837,-18812.4852 2426,-19037.5998 2553.4072,-19310.193 2563.5485,-19407.8871 2785,-19611.5998 2986.027,-19796.5241 3782.4912,-20072.0859 3983,-19886.5998 4043.8673,-19830.2928 4030.7868,-19224.8859 4041,-19142.5998 4054.9798,-19029.9673 4080.7143,-18896.652 4091.2816,-18843.824"/>
+<polygon fill="#000000" stroke="#000000" points="4093.0479,-18843.9166 4092.3156,-18838.67 4089.6162,-18843.2281 4093.0479,-18843.9166"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;path/filepath -->
+<g id="edge134" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M1528.0538,-15988.8345C1541.4939,-16106.7859 1619.8474,-16773.7912 1732,-17308.5998 2007.6102,-18622.8695 1744.5982,-19291.5828 2785,-20140.5998 2993.3266,-20310.6041 3781.7262,-20488.8981 3983,-20310.5998 4053.3176,-20248.3091 4088.7485,-19541.0273 4094.9914,-19403.6409"/>
+<polygon fill="#000000" stroke="#000000" points="4096.7405,-19403.7002 4095.2178,-19398.6263 4093.244,-19403.5423 4096.7405,-19403.7002"/>
+</g>
+<!-- github.com/containers/image/v5/internal/pkg/keyctl -->
+<g id="node69" class="node">
+<title>github.com/containers/image/v5/internal/pkg/keyctl</title>
+<g id="a_node69"><a xlink:href="https://godoc.org/github.com/containers/image/v5/internal/pkg/keyctl" xlink:title="github.com/containers/image/v5/internal/pkg/keyctl" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3492,-16050.5998C3492,-16050.5998 3217,-16050.5998 3217,-16050.5998 3211,-16050.5998 3205,-16044.5998 3205,-16038.5998 3205,-16038.5998 3205,-16026.5998 3205,-16026.5998 3205,-16020.5998 3211,-16014.5998 3217,-16014.5998 3217,-16014.5998 3492,-16014.5998 3492,-16014.5998 3498,-16014.5998 3504,-16020.5998 3504,-16026.5998 3504,-16026.5998 3504,-16038.5998 3504,-16038.5998 3504,-16044.5998 3498,-16050.5998 3492,-16050.5998"/>
+<text text-anchor="middle" x="3354.5" y="-16028.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/internal/pkg/keyctl</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/containers/image/v5/internal/pkg/keyctl -->
+<g id="edge125" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/containers/image/v5/internal/pkg/keyctl</title>
+<path fill="none" stroke="#000000" d="M1527.1584,-15988.7618C1532.9545,-16062.8322 1565.5981,-16343.862 1732,-16464.5998 1899.2642,-16585.9633 1988.8447,-16501.5998 2195.5,-16501.5998 2195.5,-16501.5998 2195.5,-16501.5998 2576.5,-16501.5998 2822.6056,-16501.5998 2922.9115,-16599.1193 3129,-16464.5998 3280.5173,-16365.7004 3336.2558,-16130.5425 3350.4987,-16055.8162"/>
+<polygon fill="#000000" stroke="#000000" points="3352.2532,-16055.9539 3351.4532,-16050.7172 3348.813,-16055.3098 3352.2532,-16055.9539"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client -->
+<g id="node83" class="node">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client</title>
+<g id="a_node83"><a xlink:href="https://godoc.org/github.com/docker/docker-credential-helpers/client" xlink:title="github.com/docker/docker&#45;credential&#45;helpers/client" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3490,-15454.5998C3490,-15454.5998 3219,-15454.5998 3219,-15454.5998 3213,-15454.5998 3207,-15448.5998 3207,-15442.5998 3207,-15442.5998 3207,-15430.5998 3207,-15430.5998 3207,-15424.5998 3213,-15418.5998 3219,-15418.5998 3219,-15418.5998 3490,-15418.5998 3490,-15418.5998 3496,-15418.5998 3502,-15424.5998 3502,-15430.5998 3502,-15430.5998 3502,-15442.5998 3502,-15442.5998 3502,-15448.5998 3496,-15454.5998 3490,-15454.5998"/>
+<text text-anchor="middle" x="3354.5" y="-15432.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker&#45;credential&#45;helpers/client</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/client -->
+<g id="edge127" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/client</title>
+<path fill="none" stroke="#000000" d="M1674.2492,-15970.5998C1809.9152,-15970.5998 2016.2112,-15970.5998 2195.5,-15970.5998 2195.5,-15970.5998 2195.5,-15970.5998 2576.5,-15970.5998 2839.8318,-15970.5998 2921.4931,-15918.7249 3129,-15756.5998 3238.913,-15670.7248 3317.5229,-15516.8768 3344.3796,-15459.2324"/>
+<polygon fill="#000000" stroke="#000000" points="3346.0103,-15459.8753 3346.5214,-15454.6026 3342.8338,-15458.4057 3346.0103,-15459.8753"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials -->
+<g id="node84" class="node">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials</title>
+<g id="a_node84"><a xlink:href="https://godoc.org/github.com/docker/docker-credential-helpers/credentials" xlink:title="github.com/docker/docker&#45;credential&#45;helpers/credentials" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3931,-15519.5998C3931,-15519.5998 3632,-15519.5998 3632,-15519.5998 3626,-15519.5998 3620,-15513.5998 3620,-15507.5998 3620,-15507.5998 3620,-15495.5998 3620,-15495.5998 3620,-15489.5998 3626,-15483.5998 3632,-15483.5998 3632,-15483.5998 3931,-15483.5998 3931,-15483.5998 3937,-15483.5998 3943,-15489.5998 3943,-15495.5998 3943,-15495.5998 3943,-15507.5998 3943,-15507.5998 3943,-15513.5998 3937,-15519.5998 3931,-15519.5998"/>
+<text text-anchor="middle" x="3781.5" y="-15497.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker&#45;credential&#45;helpers/credentials</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/credentials -->
+<g id="edge128" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/credentials</title>
+<path fill="none" stroke="#000000" d="M1538.3868,-15988.8136C1599.0007,-16075.5563 1874.4511,-16442.5998 2195.5,-16442.5998 2195.5,-16442.5998 2195.5,-16442.5998 2576.5,-16442.5998 2653.7824,-16442.5998 3587.9955,-15663.5895 3755.7788,-15523.1568"/>
+<polygon fill="#000000" stroke="#000000" points="3757.1193,-15524.3169 3759.8299,-15519.7654 3754.8726,-15521.6331 3757.1193,-15524.3169"/>
+</g>
+<!-- github.com/docker/docker/pkg/homedir -->
+<g id="node85" class="node">
+<title>github.com/docker/docker/pkg/homedir</title>
+<g id="a_node85"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/homedir" xlink:title="github.com/docker/docker/pkg/homedir" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1953,-15546.5998C1953,-15546.5998 1744,-15546.5998 1744,-15546.5998 1738,-15546.5998 1732,-15540.5998 1732,-15534.5998 1732,-15534.5998 1732,-15522.5998 1732,-15522.5998 1732,-15516.5998 1738,-15510.5998 1744,-15510.5998 1744,-15510.5998 1953,-15510.5998 1953,-15510.5998 1959,-15510.5998 1965,-15516.5998 1965,-15522.5998 1965,-15522.5998 1965,-15534.5998 1965,-15534.5998 1965,-15540.5998 1959,-15546.5998 1953,-15546.5998"/>
+<text text-anchor="middle" x="1848.5" y="-15524.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/homedir</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/docker/docker/pkg/homedir -->
+<g id="edge129" class="edge">
+<title>github.com/containers/image/v5/pkg/docker/config&#45;&gt;github.com/docker/docker/pkg/homedir</title>
+<path fill="none" stroke="#000000" d="M1539.2436,-15952.4489C1590.4934,-15882.2089 1775.7401,-15628.3204 1832.1107,-15551.0621"/>
+<polygon fill="#000000" stroke="#000000" points="1833.7905,-15551.7288 1835.3239,-15546.6582 1830.9631,-15549.6658 1833.7905,-15551.7288"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;fmt -->
+<g id="edge137" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2647.6225,-8714.5606C2784.911,-8680.0854 3082.1395,-8607.221 3187,-8595.5998 3363.3977,-8576.0505 3818.2486,-8594.6053 3983,-8660.5998 4017.5939,-8674.4571 4051.5613,-8701.061 4073.0755,-8719.9968"/>
+<polygon fill="#000000" stroke="#000000" points="4072.046,-8721.4231 4076.9451,-8723.4384 4074.3721,-8718.8079 4072.046,-8721.4231"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/BurntSushi/toml -->
+<g id="edge138" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/BurntSushi/toml</title>
+<path fill="none" stroke="#000000" d="M2578.0479,-8714.4861C2597.0003,-8492.7141 2783.6428,-6309.0763 2785,-6307.5998 2889.9799,-6193.399 3021.2927,-6360.232 3129,-6248.5998 3262.8983,-6109.8223 3106.4459,-5993.8111 3187,-5818.5998 3280.4931,-5615.2453 3412.4826,-5637.7919 3522,-5442.5998 3565.4105,-5365.2296 3534.3323,-5327.6595 3580,-5251.5998 3626.3237,-5174.4476 3708.7202,-5105.3401 3752.4176,-5071.8796"/>
+<polygon fill="#000000" stroke="#000000" points="3753.605,-5073.1751 3756.523,-5068.7537 3751.4847,-5070.3904 3753.605,-5073.1751"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;io/ioutil -->
+<g id="edge143" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2577.8559,-8750.6796C2590.3335,-8917.8237 2685.6554,-10211.1973 2727,-11261.5998 2733.5868,-11428.9457 2729.6172,-14117.5467 2785,-14275.5998 2868.0163,-14512.5144 3043.1525,-14487.6963 3129,-14723.5998 3180.0391,-14863.852 3086.822,-15954.9651 3187,-16065.5998 3237.478,-16121.3468 3448.2418,-16098.9207 3522,-16113.5998 3729.4264,-16154.8812 3788.1504,-16149.3592 3983,-16231.5998 4013.1775,-16244.3369 4044.7719,-16264.2736 4066.8553,-16279.4482"/>
+<polygon fill="#000000" stroke="#000000" points="4066.0349,-16281.0086 4071.1419,-16282.416 4068.0272,-16278.131 4066.0349,-16281.0086"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;strings -->
+<g id="edge147" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2605.1108,-8714.441C2700.3191,-8652.1811 3007.3002,-8436.2922 3129,-8167.5998 3198.7009,-8013.7121 3086.8425,-7545.6444 3187,-7409.5998 3281.9739,-7280.596 3382.4635,-7355.2864 3522,-7276.5998 3550.1837,-7260.7066 3550.3943,-7245.6543 3580,-7232.5998 3746.626,-7159.127 3815.3947,-7229.8103 3983,-7158.5998 4017.0896,-7144.1161 4050.912,-7117.893 4072.5394,-7099.2079"/>
+<polygon fill="#000000" stroke="#000000" points="4073.8157,-7100.4169 4076.4331,-7095.8112 4071.5148,-7097.7794 4073.8157,-7100.4169"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;sync -->
+<g id="edge148" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2578.1386,-8714.3887C2590.4632,-8577.2244 2669.7107,-7691.9421 2727,-6971.5998 2754.8408,-6621.5356 2712.6111,-6527.2274 2785,-6183.5998 2879.0317,-5737.2345 3046.1005,-5665.166 3129,-5216.5998 3153.6923,-5082.9908 3101.3244,-2877.0548 3187,-2771.5998 3243.1411,-2702.4978 3913.5562,-2623.8821 3983,-2679.5998 4060.7906,-2742.0146 4088.3866,-3074.1201 4094.5626,-3165.3802"/>
+<polygon fill="#000000" stroke="#000000" points="4092.8279,-3165.6706 4094.9064,-3170.5432 4096.3202,-3165.438 4092.8279,-3165.6706"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/containers/image/v5/docker/reference -->
+<g id="edge139" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/containers/image/v5/docker/reference</title>
+<path fill="none" stroke="#000000" d="M2577.685,-8714.4708C2588.9578,-8544.1897 2679.1855,-7228.7641 2785,-7093.5998 2886.0441,-6964.5292 3018.2224,-7081.4198 3129,-6960.5998 3191.5471,-6892.3826 3130.2829,-6833.736 3187,-6760.5998 3215.6271,-6723.6854 3262.2046,-6697.7636 3298.9851,-6681.7301"/>
+<polygon fill="#000000" stroke="#000000" points="3299.973,-6683.2105 3303.8774,-6679.6302 3298.5925,-6679.9943 3299.973,-6683.2105"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/containers/image/v5/types -->
+<g id="edge140" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/containers/image/v5/types</title>
+<path fill="none" stroke="#000000" d="M2577.678,-8750.8797C2590.8242,-8953.7309 2710.9692,-10778.1821 2785,-10862.5998 2799.2224,-10878.8177 2818.6195,-10889.1424 2839.2725,-10895.5883"/>
+<polygon fill="#000000" stroke="#000000" points="2838.9708,-10897.3239 2844.2619,-10897.0648 2839.9641,-10893.9678 2838.9708,-10897.3239"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/pkg/errors -->
+<g id="edge141" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M2727.3727,-8735.4207C2914.1742,-8739.7337 3241.9654,-8749.9832 3522,-8772.5998 3584.5247,-8777.6495 3655.2001,-8786.2863 3706.9874,-8793.1629"/>
+<polygon fill="#000000" stroke="#000000" points="3707.0678,-8794.939 3712.2552,-8793.865 3707.5302,-8791.4697 3707.0678,-8794.939"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge142" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M2577.3134,-8750.9294C2587.9919,-8990.1723 2701.5672,-11493.091 2785,-11621.5998 2879.4153,-11767.0246 3028.9688,-11663.9793 3129,-11805.5998 3228.3258,-11946.2216 3096.1799,-12042.3404 3187,-12188.5998 3215.0507,-12233.7734 3267.6847,-12266.1609 3306.4931,-12285.2468"/>
+<polygon fill="#000000" stroke="#000000" points="3305.8923,-12286.9004 3311.1547,-12287.5084 3307.42,-12283.7514 3305.8923,-12286.9004"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;os -->
+<g id="edge144" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2577.895,-8750.6781C2590.7258,-8917.8088 2688.6092,-10211.0852 2727,-11261.5998 2735.6419,-11498.075 2734.7417,-15289.3655 2785,-15520.5998 2865.6436,-15891.6342 3046.9237,-15931.8798 3129,-16302.5998 3161.636,-16450.0091 3087.2696,-18906.2488 3187,-19019.5998 3304.166,-19152.7677 3834.535,-19175.6551 3983,-19078.5998 4065.1091,-19024.923 4087.9225,-18896.9389 4093.9508,-18844.0689"/>
+<polygon fill="#000000" stroke="#000000" points="4095.7127,-18844.0536 4094.5142,-18838.8935 4092.2332,-18843.6748 4095.7127,-18844.0536"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;path/filepath -->
+<g id="edge145" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M2577.9034,-8750.6778C2590.8098,-8917.8058 2689.2414,-10211.0623 2727,-11261.5998 2731.6618,-11391.3038 2739.8565,-15811.9161 2785,-15933.5998 2867.7356,-16156.6127 3045.3005,-16120.9468 3129,-16343.5998 3182.8297,-16486.7946 3093.8108,-18975.2814 3187,-19096.5998 3281.4236,-19219.5252 3403.7288,-19093.408 3522,-19193.5998 3567.5038,-19232.1478 3531.6548,-19279.6816 3580,-19314.5998 3598.9332,-19328.2747 3924.2549,-19362.9506 4048.3942,-19375.7535"/>
+<polygon fill="#000000" stroke="#000000" points="4048.2861,-19377.5015 4053.4391,-19376.2732 4048.6448,-19374.0199 4048.2861,-19377.5015"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;regexp -->
+<g id="edge146" class="edge">
+<title>github.com/containers/image/v5/pkg/sysregistriesv2&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M2578.4359,-8714.4091C2592.9383,-8577.3946 2685.1648,-7693.0046 2727,-6971.5998 2775.7825,-6130.3965 2721.4783,-5916.8187 2785,-5076.5998 2785,-5076.5998 3187,-1751.5998 3187,-1751.5998 3317.1343,-1631.7421 3846.1032,-1624.5281 3983,-1736.5998 4068.3526,-1806.4745 4090.6545,-2177.2095 4095.0685,-2274.3066"/>
+<polygon fill="#000000" stroke="#000000" points="4093.3296,-2274.599 4095.2999,-2279.5163 4096.8262,-2274.4435 4093.3296,-2274.599"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;crypto/tls -->
+<g id="edge149" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;crypto/tls</title>
+<path fill="none" stroke="#000000" d="M2579.2997,-16696.7709C2595.9113,-16805.2135 2682.324,-17378.1436 2727,-17848.5998 2803.4743,-18653.9049 2607.8462,-19044.8472 3187,-19609.5998 3316.8768,-19736.2471 3846.6384,-19910.2368 3983,-19790.5998 4057.4666,-19725.2664 4007.0562,-19441.6673 4041,-19348.5998 4049.5652,-19325.1156 4065.3881,-19301.403 4077.8711,-19284.8426"/>
+<polygon fill="#000000" stroke="#000000" points="4079.3216,-19285.8266 4080.9663,-19280.7909 4076.5403,-19283.7018 4079.3216,-19285.8266"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;io/ioutil -->
+<g id="edge154" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2708.0855,-16660.5795C3052.5294,-16613.2215 3956.0283,-16487.7166 3983,-16470.5998 4011.8119,-16452.3152 4061.2217,-16365.3384 4083.935,-16323.365"/>
+<polygon fill="#000000" stroke="#000000" points="4085.6073,-16323.9508 4086.4395,-16318.7192 4082.5264,-16322.29 4085.6073,-16323.9508"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;strings -->
+<g id="edge159" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2578.8411,-16660.5491C2595.2322,-16533.2144 2693.1331,-15756.8559 2727,-15121.5998 2760.7596,-14488.3552 2679.2326,-10039.8611 2785,-9414.5998 2864.8649,-8942.4654 3035.9067,-8863.305 3129,-8393.5998 3166.7994,-8202.8819 3077.1115,-7680.9951 3187,-7520.5998 3279.8272,-7385.1077 3377.5242,-7442.7135 3522,-7364.5998 3548.6885,-7350.1701 3552.2007,-7340.7528 3580,-7328.5998 3750.2253,-7254.1827 3827.6259,-7319.4499 3983,-7217.5998 4029.5598,-7187.0792 4066.062,-7131.1945 4083.9423,-7100.0585"/>
+<polygon fill="#000000" stroke="#000000" points="4085.4982,-7100.8625 4086.4442,-7095.6502 4082.4542,-7099.1348 4085.4982,-7100.8625"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;time -->
+<g id="edge160" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2578.7047,-16660.5412C2594.162,-16533.152 2686.8352,-15756.4889 2727,-15121.5998 2733.7832,-15014.3769 2730.4187,-13281.1397 2785,-13188.5998 3099.4341,-12655.4923 3654.0672,-13002.8856 3983,-12478.5998 4063.8358,-12349.7558 4024.4635,-11947.8009 4041,-11796.5998 4057.177,-11648.6856 4083.0845,-11471.7016 4092.4649,-11409.0085"/>
+<polygon fill="#000000" stroke="#000000" points="4094.2057,-11409.1995 4093.2162,-11403.9953 4090.7444,-11408.6807 4094.2057,-11409.1995"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;github.com/docker/go&#45;connections/tlsconfig -->
+<g id="edge151" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;github.com/docker/go&#45;connections/tlsconfig</title>
+<path fill="none" stroke="#000000" d="M2578.981,-16696.7038C2591.9924,-16788.8174 2656.69,-17213.7317 2785,-17537.5998 2899.0716,-17825.5288 3022.1904,-17852.8986 3129,-18143.5998 3176.588,-18273.1189 3109.644,-18334.3376 3187,-18448.5998 3214.6981,-18489.5125 3263.8093,-18518.1314 3301.7168,-18535.4104"/>
+<polygon fill="#000000" stroke="#000000" points="3301.0039,-18537.0085 3306.2819,-18537.4614 3302.4383,-18533.8159 3301.0039,-18537.0085"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;github.com/pkg/errors -->
+<g id="edge152" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M2578.7991,-16660.5468C2594.9027,-16533.1963 2691.1941,-15756.7496 2727,-15121.5998 2738.149,-14923.8321 2733.8727,-11747.9696 2785,-11556.5998 2866.4387,-11251.7741 3044.689,-11239.6437 3129,-10935.5998 3178.0197,-10758.8239 3072.0325,-9428.5513 3187,-9285.5998 3283.3356,-9165.8154 3417.1311,-9311.9891 3522,-9199.5998 3619.1166,-9095.5188 3489.9001,-8994.811 3580,-8884.5998 3611.365,-8846.2338 3663.6381,-8825.912 3707.0824,-8815.2191"/>
+<polygon fill="#000000" stroke="#000000" points="3707.6098,-8816.8923 3712.0643,-8814.0252 3706.7941,-8813.4887 3707.6098,-8816.8923"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge153" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M2578.7359,-16660.5431C2594.4063,-16533.1672 2688.2732,-15756.5782 2727,-15121.5998 2734.7033,-14995.2935 2719.5508,-12952.9005 2785,-12844.5998 2875.5599,-12694.7476 3015.5691,-12785.9798 3129,-12652.5998 3178.0021,-12594.9797 3148.1691,-12557.5109 3187,-12492.5998 3227.3704,-12425.1151 3294.0373,-12359.8674 3329.8808,-12327.2249"/>
+<polygon fill="#000000" stroke="#000000" points="3331.1434,-12328.4424 3333.6733,-12323.7881 3328.7932,-12325.8488 3331.1434,-12328.4424"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;net/http -->
+<g id="edge156" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M2578.8713,-16660.5507C2595.469,-16533.2267 2694.5272,-15756.9287 2727,-15121.5998 2840.9462,-12892.2505 2657.9615,-7305.2414 2785,-5076.5998 2790.8898,-4973.2745 3142.2869,-1465.9355 3187,-1372.5998 3277.9706,-1182.7047 3427.9631,-1225.9953 3522,-1037.5998 3604.1912,-872.9364 3446.6735,-754.4599 3580,-627.5998 3649.1328,-561.82 3958.1035,-572.7911 4061.8795,-578.4764"/>
+<polygon fill="#000000" stroke="#000000" points="4061.868,-580.2284 4066.9577,-578.7594 4062.0628,-576.7338 4061.868,-580.2284"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;os -->
+<g id="edge157" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2578.969,-16696.9047C2600.2535,-16854.5467 2752.7436,-17980.7541 2785,-18128.5998 2919.628,-18745.6604 2795.5006,-18996.0025 3187,-19491.5998 3293.3765,-19626.2613 3381.9279,-19584.4561 3522,-19683.5998 3549.7796,-19703.2624 3548.0967,-19721.7466 3580,-19733.5998 3747.8974,-19795.9796 3850.3084,-19853.9067 3983,-19733.5998 4031.8818,-19689.2804 4032.2175,-19207.9948 4041,-19142.5998 4056.1069,-19030.1129 4081.2062,-18896.7156 4091.4427,-18843.8448"/>
+<polygon fill="#000000" stroke="#000000" points="4093.2091,-18843.9285 4092.4438,-18838.6866 4089.7732,-18843.2616 4093.2091,-18843.9285"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;path/filepath -->
+<g id="edge158" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M2579.4378,-16696.7585C2596.845,-16805.1296 2687.0571,-17377.7181 2727,-17848.5998 2740.1873,-18004.0635 2727.2402,-19106.663 2785,-19251.5998 2947.0132,-19658.14 3147.162,-19767.9901 3580,-19832.5998 3757.1484,-19859.0428 3841.8236,-19942.827 3983,-19832.5998 4052.5518,-19778.2954 4085.6291,-19488.2365 4093.8841,-19403.7225"/>
+<polygon fill="#000000" stroke="#000000" points="4095.6314,-19403.8334 4094.37,-19398.6884 4092.1476,-19403.4971 4095.6314,-19403.8334"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets -->
+<g id="node86" class="node">
+<title>github.com/docker/go&#45;connections/sockets</title>
+<g id="a_node86"><a xlink:href="https://godoc.org/github.com/docker/go-connections/sockets" xlink:title="github.com/docker/go&#45;connections/sockets" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3069.5,-9464.5998C3069.5,-9464.5998 2844.5,-9464.5998 2844.5,-9464.5998 2838.5,-9464.5998 2832.5,-9458.5998 2832.5,-9452.5998 2832.5,-9452.5998 2832.5,-9440.5998 2832.5,-9440.5998 2832.5,-9434.5998 2838.5,-9428.5998 2844.5,-9428.5998 2844.5,-9428.5998 3069.5,-9428.5998 3069.5,-9428.5998 3075.5,-9428.5998 3081.5,-9434.5998 3081.5,-9440.5998 3081.5,-9440.5998 3081.5,-9452.5998 3081.5,-9452.5998 3081.5,-9458.5998 3075.5,-9464.5998 3069.5,-9464.5998"/>
+<text text-anchor="middle" x="2957" y="-9442.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go&#45;connections/sockets</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;github.com/docker/go&#45;connections/sockets -->
+<g id="edge150" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;github.com/docker/go&#45;connections/sockets</title>
+<path fill="none" stroke="#000000" d="M2578.8172,-16660.5478C2595.0443,-16533.2042 2692.0277,-15756.796 2727,-15121.5998 2830.8743,-13234.948 2658.6416,-12755.8792 2785,-10870.5998 2823.3116,-10298.9864 2931.6742,-9604.7148 2953.2403,-9469.8904"/>
+<polygon fill="#000000" stroke="#000000" points="2955.0235,-9469.8221 2954.0866,-9464.6082 2951.5676,-9469.2683 2955.0235,-9469.8221"/>
+</g>
+<!-- net -->
+<g id="node87" class="node">
+<title>net</title>
+<g id="a_node87"><a xlink:href="https://godoc.org/net" xlink:title="net" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-17190.5998C4111,-17190.5998 4081,-17190.5998 4081,-17190.5998 4075,-17190.5998 4069,-17184.5998 4069,-17178.5998 4069,-17178.5998 4069,-17166.5998 4069,-17166.5998 4069,-17160.5998 4075,-17154.5998 4081,-17154.5998 4081,-17154.5998 4111,-17154.5998 4111,-17154.5998 4117,-17154.5998 4123,-17160.5998 4123,-17166.5998 4123,-17166.5998 4123,-17178.5998 4123,-17178.5998 4123,-17184.5998 4117,-17190.5998 4111,-17190.5998"/>
+<text text-anchor="middle" x="4096" y="-17168.8998" font-family="Times,serif" font-size="14.00" fill="#000000">net</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;net -->
+<g id="edge155" class="edge">
+<title>github.com/containers/image/v5/pkg/tlsclientconfig&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M2603.2012,-16696.8037C2715.5387,-16772.1851 3163.2476,-17060.5923 3580,-17160.5998 3758.4104,-17203.4127 3981.5207,-17185.4168 4063.6951,-17176.4965"/>
+<polygon fill="#000000" stroke="#000000" points="4064.1238,-17178.2099 4068.902,-17175.9228 4063.7404,-17174.731 4064.1238,-17178.2099"/>
+</g>
+<!-- github.com/containers/image/v5/transports&#45;&gt;fmt -->
+<g id="edge161" class="edge">
+<title>github.com/containers/image/v5/transports&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2577.4411,-2552.0322C2592.2816,-2842.616 2777.7513,-6471.5908 2785,-6482.5998 2878.7846,-6625.0361 3035.5892,-6509.9181 3129,-6652.5998 3277.6655,-6879.6811 3024.0823,-7655.5165 3187,-7872.5998 3281.4895,-7998.5045 3405.3851,-7881.8586 3522,-7987.5998 3567.9683,-8029.2818 3530.2368,-8077.531 3580,-8114.5998 3725.0196,-8222.6256 3851.5219,-8046.4476 3983,-8170.5998 4064.5752,-8247.6297 4089.6527,-8620.6309 4094.8692,-8718.2032"/>
+<polygon fill="#000000" stroke="#000000" points="4093.1342,-8718.5366 4095.144,-8723.4379 4096.6294,-8718.353 4093.1342,-8718.5366"/>
+</g>
+<!-- github.com/containers/image/v5/transports&#45;&gt;sort -->
+<g id="edge163" class="edge">
+<title>github.com/containers/image/v5/transports&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M2701.5524,-2531.2936C2924.0596,-2528.932 3378.7986,-2532.4718 3522,-2596.5998 3783.8822,-2713.8751 3866.2828,-2786.4684 3983,-3048.5998 4044.6125,-3186.9733 3962.7075,-4280.9326 4041,-4410.5998 4046.3978,-4419.5396 4055.3087,-4426.2405 4064.4022,-4431.137"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6151,-4432.7 4068.8664,-4433.3977 4065.1964,-4429.5775 4063.6151,-4432.7"/>
+</g>
+<!-- github.com/containers/image/v5/transports&#45;&gt;sync -->
+<g id="edge164" class="edge">
+<title>github.com/containers/image/v5/transports&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2701.6876,-2517.245C3028.1735,-2475.9302 3887.0058,-2376.3217 3983,-2458.5998 3996.8972,-2470.5113 4075.914,-3042.0235 4092.8176,-3165.3169"/>
+<polygon fill="#000000" stroke="#000000" points="4091.1172,-3165.799 4093.5299,-3170.5151 4094.5848,-3165.3238 4091.1172,-3165.799"/>
+</g>
+<!-- github.com/containers/image/v5/transports&#45;&gt;github.com/containers/image/v5/types -->
+<g id="edge162" class="edge">
+<title>github.com/containers/image/v5/transports&#45;&gt;github.com/containers/image/v5/types</title>
+<path fill="none" stroke="#000000" d="M2577.6904,-2551.8088C2589.5886,-2734.6481 2687.5006,-4258.6014 2727,-5494.5998 2758.4045,-6477.2937 2693.6172,-8939.6602 2785,-9918.5998 2821.1803,-10306.1824 2926.1446,-10770.5776 2951.4587,-10879.1221"/>
+<polygon fill="#000000" stroke="#000000" points="2949.8299,-10879.8425 2952.672,-10884.313 2953.238,-10879.0459 2949.8299,-10879.8425"/>
+</g>
+<!-- github.com/containers/image/v5/types&#45;&gt;context -->
+<g id="edge165" class="edge">
+<title>github.com/containers/image/v5/types&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M2960.0077,-10884.216C2979.7809,-10762.3579 3091.5073,-10058.1993 3129,-9479.5998 3174.6471,-8775.1589 3076.853,-3823.8719 3187,-3126.5998 3264.5559,-2635.6419 3431.4081,-2548.3203 3522,-2059.5998 3540.85,-1957.9089 3518.5487,-1213.7869 3580,-1130.5998 3693.7412,-976.6275 3804.251,-1047.1078 3983,-978.5998 4009.9789,-968.2598 4040.5067,-956.3609 4063.0249,-947.5462"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6868,-949.1665 4067.7044,-945.7137 4062.4105,-945.9075 4063.6868,-949.1665"/>
+</g>
+<!-- github.com/containers/image/v5/types&#45;&gt;io -->
+<g id="edge170" class="edge">
+<title>github.com/containers/image/v5/types&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2963.4613,-10920.7432C2990.2226,-10996.91 3092.6661,-11298.8072 3129,-11556.5998 3148.668,-11696.1463 3094.7115,-13986.0968 3187,-14092.5998 3285.8563,-14206.6822 3388.9625,-14077.2669 3522,-14148.5998 3555.2546,-14166.4305 3545.3844,-14195.5811 3580,-14210.5998 3662.1561,-14246.2449 3913.8538,-14267.512 3983,-14210.5998 4086.5062,-14125.4069 3963.9904,-14024.3308 4041,-13914.5998 4046.7751,-13906.3708 4055.3173,-13899.8417 4063.9565,-13894.837"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2044,-13896.1489 4068.7492,-13892.2123 4063.5231,-13893.0791 4065.2044,-13896.1489"/>
+</g>
+<!-- github.com/containers/image/v5/types&#45;&gt;time -->
+<g id="edge171" class="edge">
+<title>github.com/containers/image/v5/types&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2978.5208,-10920.6339C3017.4046,-10952.1666 3103.1406,-11016.9695 3187,-11048.5998 3522.0708,-11174.9828 3710.8194,-10940.8707 3983,-11173.5998 4046.8818,-11228.2222 3988.2945,-11288.1275 4041,-11353.5998 4047.1372,-11361.2236 4055.6136,-11367.4031 4064.0752,-11372.225"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5299,-11373.92 4068.7596,-11374.7646 4065.1981,-11370.8431 4063.5299,-11373.92"/>
+</g>
+<!-- github.com/containers/image/v5/types&#45;&gt;github.com/containers/image/v5/docker/reference -->
+<g id="edge166" class="edge">
+<title>github.com/containers/image/v5/types&#45;&gt;github.com/containers/image/v5/docker/reference</title>
+<path fill="none" stroke="#000000" d="M2959.9155,-10884.2097C2979.0966,-10762.3113 3087.6992,-10057.9399 3129,-9479.5998 3149.591,-9191.2623 3090.6447,-7151.1401 3187,-6878.5998 3216.3105,-6795.6952 3290.5656,-6719.4233 3329.6195,-6683.3966"/>
+<polygon fill="#000000" stroke="#000000" points="3331.2381,-6684.2874 3333.7444,-6679.6204 3328.8748,-6681.7058 3331.2381,-6684.2874"/>
+</g>
+<!-- github.com/containers/image/v5/types&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge168" class="edge">
+<title>github.com/containers/image/v5/types&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M2984.9541,-10920.6944C3026.8583,-10946.6625 3109.3512,-10993.3754 3187,-11011.5998 3350.5555,-11049.9869 3546.4209,-11044.0127 3667.4169,-11034.5661"/>
+<polygon fill="#000000" stroke="#000000" points="3667.5587,-11036.3105 3672.405,-11034.1714 3667.2825,-11032.8214 3667.5587,-11036.3105"/>
+</g>
+<!-- github.com/containers/image/v5/types&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge169" class="edge">
+<title>github.com/containers/image/v5/types&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M2978.4669,-10920.7377C3011.8495,-10949.2868 3077.6979,-11006.9893 3129,-11060.5998 3156.6979,-11089.544 3152.9116,-11108.5541 3187,-11129.5998 3200.7176,-11138.0689 3216.0877,-11144.7449 3231.8057,-11150.0074"/>
+<polygon fill="#000000" stroke="#000000" points="3231.3973,-11151.7147 3236.6934,-11151.5956 3232.479,-11148.386 3231.3973,-11151.7147"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression/types -->
+<g id="node79" class="node">
+<title>github.com/containers/image/v5/pkg/compression/types</title>
+<g id="a_node79"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/compression/types" xlink:title="github.com/containers/image/v5/pkg/compression/types" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3503.5,-14445.5998C3503.5,-14445.5998 3205.5,-14445.5998 3205.5,-14445.5998 3199.5,-14445.5998 3193.5,-14439.5998 3193.5,-14433.5998 3193.5,-14433.5998 3193.5,-14421.5998 3193.5,-14421.5998 3193.5,-14415.5998 3199.5,-14409.5998 3205.5,-14409.5998 3205.5,-14409.5998 3503.5,-14409.5998 3503.5,-14409.5998 3509.5,-14409.5998 3515.5,-14415.5998 3515.5,-14421.5998 3515.5,-14421.5998 3515.5,-14433.5998 3515.5,-14433.5998 3515.5,-14439.5998 3509.5,-14445.5998 3503.5,-14445.5998"/>
+<text text-anchor="middle" x="3354.5" y="-14423.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/compression/types</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/types&#45;&gt;github.com/containers/image/v5/pkg/compression/types -->
+<g id="edge167" class="edge">
+<title>github.com/containers/image/v5/types&#45;&gt;github.com/containers/image/v5/pkg/compression/types</title>
+<path fill="none" stroke="#000000" d="M2963.4649,-10920.7427C2990.2405,-10996.9074 3092.7331,-11298.7978 3129,-11556.5998 3169.1765,-11842.1931 3106.2154,-13874.7398 3187,-14151.5998 3217.5566,-14256.3214 3298.465,-14361.338 3335.5031,-14405.6727"/>
+<polygon fill="#000000" stroke="#000000" points="3334.1783,-14406.8164 3338.7342,-14409.5195 3336.8583,-14404.5653 3334.1783,-14406.8164"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;encoding/json -->
+<g id="edge227" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3824.8594,-4519.6046C3873.5276,-4542.4657 3949.9912,-4586.9942 3983,-4652.5998 4061.3623,-4808.3463 3953.7291,-17054.6647 4041,-17205.5998 4043.2654,-17209.5179 4046.2144,-17213.0015 4049.5701,-17216.0893"/>
+<polygon fill="#000000" stroke="#000000" points="4048.7236,-17217.6633 4053.6796,-17219.5346 4050.9723,-17214.9812 4048.7236,-17217.6633"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;fmt -->
+<g id="edge228" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3824.7124,-4519.6791C3873.2425,-4542.6103 3949.5764,-4587.2045 3983,-4652.5998 4025.4195,-4735.5961 4036.7103,-7914.4902 4041,-8007.5998 4053.9165,-8287.9564 4085.1511,-8627.5027 4093.7666,-8718.3346"/>
+<polygon fill="#000000" stroke="#000000" points="4092.0388,-8718.6523 4094.2542,-8723.4643 4095.5231,-8718.3211 4092.0388,-8718.6523"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;sort -->
+<g id="edge230" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3877.5693,-4483.5773C3941.1016,-4471.6587 4020.4192,-4456.7787 4063.8574,-4448.6298"/>
+<polygon fill="#000000" stroke="#000000" points="4064.3818,-4450.312 4068.9733,-4447.67 4063.7364,-4446.872 4064.3818,-4450.312"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;strings -->
+<g id="edge231" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3824.6021,-4519.7358C3873.0287,-4542.7202 3949.2654,-4587.3644 3983,-4652.5998 4038.217,-4759.3776 4026.9838,-6696.2099 4041,-6815.5998 4051.4883,-6904.9394 4077.554,-7008.9442 4089.6487,-7054.3764"/>
+<polygon fill="#000000" stroke="#000000" points="4088.0002,-7054.9863 4090.9831,-7059.3641 4091.3813,-7054.0816 4088.0002,-7054.9863"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;sync -->
+<g id="edge232" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3869.0419,-4483.594C3910.3127,-4470.4434 3956.2589,-4448.1042 3983,-4410.5998 3987.5408,-4404.2314 4078.9266,-3380.2631 4093.9255,-3211.9012"/>
+<polygon fill="#000000" stroke="#000000" points="4095.6862,-3211.8572 4094.3868,-3206.7216 4092.2,-3211.5466 4095.6862,-3211.8572"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;net/http -->
+<g id="edge229" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M3873.1245,-4483.5233C3913.9314,-4470.4545 3958.2767,-4448.207 3983,-4410.5998 4036.5369,-4329.1638 4031.9637,-999.6378 4041,-902.5998 4051.5234,-789.5919 4079.2055,-656.4882 4090.7877,-603.7704"/>
+<polygon fill="#000000" stroke="#000000" points="4092.554,-603.8871 4091.9227,-598.6274 4089.1362,-603.1327 4092.554,-603.8871"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;fmt -->
+<g id="edge233" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2591.4474,-2256.8922C2623.7418,-2297.7161 2699.3417,-2400.6314 2727,-2501.5998 2755.4117,-2605.3186 2720.5959,-6286.4782 2785,-6372.5998 2880.8901,-6500.8247 3031.2312,-6352.8016 3129,-6479.5998 3217.5595,-6594.4543 3095.5819,-7671.0074 3187,-7783.5998 3283.8922,-7902.9344 3407.7689,-7766.7403 3522,-7869.5998 3576.9694,-7919.097 3524.0265,-7977.241 3580,-8025.5998 3718.5855,-8145.332 3855.6367,-7979.9932 3983,-8111.5998 4069.4514,-8200.9315 4091.104,-8615.3626 4095.1877,-8718.3729"/>
+<polygon fill="#000000" stroke="#000000" points="4093.4479,-8718.6711 4095.3903,-8723.5995 4096.9452,-8718.5354 4093.4479,-8718.6711"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;strings -->
+<g id="edge241" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2590.5798,-2256.7712C2621.4737,-2297.7594 2694.9952,-2401.6476 2727,-2501.5998 2805.7316,-2747.4813 2750.1754,-2823.7803 2785,-3079.5998 2906.4815,-3971.9967 2982.0706,-4188.0383 3129,-5076.5998 3155.8688,-5239.0898 3088.3908,-5310.6864 3187,-5442.5998 3302.8141,-5597.5289 3403.3436,-5550.8063 3580,-5629.5998 3757.2221,-5708.6456 3870.8233,-5639.2575 3983,-5797.5998 4048.4931,-5890.0461 4026.0979,-6703.2896 4041,-6815.5998 4052.8319,-6904.7714 4078.2096,-7008.8622 4089.8886,-7054.3464"/>
+<polygon fill="#000000" stroke="#000000" points="4088.2332,-7054.9353 4091.1763,-7059.3399 4091.6223,-7054.0613 4088.2332,-7054.9353"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;unicode -->
+<g id="edge242" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;unicode</title>
+<path fill="none" stroke="#000000" d="M2710.6805,-2225.2988C3028.2118,-2197.094 3811.908,-2147.7011 3983,-2327.5998 4042.3466,-2390.0011 4030.4722,-3791.1299 4041,-3876.5998 4051.9567,-3965.5519 4077.7825,-4069.1836 4089.7323,-4114.4568"/>
+<polygon fill="#000000" stroke="#000000" points="4088.0771,-4115.0427 4091.0505,-4119.427 4091.4602,-4114.1454 4088.0771,-4115.0427"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/docker/distribution/registry/api/errcode -->
+<g id="edge235" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/docker/distribution/registry/api/errcode</title>
+<path fill="none" stroke="#000000" d="M2607.2313,-2256.7573C2641.2329,-2278.3576 2695.387,-2317.4855 2727,-2364.5998 3102.5269,-2924.2662 2908.9074,-3195.6687 3187,-3809.5998 3195.7254,-3828.8625 3562.6933,-4449.4482 3580,-4461.5998 3594.7409,-4471.9499 3611.5076,-4479.7731 3628.9271,-4485.6635"/>
+<polygon fill="#000000" stroke="#000000" points="3628.5602,-4487.3846 3633.8564,-4487.2742 3629.6473,-4484.0577 3628.5602,-4487.3846"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge237" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M2591.4975,-2256.8785C2623.8918,-2297.6752 2699.6786,-2400.5397 2727,-2501.5998 2828.1762,-2875.8447 2676.5148,-9107.4079 2785,-9479.5998 2865.6809,-9756.4006 3014.3611,-9765.0511 3129,-10029.5998 3169.035,-10121.9874 3123.3111,-10170.6127 3187,-10248.5998 3292.105,-10377.301 3422.7866,-10281.304 3522,-10414.5998 3613.528,-10537.5701 3519.3418,-10613.8175 3580,-10754.5998 3624.4954,-10857.8697 3716.5702,-10957.8836 3759.0859,-11000.7559"/>
+<polygon fill="#000000" stroke="#000000" points="3758.0282,-11002.1737 3762.7973,-11004.48 3760.5074,-10999.7031 3758.0282,-11002.1737"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;net/http -->
+<g id="edge238" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M2579.0422,-2220.511C2591.2344,-2137.7827 2648.8313,-1788.8099 2785,-1539.5998 2897.4304,-1333.8345 3025.4795,-1349.9886 3129,-1139.5998 3185.1078,-1025.5699 3122.1995,-968.9241 3187,-859.5998 3286.674,-691.4408 3387.037,-716.0117 3522,-574.5998 3551.0928,-544.1168 3542.1726,-518.1657 3580,-499.5998 3740.7889,-420.6839 3812.4791,-444.7966 3983,-499.5998 4019.1279,-511.2109 4053.4993,-539.1094 4074.6462,-558.9114"/>
+<polygon fill="#000000" stroke="#000000" points="4073.6064,-560.3373 4078.4383,-562.5089 4076.0153,-557.7981 4073.6064,-560.3373"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;net/url -->
+<g id="edge239" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M2582.9866,-2220.582C2604.7289,-2161.8497 2679.6343,-1972.3744 2785,-1844.5998 2930.9928,-1667.5577 2976.314,-1611.535 3187,-1520.5998 3512.5607,-1380.083 3638.3298,-1383.31 3983,-1466.5998 4012.7402,-1473.7866 4043.4369,-1490.2208 4065.3169,-1503.7673"/>
+<polygon fill="#000000" stroke="#000000" points="4064.4064,-1505.2619 4069.5724,-1506.4345 4066.2651,-1502.2962 4064.4064,-1505.2619"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;regexp -->
+<g id="edge240" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M2617.3121,-2220.5613C2718.6201,-2173.7844 2983.8677,-2038.7635 3129,-1847.5998 3175.2433,-1786.6896 3124.8085,-1730.1049 3187,-1685.5998 3474.7082,-1479.7119 3711.2659,-1453.0465 3983,-1679.5998 4076.7206,-1757.7376 4092.9357,-2171.3752 4095.5339,-2274.3632"/>
+<polygon fill="#000000" stroke="#000000" points="4093.7898,-2274.6333 4095.6606,-2279.5894 4097.2888,-2274.5484 4093.7898,-2274.6333"/>
+</g>
+<!-- github.com/docker/distribution/reference -->
+<g id="node90" class="node">
+<title>github.com/docker/distribution/reference</title>
+<g id="a_node90"><a xlink:href="https://godoc.org/github.com/docker/distribution/reference" xlink:title="github.com/docker/distribution/reference" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3064.5,-5126.5998C3064.5,-5126.5998 2849.5,-5126.5998 2849.5,-5126.5998 2843.5,-5126.5998 2837.5,-5120.5998 2837.5,-5114.5998 2837.5,-5114.5998 2837.5,-5102.5998 2837.5,-5102.5998 2837.5,-5096.5998 2843.5,-5090.5998 2849.5,-5090.5998 2849.5,-5090.5998 3064.5,-5090.5998 3064.5,-5090.5998 3070.5,-5090.5998 3076.5,-5096.5998 3076.5,-5102.5998 3076.5,-5102.5998 3076.5,-5114.5998 3076.5,-5114.5998 3076.5,-5120.5998 3070.5,-5126.5998 3064.5,-5126.5998"/>
+<text text-anchor="middle" x="2957" y="-5104.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/reference</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/docker/distribution/reference -->
+<g id="edge234" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/docker/distribution/reference</title>
+<path fill="none" stroke="#000000" d="M2591.1269,-2256.6072C2623.1263,-2297.264 2698.7391,-2400.5252 2727,-2501.5998 2862.9988,-2987.998 2660.0824,-4284.2385 2785,-4773.5998 2817.4784,-4900.8332 2904.8712,-5034.6327 2941.0156,-5086.3736"/>
+<polygon fill="#000000" stroke="#000000" points="2939.6353,-5087.4533 2943.9401,-5090.5408 2942.5003,-5085.4428 2939.6353,-5087.4533"/>
+</g>
+<!-- github.com/gorilla/mux -->
+<g id="node94" class="node">
+<title>github.com/gorilla/mux</title>
+<g id="a_node94"><a xlink:href="https://godoc.org/github.com/gorilla/mux" xlink:title="github.com/gorilla/mux" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3842.5,-2377.5998C3842.5,-2377.5998 3720.5,-2377.5998 3720.5,-2377.5998 3714.5,-2377.5998 3708.5,-2371.5998 3708.5,-2365.5998 3708.5,-2365.5998 3708.5,-2353.5998 3708.5,-2353.5998 3708.5,-2347.5998 3714.5,-2341.5998 3720.5,-2341.5998 3720.5,-2341.5998 3842.5,-2341.5998 3842.5,-2341.5998 3848.5,-2341.5998 3854.5,-2347.5998 3854.5,-2353.5998 3854.5,-2353.5998 3854.5,-2365.5998 3854.5,-2365.5998 3854.5,-2371.5998 3848.5,-2377.5998 3842.5,-2377.5998"/>
+<text text-anchor="middle" x="3781.5" y="-2355.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/gorilla/mux</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/gorilla/mux -->
+<g id="edge236" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/gorilla/mux</title>
+<path fill="none" stroke="#000000" d="M2710.6402,-2252.0695C2963.1111,-2277.4213 3501.6091,-2331.4946 3703.1787,-2351.7352"/>
+<polygon fill="#000000" stroke="#000000" points="3703.0192,-2353.4779 3708.1691,-2352.2363 3703.369,-2349.9954 3703.0192,-2353.4779"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;bytes -->
+<g id="edge243" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M406.3258,-5303.9314C410.5576,-5536.0898 456.8927,-7906.7662 596,-8592.5998 1076.9464,-10963.7859 1537.6204,-11469.13 2426,-13719.5998 2444.4095,-13766.2353 2745.4969,-14516.7244 2785,-14547.5998 2890.0613,-14629.7152 3885.2297,-14709.2738 3983,-14618.5998 4045.8271,-14560.3329 4022.3915,-13933.242 4041,-13849.5998 4050.949,-13804.8808 4071.9708,-13755.5492 4084.9504,-13727.4933"/>
+<polygon fill="#000000" stroke="#000000" points="4086.5919,-13728.1138 4087.1186,-13722.8427 4083.4197,-13726.6349 4086.5919,-13728.1138"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;context -->
+<g id="edge244" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M406.4957,-5267.4235C412.6123,-5046.9295 475.3566,-2890.1154 596,-2637.5998 776.5988,-2259.5932 2548.5253,-855.9745 2785,-747.5998 3269.6817,-525.4737 3537.0278,-393.4265 3983,-685.5998 4062.2773,-737.5374 4086.6287,-859.9035 4093.5083,-911.2623"/>
+<polygon fill="#000000" stroke="#000000" points="4091.7822,-911.5614 4094.1568,-916.2969 4095.2536,-911.1142 4091.7822,-911.5614"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;encoding/json -->
+<g id="edge245" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M406.2673,-5303.846C411.0805,-5629.5194 480.6531,-10221.7673 596,-11562.5998 708.4685,-12869.9729 787.2363,-13192.7909 1012,-14485.5998 1150.2678,-15280.8961 1204.5263,-15476.2337 1378,-16264.5998 1526.1327,-16937.8016 1212.3501,-17321.7115 1732,-17774.5998 2110.0202,-18104.0542 3601.0001,-18256.4315 3983,-17931.5998 4035.6945,-17886.7913 4083.6583,-17377.3248 4093.9629,-17261.104"/>
+<polygon fill="#000000" stroke="#000000" points="4095.7283,-17261.0049 4094.425,-17255.8703 4092.2419,-17260.697 4095.7283,-17261.0049"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;errors -->
+<g id="edge246" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M417.889,-5267.4543C444.4829,-5228.5803 513.0042,-5137.5561 596,-5097.5998 763.7405,-5016.8453 827.0403,-5069.7682 1012,-5048.5998 1566.2025,-4985.1722 3057.2669,-4709.0765 3522,-5017.5998 3570.3534,-5049.7003 3535.0554,-5097.8782 3580,-5134.5998 3723.5073,-5251.8515 3864.7196,-5098.9392 3983,-5241.5998 4084.2492,-5363.7187 4094.8665,-6575.8128 4095.8956,-6759.3623"/>
+<polygon fill="#000000" stroke="#000000" points="4094.1462,-6759.5002 4095.9233,-6764.4906 4097.6462,-6759.4812 4094.1462,-6759.5002"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;fmt -->
+<g id="edge247" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M406.3746,-5303.7462C412.0702,-5556.9815 490.6344,-8375.5998 1166,-8375.5998 1166,-8375.5998 1166,-8375.5998 2195.5,-8375.5998 2363.627,-8375.5998 3479.2745,-8472.8452 3522,-8477.5998 3727.6461,-8500.4847 3811.9008,-8426.2422 3983,-8542.5998 4045.9813,-8585.4309 4078.0035,-8675.7095 4090.1154,-8718.354"/>
+<polygon fill="#000000" stroke="#000000" points="4088.4685,-8718.9639 4091.4945,-8723.312 4091.8405,-8718.0259 4088.4685,-8718.9639"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;io -->
+<g id="edge257" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M406.3638,-5303.8437C411.5834,-5561.9929 473.05,-8485.8255 596,-9338.5998 786.735,-10661.5275 1054.2037,-14260.7572 2023,-15181.5998 2181.023,-15331.801 3831.0749,-15429.9661 3983,-15273.5998 4088.3199,-15165.2012 3963.7471,-14044.5018 4041,-13914.5998 4046.3806,-13905.5523 4055.287,-13898.6869 4064.3816,-13893.6257"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2313,-13895.1563 4068.8468,-13891.2844 4063.6059,-13892.0565 4065.2313,-13895.1563"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;io/ioutil -->
+<g id="edge258" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M406.2001,-5303.9971C409.5532,-5606.2368 455.7209,-9546.0087 596,-10693.5998 689.4221,-11457.864 842.6581,-11626.74 954,-12388.5998 999.4638,-12699.6871 979.4871,-12780.8936 1012,-13093.5998 1146.8041,-14390.1334 914.3398,-14785.3261 1378,-16003.5998 1476.7943,-16263.1833 1489.1398,-16383.8303 1732,-16518.5998 2003.4504,-16669.2348 2819.501,-16545.8185 3129,-16521.5998 3176.7879,-16517.8604 3939.4044,-16418.527 3983,-16398.5998 4021.6242,-16380.945 4057.0152,-16345.8534 4077.5414,-16322.8079"/>
+<polygon fill="#000000" stroke="#000000" points="4078.961,-16323.8439 4080.9523,-16318.9349 4076.3344,-16321.5306 4078.961,-16323.8439"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;strconv -->
+<g id="edge261" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M409.8056,-5267.5307C428.0354,-5182.7838 509.1356,-4826.0269 596,-4759.5998 745.5451,-4645.2394 3840.6484,-4529.4005 3983,-4652.5998 4050.9155,-4711.3778 4026.4284,-4963.9712 4041,-5052.5998 4057.9367,-5155.6132 4081.5344,-5277.934 4091.3896,-5328.2329"/>
+<polygon fill="#000000" stroke="#000000" points="4089.7313,-5328.8703 4092.4112,-5333.4399 4093.1658,-5328.1965 4089.7313,-5328.8703"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;strings -->
+<g id="edge262" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M418.4444,-5303.6207C530.509,-5463.9721 1385.7377,-6647.8925 2426,-6971.5998 2523.953,-7002.0807 3212.4127,-6989.1993 3522,-6992.5998 3726.9001,-6994.8505 3787.8345,-6937.1533 3983,-6999.5998 4018.2666,-7010.884 4052.3387,-7037.1913 4073.6819,-7056.1363"/>
+<polygon fill="#000000" stroke="#000000" points="4072.6279,-7057.5419 4077.5164,-7059.5827 4074.9676,-7054.9388 4072.6279,-7057.5419"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;time -->
+<g id="edge263" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M406.06,-5303.6177C406.9982,-5546.8314 421.1666,-8175.1095 596,-8463.5998 692.1929,-8622.3264 837.158,-8534.3945 954,-8678.5998 1621.9613,-9502.9916 1164.1197,-10039.3264 1732,-10935.5998 1847.8733,-11118.4802 2527.6205,-11709.5998 2576.5,-11709.5998 2576.5,-11709.5998 2576.5,-11709.5998 2957,-11709.5998 3015.1595,-11709.5998 3932.1592,-11529.8441 3983,-11501.5998 4024.9919,-11478.2715 4061.2671,-11434.4107 4080.7512,-11407.8165"/>
+<polygon fill="#000000" stroke="#000000" points="4082.2156,-11408.778 4083.7324,-11403.7023 4079.3815,-11406.7243 4082.2156,-11408.778"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/api/errcode -->
+<g id="edge250" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/api/errcode</title>
+<path fill="none" stroke="#000000" d="M409.0303,-5267.3029C424.7874,-5174.4119 500.3139,-4756.2065 596,-4673.5998 1470.9162,-3918.2766 2031.2997,-4468.2609 3187,-4449.5998 3335.8695,-4447.196 3373.8851,-4434.4381 3522,-4449.5998 3548.1869,-4452.2804 3554.201,-4456.3703 3580,-4461.5998 3614.0918,-4468.5102 3651.4543,-4475.9611 3684.647,-4482.5376"/>
+<polygon fill="#000000" stroke="#000000" points="3684.6925,-4484.3305 3689.9372,-4483.5853 3685.3725,-4480.8972 3684.6925,-4484.3305"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/api/v2 -->
+<g id="edge251" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/api/v2</title>
+<path fill="none" stroke="#000000" d="M408.3746,-5267.511C419.4906,-5187.0055 471.1927,-4854.6971 596,-4614.5998 1190.5194,-3470.8963 1521.0377,-3282.5075 2426,-2364.5998 2465.4957,-2324.5391 2516.6393,-2283.7479 2547.9831,-2259.8281"/>
+<polygon fill="#000000" stroke="#000000" points="2549.1623,-2261.1299 2552.0823,-2256.7099 2547.0433,-2258.3443 2549.1623,-2261.1299"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge256" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M406.496,-5303.863C413.3372,-5553.0549 490.2894,-8270.5708 596,-8397.5998 700.8749,-8523.6246 801.7553,-8425.7514 954,-8486.5998 1127.7375,-8556.0384 1176.1471,-8574.9638 1320,-8694.5998 1875.8325,-9156.8609 1919.9893,-9371.2186 2368,-9938.5998 2561.1891,-10183.2633 2538.5505,-10311.6945 2785,-10502.5998 2915.3621,-10603.5812 3006.856,-10530.8191 3129,-10641.5998 3170.2302,-10678.9943 3143.9142,-10717.3594 3187,-10752.5998 3306.9828,-10850.7354 3404.9625,-10747.9696 3522,-10849.5998 3569.4643,-10890.8157 3531.1847,-10938.9933 3580,-10978.5998 3604.6782,-10998.6226 3636.4639,-11010.0506 3667.4628,-11016.4365"/>
+<polygon fill="#000000" stroke="#000000" points="3667.1854,-11018.1655 3672.43,-11017.4187 3667.8644,-11014.732 3667.1854,-11018.1655"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;net/http -->
+<g id="edge259" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M406.2463,-5267.5509C409.6086,-5029.2561 448.5944,-2506.7773 596,-2216.5998 1256.3193,-916.7183 1798.1358,-648.1871 3187,-204.5998 3524.3901,-96.8413 3704.3332,-23.9934 3983,-242.5998 4034.4781,-282.983 4078.0228,-487.3263 4091.6085,-557.1624"/>
+<polygon fill="#000000" stroke="#000000" points="4089.9423,-557.7641 4092.6089,-562.3414 4093.3788,-557.1002 4089.9423,-557.7641"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;net/url -->
+<g id="edge260" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M406.3059,-5267.4982C409.5773,-5085.2455 441.1032,-3579.508 596,-3162.5998 706.4826,-2865.2333 797.2803,-2816.1137 1012,-2582.5998 1776.4364,-1751.253 2073.7667,-1458.8947 3187,-1268.5998 3540.4819,-1208.176 3678.2795,-1211.534 3983,-1400.5998 4025.7559,-1427.128 4062.2659,-1474.3218 4081.4888,-1502.2084"/>
+<polygon fill="#000000" stroke="#000000" points="4080.1609,-1503.3673 4084.4238,-1506.5122 4083.0525,-1501.3953 4080.1609,-1503.3673"/>
+</g>
+<!-- github.com/docker/distribution -->
+<g id="node89" class="node">
+<title>github.com/docker/distribution</title>
+<g id="a_node89"><a xlink:href="https://godoc.org/github.com/docker/distribution" xlink:title="github.com/docker/distribution" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2657,-5792.5998C2657,-5792.5998 2496,-5792.5998 2496,-5792.5998 2490,-5792.5998 2484,-5786.5998 2484,-5780.5998 2484,-5780.5998 2484,-5768.5998 2484,-5768.5998 2484,-5762.5998 2490,-5756.5998 2496,-5756.5998 2496,-5756.5998 2657,-5756.5998 2657,-5756.5998 2663,-5756.5998 2669,-5762.5998 2669,-5768.5998 2669,-5768.5998 2669,-5780.5998 2669,-5780.5998 2669,-5786.5998 2663,-5792.5998 2657,-5792.5998"/>
+<text text-anchor="middle" x="2576.5" y="-5770.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution -->
+<g id="edge248" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution</title>
+<path fill="none" stroke="#000000" d="M538.2457,-5302.1131C689.6757,-5319.5673 945.3206,-5344.5998 1166,-5344.5998 1166,-5344.5998 1166,-5344.5998 1848.5,-5344.5998 2010.9984,-5344.5998 2436.9476,-5666.2293 2549.1089,-5753.162"/>
+<polygon fill="#000000" stroke="#000000" points="2548.2557,-5754.715 2553.2787,-5756.3979 2550.4015,-5751.9499 2548.2557,-5754.715"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/reference -->
+<g id="edge249" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/reference</title>
+<path fill="none" stroke="#000000" d="M435.6951,-5267.5275C472.0773,-5246.3121 536.2616,-5211.8399 596,-5194.5998 1134.4118,-5039.2176 1288.1154,-5049.5998 1848.5,-5049.5998 1848.5,-5049.5998 1848.5,-5049.5998 2195.5,-5049.5998 2420.2459,-5049.5998 2681.1908,-5075.4618 2831.8874,-5092.9408"/>
+<polygon fill="#000000" stroke="#000000" points="2832.0326,-5094.7194 2837.2014,-5093.559 2832.4371,-5091.2428 2832.0326,-5094.7194"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge -->
+<g id="node95" class="node">
+<title>github.com/docker/distribution/registry/client/auth/challenge</title>
+<g id="a_node95"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/auth/challenge" xlink:title="github.com/docker/distribution/registry/client/auth/challenge" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3943,-1899.5998C3943,-1899.5998 3620,-1899.5998 3620,-1899.5998 3614,-1899.5998 3608,-1893.5998 3608,-1887.5998 3608,-1887.5998 3608,-1875.5998 3608,-1875.5998 3608,-1869.5998 3614,-1863.5998 3620,-1863.5998 3620,-1863.5998 3943,-1863.5998 3943,-1863.5998 3949,-1863.5998 3955,-1869.5998 3955,-1875.5998 3955,-1875.5998 3955,-1887.5998 3955,-1887.5998 3955,-1893.5998 3949,-1899.5998 3943,-1899.5998"/>
+<text text-anchor="middle" x="3781.5" y="-1877.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/auth/challenge</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/client/auth/challenge -->
+<g id="edge252" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/client/auth/challenge</title>
+<path fill="none" stroke="#000000" d="M407.2599,-5267.5098C417.1375,-5128.5845 484.7017,-4229.8812 596,-3984.5998 905.2705,-3303.0237 1117.7152,-2255.9163 2785,-1508.5998 3165.2463,-1338.1645 3649.0955,-1757.6971 3758.7326,-1859.8247"/>
+<polygon fill="#000000" stroke="#000000" points="3757.6264,-1861.1862 3762.4741,-1863.3223 3760.0166,-1858.6294 3757.6264,-1861.1862"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport -->
+<g id="node96" class="node">
+<title>github.com/docker/distribution/registry/client/transport</title>
+<g id="a_node96"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/transport" xlink:title="github.com/docker/distribution/registry/client/transport" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3927.5,-4702.5998C3927.5,-4702.5998 3635.5,-4702.5998 3635.5,-4702.5998 3629.5,-4702.5998 3623.5,-4696.5998 3623.5,-4690.5998 3623.5,-4690.5998 3623.5,-4678.5998 3623.5,-4678.5998 3623.5,-4672.5998 3629.5,-4666.5998 3635.5,-4666.5998 3635.5,-4666.5998 3927.5,-4666.5998 3927.5,-4666.5998 3933.5,-4666.5998 3939.5,-4672.5998 3939.5,-4678.5998 3939.5,-4678.5998 3939.5,-4690.5998 3939.5,-4690.5998 3939.5,-4696.5998 3933.5,-4702.5998 3927.5,-4702.5998"/>
+<text text-anchor="middle" x="3781.5" y="-4680.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/transport</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/client/transport -->
+<g id="edge253" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/client/transport</title>
+<path fill="none" stroke="#000000" d="M407.4603,-5267.4037C414.0589,-5198.6392 447.6098,-4953.3275 596,-4850.5998 845.4475,-4677.9121 3001.9977,-4679.2319 3617.8891,-4683.1926"/>
+<polygon fill="#000000" stroke="#000000" points="3618.2669,-4684.945 3623.2781,-4683.2276 3618.2896,-4681.445 3618.2669,-4684.945"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache -->
+<g id="node97" class="node">
+<title>github.com/docker/distribution/registry/storage/cache</title>
+<g id="a_node97"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/storage/cache" xlink:title="github.com/docker/distribution/registry/storage/cache" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1308,-5424.5998C1308,-5424.5998 1024,-5424.5998 1024,-5424.5998 1018,-5424.5998 1012,-5418.5998 1012,-5412.5998 1012,-5412.5998 1012,-5400.5998 1012,-5400.5998 1012,-5394.5998 1018,-5388.5998 1024,-5388.5998 1024,-5388.5998 1308,-5388.5998 1308,-5388.5998 1314,-5388.5998 1320,-5394.5998 1320,-5400.5998 1320,-5400.5998 1320,-5412.5998 1320,-5412.5998 1320,-5418.5998 1314,-5424.5998 1308,-5424.5998"/>
+<text text-anchor="middle" x="1166" y="-5402.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/storage/cache</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/storage/cache -->
+<g id="edge254" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/storage/cache</title>
+<path fill="none" stroke="#000000" d="M439.1518,-5303.7728C476.1774,-5323.0942 538.4806,-5352.7357 596,-5366.5998 732.0075,-5399.3823 891.149,-5408.1848 1006.6599,-5409.4793"/>
+<polygon fill="#000000" stroke="#000000" points="1006.7646,-5411.2303 1011.7824,-5409.5321 1006.8007,-5407.7305 1006.7646,-5411.2303"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory -->
+<g id="node98" class="node">
+<title>github.com/docker/distribution/registry/storage/cache/memory</title>
+<g id="a_node98"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/storage/cache/memory" xlink:title="github.com/docker/distribution/registry/storage/cache/memory" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M942,-5244.5998C942,-5244.5998 608,-5244.5998 608,-5244.5998 602,-5244.5998 596,-5238.5998 596,-5232.5998 596,-5232.5998 596,-5220.5998 596,-5220.5998 596,-5214.5998 602,-5208.5998 608,-5208.5998 608,-5208.5998 942,-5208.5998 942,-5208.5998 948,-5208.5998 954,-5214.5998 954,-5220.5998 954,-5220.5998 954,-5232.5998 954,-5232.5998 954,-5238.5998 948,-5244.5998 942,-5244.5998"/>
+<text text-anchor="middle" x="775" y="-5222.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/storage/cache/memory</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/storage/cache/memory -->
+<g id="edge255" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/storage/cache/memory</title>
+<path fill="none" stroke="#000000" d="M518.7172,-5267.5773C562.5945,-5260.5617 612.8888,-5252.52 657.2374,-5245.4291"/>
+<polygon fill="#000000" stroke="#000000" points="657.6768,-5247.1311 662.3378,-5244.6136 657.1241,-5243.675 657.6768,-5247.1311"/>
+</g>
+<!-- github.com/docker/go&#45;connections/tlsconfig&#45;&gt;crypto/tls -->
+<g id="edge363" class="edge">
+<title>github.com/docker/go&#45;connections/tlsconfig&#45;&gt;crypto/tls</title>
+<path fill="none" stroke="#000000" d="M3373.5745,-18573.7868C3476.1787,-18671.6172 3961.9744,-19134.81 4073.2524,-19240.9106"/>
+<polygon fill="#000000" stroke="#000000" points="4072.2253,-19242.3493 4077.0517,-19244.5331 4074.6405,-19239.8162 4072.2253,-19242.3493"/>
+</g>
+<!-- github.com/docker/go&#45;connections/tlsconfig&#45;&gt;crypto/x509 -->
+<g id="edge364" class="edge">
+<title>github.com/docker/go&#45;connections/tlsconfig&#45;&gt;crypto/x509</title>
+<path fill="none" stroke="#000000" d="M3483.3293,-18570.8494C3643.7848,-18587.0161 3906.1908,-18603.282 3983,-18547.5998 4034.9867,-18509.9125 4078.1996,-18308.5882 4091.6546,-18239.7205"/>
+<polygon fill="#000000" stroke="#000000" points="4093.4111,-18239.8547 4092.6453,-18234.613 4089.9752,-18239.1883 4093.4111,-18239.8547"/>
+</g>
+<!-- github.com/docker/go&#45;connections/tlsconfig&#45;&gt;encoding/pem -->
+<g id="edge365" class="edge">
+<title>github.com/docker/go&#45;connections/tlsconfig&#45;&gt;encoding/pem</title>
+<path fill="none" stroke="#000000" d="M3401.7707,-18573.7207C3509.7075,-18612.1218 3780.5845,-18691.485 3983,-18606.5998 4027.5937,-18587.899 4063.4611,-18542.0668 4082.0717,-18514.2381"/>
+<polygon fill="#000000" stroke="#000000" points="4083.6186,-18515.0711 4084.9091,-18509.9332 4080.6963,-18513.1449 4083.6186,-18515.0711"/>
+</g>
+<!-- github.com/docker/go&#45;connections/tlsconfig&#45;&gt;fmt -->
+<g id="edge366" class="edge">
+<title>github.com/docker/go&#45;connections/tlsconfig&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3356.0388,-18537.5439C3370.0855,-18371.9419 3476.5955,-17099.5739 3522,-16065.5998 3525.4311,-15987.4641 3536.9031,-13315.8656 3580,-13250.5998 3688.2942,-13086.5993 3874.4457,-13232.4283 3983,-13068.5998 4020.4328,-13012.1069 4039.2078,-10697.3453 4041,-10629.5998 4061.248,-9864.2427 4090.3118,-8924.4937 4095.2682,-8765.0892"/>
+<polygon fill="#000000" stroke="#000000" points="4097.0279,-8764.8014 4095.4343,-8759.7494 4093.5296,-8764.6926 4097.0279,-8764.8014"/>
+</g>
+<!-- github.com/docker/go&#45;connections/tlsconfig&#45;&gt;io/ioutil -->
+<g id="edge368" class="edge">
+<title>github.com/docker/go&#45;connections/tlsconfig&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M3355.8134,-18537.4132C3368.5972,-18362.2792 3472.6992,-16978.1431 3580,-16832.5998 3696.95,-16673.9682 3858.0211,-16799.986 3983,-16647.5998 4053.6859,-16561.4128 4001.5334,-16507.8451 4041,-16403.5998 4051.9435,-16374.6941 4069.361,-16343.684 4081.659,-16323.3294"/>
+<polygon fill="#000000" stroke="#000000" points="4083.2506,-16324.0803 4084.3564,-16318.8995 4080.2612,-16322.26 4083.2506,-16324.0803"/>
+</g>
+<!-- github.com/docker/go&#45;connections/tlsconfig&#45;&gt;github.com/pkg/errors -->
+<g id="edge367" class="edge">
+<title>github.com/docker/go&#45;connections/tlsconfig&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M3356.1227,-18537.5474C3370.922,-18371.9761 3482.8476,-17099.8295 3522,-16065.5998 3536.3253,-15687.1905 3498.6817,-9619.446 3580,-9249.5998 3618.0417,-9076.5811 3727.775,-8889.5526 3767.1897,-8826.0993"/>
+<polygon fill="#000000" stroke="#000000" points="3768.7435,-8826.9149 3769.9041,-8821.7461 3765.7736,-8825.063 3768.7435,-8826.9149"/>
+</g>
+<!-- github.com/docker/go&#45;connections/tlsconfig&#45;&gt;os -->
+<g id="edge369" class="edge">
+<title>github.com/docker/go&#45;connections/tlsconfig&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3397.0216,-18573.6226C3442.1421,-18592.5433 3515.6608,-18622.793 3580,-18646.5998 3761.3274,-18713.6946 3982.2468,-18784.6345 4063.7799,-18810.457"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6518,-18812.2519 4068.9468,-18812.0921 4064.7079,-18808.915 4063.6518,-18812.2519"/>
+</g>
+<!-- github.com/docker/go&#45;connections/tlsconfig&#45;&gt;runtime -->
+<g id="edge370" class="edge">
+<title>github.com/docker/go&#45;connections/tlsconfig&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M3483.052,-18545.8831C3661.6425,-18531.8115 3965.0856,-18505.5899 3983,-18488.5998 3995.7912,-18476.4686 4075.3368,-17928.1843 4092.6683,-17807.8049"/>
+<polygon fill="#000000" stroke="#000000" points="4094.4195,-17807.9213 4093.3996,-17802.723 4090.9552,-17807.4228 4094.4195,-17807.9213"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;bytes -->
+<g id="edge382" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3357.6848,-3743.7341C3377.6384,-3858.3398 3485.7565,-4494.1091 3522,-5017.5998 3527.968,-5103.8 3527.0023,-11170.3552 3580,-11238.5998 3693.6647,-11384.9647 3866.536,-11201.4525 3983,-11345.5998 4021.7885,-11393.6083 4087.611,-13439.2995 4095.2669,-13681.2906"/>
+<polygon fill="#000000" stroke="#000000" points="4093.5197,-13681.4088 4095.4269,-13686.3509 4097.0179,-13681.2981 4093.5197,-13681.4088"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;encoding -->
+<g id="edge383" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;encoding</title>
+<path fill="none" stroke="#000000" d="M3430.9033,-3732.1076C3461.2658,-3737.5606 3495.2559,-3747.5068 3522,-3765.5998 3558.9733,-3790.6131 3548.0111,-3816.4646 3580,-3847.5998 3734.3184,-3997.8002 3866.5746,-3935.4384 3983,-4116.5998 4070.3808,-4252.567 4012.032,-4316.5925 4041,-4475.5998 4057.7774,-4567.6922 4080.9045,-4676.7129 4090.9756,-4723.4492"/>
+<polygon fill="#000000" stroke="#000000" points="4089.3153,-4724.0524 4092.0807,-4728.5707 4092.7366,-4723.3141 4089.3153,-4724.0524"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;encoding/json -->
+<g id="edge384" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3357.7155,-3743.732C3377.8564,-3858.325 3486.919,-4494.0299 3522,-5017.5998 3527.5155,-5099.9162 3529.2398,-16832.5628 3580,-16897.5998 3693.7437,-17043.335 3850.6225,-16871.5549 3983,-17000.5998 4050.8022,-17066.6951 3982.9614,-17130.785 4041,-17205.5998 4044.0979,-17209.5932 4047.8451,-17213.1812 4051.9106,-17216.3813"/>
+<polygon fill="#000000" stroke="#000000" points="4051.0081,-17217.8903 4056.0699,-17219.4526 4053.0873,-17215.0747 4051.0081,-17217.8903"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;fmt -->
+<g id="edge385" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3357.5968,-3743.7405C3377.0127,-3858.3852 3482.4195,-4494.3508 3522,-5017.5998 3527.5055,-5090.3819 3530.9369,-7590.5595 3580,-7644.5998 3701.2475,-7778.1473 3856.173,-7564.3391 3983,-7692.5998 4020.4688,-7730.4922 4083.2076,-8566.5242 4094.2989,-8718.1326"/>
+<polygon fill="#000000" stroke="#000000" points="4092.5619,-8718.3754 4094.6716,-8723.2346 4096.0526,-8718.1204 4092.5619,-8718.3754"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;reflect -->
+<g id="edge387" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M3430.9172,-3726.8476C3462.9276,-3731.2734 3498.1617,-3741.8715 3522,-3765.5998 3599.1629,-3842.4068 3522.8089,-3910.9575 3580,-4003.5998 3703.4003,-4203.4926 3870.3813,-4139.4401 3983,-4345.5998 4076.2936,-4516.3831 4006.3392,-4588.1077 4041,-4779.5998 4055.5227,-4859.8339 4078.9383,-4953.7453 4089.932,-4996.4079"/>
+<polygon fill="#000000" stroke="#000000" points="4088.2643,-4996.9491 4091.2096,-5001.3522 4091.6529,-4996.0734 4088.2643,-4996.9491"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;sort -->
+<g id="edge388" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3430.7301,-3739.354C3590.0012,-3768.8 3946.3931,-3838.6648 3983,-3880.5998 4138.8325,-4059.1136 3911.8483,-4211.9273 4041,-4410.5998 4046.5375,-4419.1181 4055.1947,-4425.6459 4064.0154,-4430.5141"/>
+<polygon fill="#000000" stroke="#000000" points="4063.669,-4432.3053 4068.9139,-4433.05 4065.2782,-4429.1971 4063.669,-4432.3053"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;strconv -->
+<g id="edge389" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3430.9265,-3725.02C3463.7379,-3728.974 3499.6074,-3739.7068 3522,-3765.5998 3616.1368,-3874.4521 3481.1116,-4306.0453 3580,-4410.5998 3704.3876,-4542.1146 3857.9906,-4338.676 3983,-4469.5998 4072.9104,-4563.764 4022.7631,-4923.6883 4041,-5052.5998 4055.6232,-5155.967 4080.4823,-5278.0949 4091.0308,-5328.2878"/>
+<polygon fill="#000000" stroke="#000000" points="4089.3823,-5328.9518 4092.1257,-5333.4835 4092.8071,-5328.2301 4089.3823,-5328.9518"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;strings -->
+<g id="edge390" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3356.8213,-3743.7644C3379.744,-3923.0072 3565.0344,-5368.7324 3580,-5384.5998 3705.233,-5517.3789 3864.0207,-5325.1892 3983,-5463.5998 4032.0077,-5520.6113 4031.6347,-6741.0052 4041,-6815.5998 4052.2056,-6904.8523 4077.904,-7008.9017 4089.7768,-7054.3609"/>
+<polygon fill="#000000" stroke="#000000" points="4088.1245,-7054.9594 4091.0863,-7059.3515 4091.5099,-7054.0711 4088.1245,-7054.9594"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;sync -->
+<g id="edge391" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3361.5592,-3707.3003C3398.4266,-3611.8307 3567.6394,-3174.942 3580,-3166.5998 3734.2742,-3062.48 3977.6165,-3141.674 4064.1565,-3175.3597"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6012,-3177.0217 4068.8949,-3177.2199 4064.8802,-3173.7637 4063.6012,-3177.0217"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;unicode -->
+<g id="edge392" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;unicode</title>
+<path fill="none" stroke="#000000" d="M3430.7726,-3711.2584C3579.0183,-3685.2192 3898.8842,-3638.5807 3983,-3703.5998 3999.9116,-3716.672 4070.975,-4026.7429 4090.8284,-4114.5949"/>
+<polygon fill="#000000" stroke="#000000" points="4089.1448,-4115.0845 4091.9531,-4119.5763 4092.5589,-4114.3137 4089.1448,-4115.0845"/>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;unicode/utf8 -->
+<g id="edge393" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M3430.6198,-3718.6505C3580.1432,-3706.7713 3904.5023,-3690.3184 3983,-3762.5998 4050.3681,-3824.633 4022.7653,-4080.8554 4041,-4170.5998 4054.7466,-4238.2555 4077.4831,-4316.6561 4088.9391,-4354.63"/>
+<polygon fill="#000000" stroke="#000000" points="4087.2884,-4355.2173 4090.4121,-4359.4957 4090.6382,-4354.2031 4087.2884,-4355.2173"/>
+</g>
+<!-- gopkg.in/yaml.v2 -->
+<g id="node109" class="node">
+<title>gopkg.in/yaml.v2</title>
+<g id="a_node109"><a xlink:href="https://godoc.org/gopkg.in/yaml.v2" xlink:title="gopkg.in/yaml.v2" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3826,-4395.5998C3826,-4395.5998 3737,-4395.5998 3737,-4395.5998 3731,-4395.5998 3725,-4389.5998 3725,-4383.5998 3725,-4383.5998 3725,-4371.5998 3725,-4371.5998 3725,-4365.5998 3731,-4359.5998 3737,-4359.5998 3737,-4359.5998 3826,-4359.5998 3826,-4359.5998 3832,-4359.5998 3838,-4365.5998 3838,-4371.5998 3838,-4371.5998 3838,-4383.5998 3838,-4383.5998 3838,-4389.5998 3832,-4395.5998 3826,-4395.5998"/>
+<text text-anchor="middle" x="3781.5" y="-4373.8998" font-family="Times,serif" font-size="14.00" fill="#000000">gopkg.in/yaml.v2</text>
+</a>
+</g>
+</g>
+<!-- github.com/ghodss/yaml&#45;&gt;gopkg.in/yaml.v2 -->
+<g id="edge386" class="edge">
+<title>github.com/ghodss/yaml&#45;&gt;gopkg.in/yaml.v2</title>
+<path fill="none" stroke="#000000" d="M3430.6971,-3725.6588C3463.2583,-3729.7731 3498.9908,-3740.4554 3522,-3765.5998 3641.7867,-3896.5021 3503.2865,-4000.602 3580,-4160.5998 3621.1019,-4246.3242 3707.9623,-4321.4294 3753.0289,-4356.528"/>
+<polygon fill="#000000" stroke="#000000" points="3751.9676,-4357.9194 3756.9928,-4359.5958 3754.1098,-4355.1515 3751.9676,-4357.9194"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;crypto -->
+<g id="edge485" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;crypto</title>
+<path fill="none" stroke="#000000" d="M3824.7825,-11040.6434C3873.3785,-11063.541 3949.7742,-11108.1038 3983,-11173.5998 4112.6991,-11429.2682 3969.2726,-16056.033 4041,-16333.5998 4048.6279,-16363.1178 4066.4974,-16393.4307 4079.8106,-16413.2542"/>
+<polygon fill="#000000" stroke="#000000" points="4078.4863,-16414.4187 4082.7466,-16417.5671 4081.3795,-16412.4491 4078.4863,-16414.4187"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;fmt -->
+<g id="edge486" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3801.427,-11004.5379C3844.0343,-10964.4832 3943.3362,-10863.1296 3983,-10754.5998 4054.7528,-10558.2669 4090.9874,-8975.6349 4095.514,-8764.7813"/>
+<polygon fill="#000000" stroke="#000000" points="4097.2636,-8764.8128 4095.621,-8759.7765 4093.7644,-8764.738 4097.2636,-8764.8128"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;io -->
+<g id="edge488" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3824.6506,-11040.7108C3873.1227,-11063.6717 3949.4022,-11108.2939 3983,-11173.5998 4048.1816,-11300.2969 4013.5694,-13597.7844 4041,-13737.5998 4049.7667,-13782.2843 4071.1779,-13831.1854 4084.5516,-13858.9566"/>
+<polygon fill="#000000" stroke="#000000" points="4083.0288,-13859.8269 4086.788,-13863.5594 4086.1769,-13858.2973 4083.0288,-13859.8269"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;strings -->
+<g id="edge490" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3802.0465,-11004.3395C3845.4065,-10964.2826 3945.3612,-10863.5405 3983,-10754.5998 4034.0263,-10606.9107 4034.4297,-8098.7171 4041,-7942.5998 4055.0794,-7608.057 4086.237,-7201.5575 4094.1462,-7100.9503"/>
+<polygon fill="#000000" stroke="#000000" points="4095.9002,-7100.9679 4094.5482,-7095.8459 4092.411,-7100.693 4095.9002,-7100.9679"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;regexp -->
+<g id="edge489" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M3802.1592,-11004.3779C3845.7379,-10964.3959 3946.0927,-10863.7904 3983,-10754.5998 4054.5512,-10542.9151 4025.4155,-2933.5059 4041,-2710.5998 4051.4519,-2561.1054 4081.0082,-2383.0887 4091.9019,-2320.6239"/>
+<polygon fill="#000000" stroke="#000000" points="4093.6372,-2320.8598 4092.7756,-2315.6329 4090.1896,-2320.2563 4093.6372,-2320.8598"/>
+</g>
+<!-- hash -->
+<g id="node119" class="node">
+<title>hash</title>
+<g id="a_node119"><a xlink:href="https://godoc.org/hash" xlink:title="hash" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-11846.5998C4111,-11846.5998 4081,-11846.5998 4081,-11846.5998 4075,-11846.5998 4069,-11840.5998 4069,-11834.5998 4069,-11834.5998 4069,-11822.5998 4069,-11822.5998 4069,-11816.5998 4075,-11810.5998 4081,-11810.5998 4081,-11810.5998 4111,-11810.5998 4111,-11810.5998 4117,-11810.5998 4123,-11816.5998 4123,-11822.5998 4123,-11822.5998 4123,-11834.5998 4123,-11834.5998 4123,-11840.5998 4117,-11846.5998 4111,-11846.5998"/>
+<text text-anchor="middle" x="4096" y="-11824.8998" font-family="Times,serif" font-size="14.00" fill="#000000">hash</text>
+</a>
+</g>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;hash -->
+<g id="edge487" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;hash</title>
+<path fill="none" stroke="#000000" d="M3820.8945,-11040.6059C3867.6276,-11064.1562 3944.0885,-11110.0287 3983,-11173.5998 4016.9537,-11229.0712 4078.7401,-11694.813 4093.0059,-11805.2037"/>
+<polygon fill="#000000" stroke="#000000" points="4091.312,-11805.752 4093.6874,-11810.487 4094.7833,-11805.3042 4091.312,-11805.752"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;time -->
+<g id="edge494" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3412.3399,-11187.636C3457.8381,-11201.7418 3522.8713,-11221.7237 3580,-11238.5998 3762.473,-11292.5033 3982.269,-11353.9757 4063.6536,-11376.6201"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5262,-11378.401 4068.8123,-11378.055 4064.4641,-11375.029 4063.5262,-11378.401"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge492" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M3385.9519,-11151.5989C3428.6133,-11127.9096 3507.7993,-11086.4865 3580,-11062.5998 3607.7674,-11053.4133 3638.422,-11046.0077 3667.2645,-11040.1848"/>
+<polygon fill="#000000" stroke="#000000" points="3667.6902,-11041.8845 3672.2512,-11039.19 3667.0054,-11038.4521 3667.6902,-11041.8845"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go -->
+<g id="edge493" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go</title>
+<path fill="none" stroke="#000000" d="M3358.1079,-11151.3872C3377.345,-11053.7669 3468.9535,-10582.409 3522,-10193.5998 3539.2412,-10067.2287 3507.1787,-9727.3088 3580,-9622.5998 3607.9105,-9582.4676 3656.8358,-9557.8966 3699.4866,-9543.2718"/>
+<polygon fill="#000000" stroke="#000000" points="3700.206,-9544.8763 3704.3883,-9541.6249 3699.0912,-9541.5586 3700.206,-9544.8763"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;fmt -->
+<g id="edge504" class="edge">
+<title>github.com/pkg/errors&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3850.5184,-8789.9937C3916.7726,-8776.9324 4013.9231,-8757.7803 4063.8147,-8747.9448"/>
+<polygon fill="#000000" stroke="#000000" points="4064.2176,-8749.6491 4068.7846,-8746.965 4063.5406,-8746.2152 4064.2176,-8749.6491"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;io -->
+<g id="edge505" class="edge">
+<title>github.com/pkg/errors&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3793.0635,-8821.7533C3829.7867,-8880.5958 3943.6544,-9072.5151 3983,-9249.5998 4091.1678,-9736.4362 3947.3612,-13247.7613 4041,-13737.5998 4049.55,-13782.3262 4071.0326,-13831.2135 4084.4785,-13858.9708"/>
+<polygon fill="#000000" stroke="#000000" points="4082.9592,-13859.8477 4086.7274,-13863.5711 4086.1036,-13858.3105 4082.9592,-13859.8477"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;strings -->
+<g id="edge508" class="edge">
+<title>github.com/pkg/errors&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3786.0781,-8785.2961C3809.5237,-8691.0898 3917.5809,-8251.1938 3983,-7886.5998 4038.4919,-7577.332 4082.5723,-7197.4133 4093.428,-7100.7921"/>
+<polygon fill="#000000" stroke="#000000" points="4095.19,-7100.7821 4094.0079,-7095.6183 4091.7118,-7100.3922 4095.19,-7100.7821"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;path -->
+<g id="edge506" class="edge">
+<title>github.com/pkg/errors&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M3786.9551,-8785.425C3814.73,-8691.8549 3940.7605,-8254.6002 3983,-7886.5998 4102.8698,-6842.2666 3965.2023,-4205.0536 4041,-3156.5998 4051.8316,-3006.7749 4081.153,-2828.3077 4091.9434,-2765.6831"/>
+<polygon fill="#000000" stroke="#000000" points="4093.681,-2765.9045 4092.8086,-2760.6794 4090.2322,-2765.308 4093.681,-2765.9045"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;runtime -->
+<g id="edge507" class="edge">
+<title>github.com/pkg/errors&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M3793.1052,-8821.7441C3829.9541,-8880.5589 3944.1585,-9072.4038 3983,-9249.5998 4078.4154,-9684.8877 4015.6513,-16825.6986 4041,-17270.5998 4051.8216,-17460.5321 4082.4323,-17688.3443 4092.6401,-17761.0781"/>
+<polygon fill="#000000" stroke="#000000" points="4090.9655,-17761.7365 4093.3957,-17766.4436 4094.4313,-17761.2484 4090.9655,-17761.7365"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;bufio -->
+<g id="edge609" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3355.7505,-12323.7332C3362.5248,-12409.6141 3401.4474,-12780.6708 3580,-13006.5998 3709.8266,-13170.8741 3872.356,-13072.839 3983,-13250.5998 4139.5394,-13502.0963 3882.2685,-13664.4811 4041,-13914.5998 4046.444,-13923.1781 4055.0749,-13929.7229 4063.9003,-13934.5881"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5587,-13936.3812 4068.8043,-13937.1204 4065.1646,-13933.2713 4063.5587,-13936.3812"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;bytes -->
+<g id="edge610" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3423.2728,-12323.6273C3582.5685,-12365.6799 3964.7141,-12468.384 3983,-12490.5998 4061.604,-12586.0968 4090.8705,-13519.1731 4095.3678,-13680.8019"/>
+<polygon fill="#000000" stroke="#000000" points="4093.6301,-13681.2736 4095.5174,-13686.2234 4097.1287,-13681.177 4093.6301,-13681.2736"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;context -->
+<g id="edge611" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M3401.3843,-12287.5368C3441.515,-12269.272 3496.6954,-12236.8115 3522,-12188.5998 3655.4729,-11934.3003 3488.6181,-2121.8728 3580,-1849.5998 3675.1722,-1566.0334 3848.3425,-1577.686 3983,-1310.5998 4046.1483,-1185.3483 4080.7916,-1018.4971 4091.9519,-957.8643"/>
+<polygon fill="#000000" stroke="#000000" points="4093.7239,-957.9014 4092.8984,-952.6687 4090.2806,-957.2741 4093.7239,-957.9014"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;encoding/json -->
+<g id="edge612" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3358.848,-12323.783C3381.4476,-12419.286 3485.8409,-12872.85 3522,-13250.5998 3531.9454,-13354.4984 3516.1115,-16925.0644 3580,-17007.5998 3693.7192,-17154.5097 3832.4235,-17009.7821 3983,-17118.5998 4020.6654,-17145.8197 4007.1519,-17173.7578 4041,-17205.5998 4045.2746,-17209.6211 4050.1436,-17213.3514 4055.1763,-17216.7384"/>
+<polygon fill="#000000" stroke="#000000" points="4054.286,-17218.2469 4059.4327,-17219.5016 4056.1918,-17215.3112 4054.286,-17218.2469"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;fmt -->
+<g id="edge613" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3404.4284,-12287.5069C3449.683,-12271.7871 3518.5067,-12249.5617 3580,-12236.5998 3668.2494,-12217.9982 3922.02,-12255.0484 3983,-12188.5998 3998.4074,-12171.8107 4086.7886,-9066.6037 4095.3355,-8765.075"/>
+<polygon fill="#000000" stroke="#000000" points="4097.0971,-8764.6887 4095.4895,-8759.6411 4093.5985,-8764.5895 4097.0971,-8764.6887"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;io -->
+<g id="edge615" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3364.6989,-12323.6917C3394.7663,-12376.4581 3486.805,-12533.9463 3580,-12652.5998 3741.387,-12858.0737 3876.0595,-12842.2115 3983,-13080.5998 4102.9806,-13348.0566 3975.1858,-13451.948 4041,-13737.5998 4051.2237,-13781.9736 4072.155,-13830.977 4085.0431,-13858.8518"/>
+<polygon fill="#000000" stroke="#000000" points="4083.4978,-13859.679 4087.1955,-13863.4725 4086.6705,-13858.2011 4083.4978,-13859.679"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;reflect -->
+<g id="edge618" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M3401.3555,-12287.5217C3441.4672,-12269.2468 3496.6356,-12236.7801 3522,-12188.5998 3614.7824,-12012.3574 3438.5948,-5158.8658 3580,-5018.5998 3647.9394,-4951.2078 3961.8574,-4996.9955 4063.8766,-5013.9921"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6395,-5015.7266 4068.86,-5014.8276 4064.2182,-5012.2748 4063.6395,-5015.7266"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;sort -->
+<g id="edge620" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3401.3636,-12287.5259C3441.4806,-12269.2539 3496.6524,-12236.7889 3522,-12188.5998 3572.7119,-12092.1902 3502.6881,-4422.3421 3580,-4345.5998 3643.5591,-4282.5091 3901.2689,-4308.9907 3983,-4345.5998 4018.335,-4361.4271 4011.0064,-4386.1163 4041,-4410.5998 4048.1486,-4416.4351 4056.4878,-4421.8694 4064.4796,-4426.543"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6653,-4428.0932 4068.874,-4429.0589 4065.4043,-4425.0558 4063.6653,-4428.0932"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;strings -->
+<g id="edge621" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3400.7584,-12287.5904C3440.7633,-12269.275 3496.1211,-12236.7071 3522,-12188.5998 3639.4231,-11970.3175 3453.3539,-10171.6635 3580,-9958.5998 3685.7128,-9780.7534 3876.7084,-9903.1009 3983,-9725.5998 4033.9169,-9640.5713 4036.2438,-8041.5936 4041,-7942.5998 4057.0688,-7608.1466 4086.7574,-7201.581 4094.2483,-7100.9549"/>
+<polygon fill="#000000" stroke="#000000" points="4096.0023,-7100.9658 4094.6289,-7095.8495 4092.5119,-7100.7056 4096.0023,-7100.9658"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;sync -->
+<g id="edge622" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3401.3713,-12287.53C3441.4934,-12269.2606 3496.6683,-12236.7973 3522,-12188.5998 3633.3492,-11976.7404 3433.4459,-3761.8217 3580,-3572.5998 3693.4749,-3426.0878 3843.8602,-3588.0025 3983,-3465.5998 4061.4187,-3396.6141 4086.3642,-3265.0864 4093.4574,-3211.8404"/>
+<polygon fill="#000000" stroke="#000000" points="4095.2245,-3211.8195 4094.1265,-3206.6371 4091.753,-3211.373 4095.2245,-3211.8195"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;time -->
+<g id="edge624" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3402.572,-12323.7427C3518.9264,-12364.1999 3818.2401,-12447.074 3983,-12298.5998 4066.4223,-12223.4234 4027.1075,-11908.0348 4041,-11796.5998 4059.451,-11648.5992 4084.0594,-11470.9684 4092.7748,-11408.5964"/>
+<polygon fill="#000000" stroke="#000000" points="4094.5123,-11408.8068 4093.4717,-11403.6126 4091.046,-11408.3221 4094.5123,-11408.8068"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;os -->
+<g id="edge617" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3358.8599,-12323.7819C3381.5194,-12419.2792 3486.1663,-12872.819 3522,-13250.5998 3533.0939,-13367.5581 3502.2495,-17391.525 3580,-17479.5998 3699.6748,-17615.166 3861.1353,-17401.9987 3983,-17535.5998 4076.9238,-17638.5691 3968.7409,-18669.4236 4041,-18788.5998 4046.4143,-18797.5296 4055.3296,-18804.2279 4064.4219,-18809.1251"/>
+<polygon fill="#000000" stroke="#000000" points="4063.634,-18810.6876 4068.8851,-18811.3864 4065.2158,-18807.5655 4063.634,-18810.6876"/>
+</g>
+<!-- golang.org/x/sys/unix -->
+<g id="node70" class="node">
+<title>golang.org/x/sys/unix</title>
+<g id="a_node70"><a xlink:href="https://godoc.org/golang.org/x/sys/unix" xlink:title="golang.org/x/sys/unix" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3836.5,-14020.5998C3836.5,-14020.5998 3726.5,-14020.5998 3726.5,-14020.5998 3720.5,-14020.5998 3714.5,-14014.5998 3714.5,-14008.5998 3714.5,-14008.5998 3714.5,-13996.5998 3714.5,-13996.5998 3714.5,-13990.5998 3720.5,-13984.5998 3726.5,-13984.5998 3726.5,-13984.5998 3836.5,-13984.5998 3836.5,-13984.5998 3842.5,-13984.5998 3848.5,-13990.5998 3848.5,-13996.5998 3848.5,-13996.5998 3848.5,-14008.5998 3848.5,-14008.5998 3848.5,-14014.5998 3842.5,-14020.5998 3836.5,-14020.5998"/>
+<text text-anchor="middle" x="3781.5" y="-13998.8998" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/sys/unix</text>
+</a>
+</g>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;golang.org/x/sys/unix -->
+<g id="edge614" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;golang.org/x/sys/unix</title>
+<path fill="none" stroke="#000000" d="M3357.5736,-12323.8354C3375.2734,-12427.9278 3467.3563,-12957.209 3580,-13380.5998 3643.1122,-13617.8179 3743.3839,-13898.1077 3772.9999,-13979.4278"/>
+<polygon fill="#000000" stroke="#000000" points="3771.4902,-13980.3961 3774.8474,-13984.4939 3774.7784,-13979.197 3771.4902,-13980.3961"/>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;runtime -->
+<g id="edge619" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M3358.8528,-12323.7826C3381.4766,-12419.2832 3485.972,-12872.8375 3522,-13250.5998 3542.7522,-13468.1918 3481.9994,-16989.2212 3580,-17184.5998 3681.3153,-17386.5867 3847.4478,-17313.7986 3983,-17494.5998 4047.7108,-17580.9119 4080.2153,-17708.9733 4091.3905,-17761.1739"/>
+<polygon fill="#000000" stroke="#000000" points="4089.7228,-17761.7475 4092.4668,-17766.2789 4093.1476,-17761.0254 4089.7228,-17761.7475"/>
+</g>
+<!-- log -->
+<g id="node111" class="node">
+<title>log</title>
+<g id="a_node111"><a xlink:href="https://godoc.org/log" xlink:title="log" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-11338.5998C4111,-11338.5998 4081,-11338.5998 4081,-11338.5998 4075,-11338.5998 4069,-11332.5998 4069,-11326.5998 4069,-11326.5998 4069,-11314.5998 4069,-11314.5998 4069,-11308.5998 4075,-11302.5998 4081,-11302.5998 4081,-11302.5998 4111,-11302.5998 4111,-11302.5998 4117,-11302.5998 4123,-11308.5998 4123,-11314.5998 4123,-11314.5998 4123,-11326.5998 4123,-11326.5998 4123,-11332.5998 4117,-11338.5998 4111,-11338.5998"/>
+<text text-anchor="middle" x="4096" y="-11316.8998" font-family="Times,serif" font-size="14.00" fill="#000000">log</text>
+</a>
+</g>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;log -->
+<g id="edge616" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;log</title>
+<path fill="none" stroke="#000000" d="M3437.228,-12299.7326C3603.5619,-12287.5517 3964.3103,-12259.0381 3983,-12239.5998 4119.7541,-12097.3687 3938.2264,-11522.0307 4041,-11353.5998 4046.483,-11344.614 4055.4162,-11337.7647 4064.5039,-11332.6994"/>
+<polygon fill="#000000" stroke="#000000" points="4065.3521,-11334.2306 4068.9631,-11330.3545 4063.7231,-11331.1328 4065.3521,-11334.2306"/>
+</g>
+<!-- sync/atomic -->
+<g id="node112" class="node">
+<title>sync/atomic</title>
+<g id="a_node112"><a xlink:href="https://godoc.org/sync/atomic" xlink:title="sync/atomic" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4125.5,-7992.5998C4125.5,-7992.5998 4066.5,-7992.5998 4066.5,-7992.5998 4060.5,-7992.5998 4054.5,-7986.5998 4054.5,-7980.5998 4054.5,-7980.5998 4054.5,-7968.5998 4054.5,-7968.5998 4054.5,-7962.5998 4060.5,-7956.5998 4066.5,-7956.5998 4066.5,-7956.5998 4125.5,-7956.5998 4125.5,-7956.5998 4131.5,-7956.5998 4137.5,-7962.5998 4137.5,-7968.5998 4137.5,-7968.5998 4137.5,-7980.5998 4137.5,-7980.5998 4137.5,-7986.5998 4131.5,-7992.5998 4125.5,-7992.5998"/>
+<text text-anchor="middle" x="4096" y="-7970.8998" font-family="Times,serif" font-size="14.00" fill="#000000">sync/atomic</text>
+</a>
+</g>
+</g>
+<!-- github.com/sirupsen/logrus&#45;&gt;sync/atomic -->
+<g id="edge623" class="edge">
+<title>github.com/sirupsen/logrus&#45;&gt;sync/atomic</title>
+<path fill="none" stroke="#000000" d="M3400.6761,-12287.5459C3440.6257,-12269.2007 3495.9484,-12236.6139 3522,-12188.5998 3614.6236,-12017.8909 3510.5286,-10622.9679 3580,-10441.5998 3679.6379,-10181.4764 3881.6598,-10221.0648 3983,-9961.5998 4033.6642,-9831.8825 4032.581,-8848.6054 4041,-8709.5998 4057.9899,-8429.0797 4086.3087,-8088.9145 4094.0133,-7997.91"/>
+<polygon fill="#000000" stroke="#000000" points="4095.7703,-7997.9005 4094.449,-7992.7705 4092.2828,-7997.6048 4095.7703,-7997.9005"/>
+</g>
+<!-- github.com/containers/image/v5/internal/pkg/keyctl&#45;&gt;unsafe -->
+<g id="edge87" class="edge">
+<title>github.com/containers/image/v5/internal/pkg/keyctl&#45;&gt;unsafe</title>
+<path fill="none" stroke="#000000" d="M3445.9938,-16050.6363C3584.8255,-16072.0854 3846.8972,-16088.2934 3983,-15939.5998 4050.6682,-15865.6716 4090.5142,-14247.6762 4095.4737,-14034.6683"/>
+<polygon fill="#000000" stroke="#000000" points="4097.2244,-14034.6558 4095.5909,-14029.6165 4093.7253,-14034.5745 4097.2244,-14034.6558"/>
+</g>
+<!-- github.com/containers/image/v5/internal/pkg/keyctl&#45;&gt;golang.org/x/sys/unix -->
+<g id="edge86" class="edge">
+<title>github.com/containers/image/v5/internal/pkg/keyctl&#45;&gt;golang.org/x/sys/unix</title>
+<path fill="none" stroke="#000000" d="M3361.6894,-16014.5234C3388.8283,-15945.3 3485.3618,-15689.904 3522,-15469.5998 3535.0803,-15390.9483 3527.1601,-14095.3084 3580,-14035.5998 3611.4428,-14000.0697 3665.0859,-13992.671 3709.1211,-13993.7003"/>
+<polygon fill="#000000" stroke="#000000" points="3709.1163,-13995.4509 3714.1678,-13993.8557 3709.2241,-13991.9525 3709.1163,-13995.4509"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;bytes -->
+<g id="edge663" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3808.4978,-13984.4087C3848.6999,-13956.6846 3925.7044,-13901.0933 3983,-13844.5998 4021.5823,-13806.5577 4059.7618,-13755.789 4080.369,-13727.0085"/>
+<polygon fill="#000000" stroke="#000000" points="4081.8021,-13728.013 4083.2799,-13722.9258 4078.9523,-13725.9811 4081.8021,-13728.013"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;encoding/binary -->
+<g id="edge664" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M3807.4023,-13984.3824C3852.5084,-13950.9654 3944.3493,-13875.0286 3983,-13785.5998 4051.3925,-13627.3555 4090.0447,-12328.2709 4095.3649,-12137.861"/>
+<polygon fill="#000000" stroke="#000000" points="4097.1158,-12137.8522 4095.5056,-12132.8054 4093.6171,-12137.7547 4097.1158,-12137.8522"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;sort -->
+<g id="edge667" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3789.7495,-13984.289C3822.4513,-13910.5834 3943.1265,-13627.6954 3983,-13380.5998 4059.6996,-12905.2933 3991.4478,-5193.4983 4041,-4714.5998 4050.6641,-4621.2004 4077.4919,-4512.3608 4089.7477,-4465.7263"/>
+<polygon fill="#000000" stroke="#000000" points="4091.4427,-4466.1612 4091.0278,-4460.8801 4088.0588,-4465.2673 4091.4427,-4466.1612"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;strings -->
+<g id="edge668" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3789.7212,-13984.2844C3822.3152,-13910.5613 3942.6358,-13627.6158 3983,-13380.5998 4080.4476,-12784.2526 4018.5982,-8546.441 4041,-7942.5998 4053.4136,-7607.991 4085.8013,-7201.5403 4094.0607,-7100.9469"/>
+<polygon fill="#000000" stroke="#000000" points="4095.8147,-7100.9699 4094.4807,-7095.8432 4092.3265,-7100.6828 4095.8147,-7100.9699"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;sync -->
+<g id="edge669" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3789.754,-13984.2897C3822.4729,-13910.5869 3943.2045,-13627.708 3983,-13380.5998 4067.5257,-12855.7416 4015.9924,-4342.6322 4041,-3811.5998 4052.0579,-3576.7872 4083.7482,-3293.5677 4093.2652,-3211.7517"/>
+<polygon fill="#000000" stroke="#000000" points="4095.0183,-3211.8256 4093.8595,-3206.6565 4091.5419,-3211.4201 4095.0183,-3211.8256"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;time -->
+<g id="edge671" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3807.7511,-13984.5291C3853.4034,-13951.342 3946.0719,-13875.7536 3983,-13785.5998 4066.8048,-13581.0044 4023.0909,-12016.9672 4041,-11796.5998 4053.0528,-11648.2926 4081.4957,-11471.5502 4092.0058,-11408.9648"/>
+<polygon fill="#000000" stroke="#000000" points="4093.7439,-11409.1816 4092.8489,-11403.9603 4090.2925,-11408.6 4093.7439,-11409.1816"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;unsafe -->
+<g id="edge672" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;unsafe</title>
+<path fill="none" stroke="#000000" d="M3848.5824,-14004.5195C3914.8585,-14006.4161 4013.4105,-14009.2364 4063.7959,-14010.6782"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7654,-14012.428 4068.8135,-14010.8218 4063.8656,-14008.9294 4063.7654,-14012.428"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;runtime -->
+<g id="edge666" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M3848.6081,-13993.6469C3893.7199,-13991.7624 3950.6465,-13998.2014 3983,-14035.5998 4041.8015,-14103.5704 4035.4077,-17180.8984 4041,-17270.5998 4052.8372,-17460.4715 4082.7816,-17688.3234 4092.7302,-17761.0728"/>
+<polygon fill="#000000" stroke="#000000" points="4091.053,-17761.7236 4093.4664,-17766.4394 4094.5206,-17761.2479 4091.053,-17761.7236"/>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;net -->
+<g id="edge665" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M3848.5993,-13993.6545C3893.7071,-13991.7734 3950.6325,-13998.2135 3983,-14035.5998 4095.9275,-14166.0376 3953.592,-16991.8501 4041,-17140.5998 4046.2907,-17149.6035 4055.1736,-17156.321 4064.2744,-17161.2132"/>
+<polygon fill="#000000" stroke="#000000" points="4063.4926,-17162.7788 4068.7447,-17163.4702 4065.0701,-17159.6545 4063.4926,-17162.7788"/>
+</g>
+<!-- syscall -->
+<g id="node103" class="node">
+<title>syscall</title>
+<g id="a_node103"><a xlink:href="https://godoc.org/syscall" xlink:title="syscall" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4111,-18773.5998C4111,-18773.5998 4081,-18773.5998 4081,-18773.5998 4075,-18773.5998 4069,-18767.5998 4069,-18761.5998 4069,-18761.5998 4069,-18749.5998 4069,-18749.5998 4069,-18743.5998 4075,-18737.5998 4081,-18737.5998 4081,-18737.5998 4111,-18737.5998 4111,-18737.5998 4117,-18737.5998 4123,-18743.5998 4123,-18749.5998 4123,-18749.5998 4123,-18761.5998 4123,-18761.5998 4123,-18767.5998 4117,-18773.5998 4111,-18773.5998"/>
+<text text-anchor="middle" x="4096" y="-18751.8998" font-family="Times,serif" font-size="14.00" fill="#000000">syscall</text>
+</a>
+</g>
+</g>
+<!-- golang.org/x/sys/unix&#45;&gt;syscall -->
+<g id="edge670" class="edge">
+<title>golang.org/x/sys/unix&#45;&gt;syscall</title>
+<path fill="none" stroke="#000000" d="M3848.6663,-13993.5967C3893.8052,-13991.6887 3950.7403,-13998.1205 3983,-14035.5998 4064.3523,-14130.1149 4025.6589,-18400.8422 4041,-18524.5998 4050.6054,-18602.0871 4076.1029,-18691.3592 4088.7196,-18732.5644"/>
+<polygon fill="#000000" stroke="#000000" points="4087.1211,-18733.3198 4090.2655,-18737.583 4090.466,-18732.2894 4087.1211,-18733.3198"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;bytes -->
+<g id="edge108" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2700.1195,-12366.6531C2710.3498,-12372.2861 2719.5882,-12379.4689 2727,-12388.5998 2804.1352,-12483.626 2737.862,-13375.6494 2785,-13488.5998 2887.2705,-13733.6568 2970.9604,-13776.2007 3187,-13930.5998 3481.318,-14140.943 3699.8647,-14325.7703 3983,-14100.5998 4072.6118,-14029.3337 4007.7317,-13959.1551 4041,-13849.5998 4054.3854,-13805.5205 4074.4473,-13755.59 4086.2816,-13727.3207"/>
+<polygon fill="#000000" stroke="#000000" points="4087.926,-13727.9247 4088.25,-13722.6372 4084.6993,-13726.5686 4087.926,-13727.9247"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;compress/bzip2 -->
+<g id="edge109" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;compress/bzip2</title>
+<path fill="none" stroke="#000000" d="M2611.37,-12330.5144C2652.0507,-12310.0981 2721.8106,-12277.1842 2785,-12257.5998 2822.9236,-12245.8461 2866.9094,-12237.4972 2900.9064,-12232.1376"/>
+<polygon fill="#000000" stroke="#000000" points="2901.1766,-12233.8667 2905.8479,-12231.3685 2900.6383,-12230.4084 2901.1766,-12233.8667"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;fmt -->
+<g id="edge110" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2579.961,-12330.5447C2598.9352,-12230.6844 2690.7958,-11735.6989 2727,-11326.5998 2736.3681,-11220.7425 2722.8203,-9500.7812 2785,-9414.5998 2880.268,-9282.558 2978.2278,-9350.0715 3129,-9288.5998 3306.3873,-9216.2768 3382.159,-9242.5249 3522,-9111.5998 3562.0591,-9074.0948 3539.3379,-9039.4502 3580,-9002.5998 3723.5367,-8872.5185 3817.3427,-8937.0071 3983,-8836.5998 4017.8086,-8815.5018 4052.9172,-8784.02 4074.5576,-8763.1573"/>
+<polygon fill="#000000" stroke="#000000" points="4075.8055,-8764.385 4078.1755,-8759.6472 4073.3683,-8761.8729 4075.8055,-8764.385"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;io -->
+<g id="edge118" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2576.821,-12366.8678C2580.265,-12550.7864 2613.7056,-14070.0671 2785,-14483.5998 2892.6684,-14743.5288 2944.8844,-14829.2974 3187,-14972.5998 3339.8802,-15063.0859 3854.2075,-15168.9624 3983,-15046.5998 4074.305,-14959.8532 3976.1636,-14022.5712 4041,-13914.5998 4046.4191,-13905.5754 4055.3356,-13898.716 4064.4277,-13893.6533"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2768,-13895.184 4068.8906,-13891.3106 4063.65,-13892.085 4065.2768,-13895.184"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;io/ioutil -->
+<g id="edge119" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2577.5304,-12366.8222C2588.5563,-12560.3527 2686.3024,-14239.9456 2785,-14730.5998 2883.9266,-15222.3926 3006.3306,-15319.1856 3129,-15805.5998 3163.3829,-15941.9365 3093.0522,-16011.9877 3187,-16116.5998 3308.8536,-16252.2854 3401.6156,-16196.6805 3580,-16234.5998 3758.896,-16272.6279 3977.655,-16291.7921 4061.2621,-16298.1403"/>
+<polygon fill="#000000" stroke="#000000" points="4061.2169,-16299.8918 4066.3342,-16298.5223 4061.4798,-16296.4017 4061.2169,-16299.8918"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/pkg/errors -->
+<g id="edge115" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M2579.9724,-12330.5457C2599.0069,-12230.6907 2691.136,-11735.7289 2727,-11326.5998 2736.8183,-11214.5943 2713.1031,-9390.0434 2785,-9303.5998 2885.0016,-9183.3652 3003.09,-9322.3542 3129,-9229.5998 3171.2984,-9198.4398 3144.7878,-9157.8764 3187,-9126.5998 3309.5134,-9035.8251 3412.2031,-9158.4025 3522,-9052.5998 3595.7426,-8981.5398 3503.1665,-8897.3059 3580,-8829.5998 3614.2035,-8799.4595 3665.2311,-8793.1258 3707.3767,-8794.2428"/>
+<polygon fill="#000000" stroke="#000000" points="3707.4144,-8795.9949 3712.4707,-8794.4148 3707.5327,-8792.4969 3707.4144,-8795.9949"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge116" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M2721.6794,-12342.8879C2833.1449,-12338.2311 2990.8607,-12331.0555 3129,-12322.5998 3174.47,-12319.8165 3225.0477,-12316.0639 3266.7616,-12312.7923"/>
+<polygon fill="#000000" stroke="#000000" points="3266.9563,-12314.5325 3271.8037,-12312.3959 3266.6819,-12311.0433 3266.9563,-12314.5325"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression/internal -->
+<g id="node78" class="node">
+<title>github.com/containers/image/v5/pkg/compression/internal</title>
+<g id="a_node78"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/compression/internal" xlink:title="github.com/containers/image/v5/pkg/compression/internal" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3937,-14563.5998C3937,-14563.5998 3626,-14563.5998 3626,-14563.5998 3620,-14563.5998 3614,-14557.5998 3614,-14551.5998 3614,-14551.5998 3614,-14539.5998 3614,-14539.5998 3614,-14533.5998 3620,-14527.5998 3626,-14527.5998 3626,-14527.5998 3937,-14527.5998 3937,-14527.5998 3943,-14527.5998 3949,-14533.5998 3949,-14539.5998 3949,-14539.5998 3949,-14551.5998 3949,-14551.5998 3949,-14557.5998 3943,-14563.5998 3937,-14563.5998"/>
+<text text-anchor="middle" x="3781.5" y="-14541.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/compression/internal</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/containers/image/v5/pkg/compression/internal -->
+<g id="edge111" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/containers/image/v5/pkg/compression/internal</title>
+<path fill="none" stroke="#000000" d="M2700.7019,-12366.6635C2710.7462,-12372.3037 2719.7841,-12379.4851 2727,-12388.5998 2868.5769,-12567.4336 2630.0196,-14273.2481 2785,-14440.5998 2798.8509,-14455.5564 3326.9195,-14504.9254 3608.5688,-14530.2782"/>
+<polygon fill="#000000" stroke="#000000" points="3608.7994,-14532.056 3613.9362,-14530.7612 3609.1132,-14528.5701 3608.7994,-14532.056"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/containers/image/v5/pkg/compression/types -->
+<g id="edge112" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/containers/image/v5/pkg/compression/types</title>
+<path fill="none" stroke="#000000" d="M2700.6957,-12366.6685C2710.7412,-12372.3077 2719.7811,-12379.4874 2727,-12388.5998 2865.3946,-12563.295 2629.8448,-14233.605 2785,-14393.5998 2839.0053,-14449.2897 3040.1432,-14449.5577 3188.1375,-14441.3769"/>
+<polygon fill="#000000" stroke="#000000" points="3188.4854,-14443.1102 3193.3794,-14441.0824 3188.289,-14439.6158 3188.4854,-14443.1102"/>
+</g>
+<!-- github.com/klauspost/compress/zstd -->
+<g id="node80" class="node">
+<title>github.com/klauspost/compress/zstd</title>
+<g id="a_node80"><a xlink:href="https://godoc.org/github.com/klauspost/compress/zstd" xlink:title="github.com/klauspost/compress/zstd" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3052.5,-11606.5998C3052.5,-11606.5998 2861.5,-11606.5998 2861.5,-11606.5998 2855.5,-11606.5998 2849.5,-11600.5998 2849.5,-11594.5998 2849.5,-11594.5998 2849.5,-11582.5998 2849.5,-11582.5998 2849.5,-11576.5998 2855.5,-11570.5998 2861.5,-11570.5998 2861.5,-11570.5998 3052.5,-11570.5998 3052.5,-11570.5998 3058.5,-11570.5998 3064.5,-11576.5998 3064.5,-11582.5998 3064.5,-11582.5998 3064.5,-11594.5998 3064.5,-11594.5998 3064.5,-11600.5998 3058.5,-11606.5998 3052.5,-11606.5998"/>
+<text text-anchor="middle" x="2957" y="-11584.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/zstd</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/klauspost/compress/zstd -->
+<g id="edge113" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/klauspost/compress/zstd</title>
+<path fill="none" stroke="#000000" d="M2585.5304,-12330.5628C2636.5912,-12228.5753 2888.8602,-11724.7004 2945.6379,-11611.2941"/>
+<polygon fill="#000000" stroke="#000000" points="2947.2116,-11612.0599 2947.8852,-11606.8054 2944.0819,-11610.4929 2947.2116,-11612.0599"/>
+</g>
+<!-- github.com/klauspost/pgzip -->
+<g id="node81" class="node">
+<title>github.com/klauspost/pgzip</title>
+<g id="a_node81"><a xlink:href="https://godoc.org/github.com/klauspost/pgzip" xlink:title="github.com/klauspost/pgzip" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3426,-11848.5998C3426,-11848.5998 3283,-11848.5998 3283,-11848.5998 3277,-11848.5998 3271,-11842.5998 3271,-11836.5998 3271,-11836.5998 3271,-11824.5998 3271,-11824.5998 3271,-11818.5998 3277,-11812.5998 3283,-11812.5998 3283,-11812.5998 3426,-11812.5998 3426,-11812.5998 3432,-11812.5998 3438,-11818.5998 3438,-11824.5998 3438,-11824.5998 3438,-11836.5998 3438,-11836.5998 3438,-11842.5998 3432,-11848.5998 3426,-11848.5998"/>
+<text text-anchor="middle" x="3354.5" y="-11826.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/pgzip</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/klauspost/pgzip -->
+<g id="edge114" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/klauspost/pgzip</title>
+<path fill="none" stroke="#000000" d="M2598.0858,-12330.3693C2634.94,-12299.7302 2713.0091,-12236.9197 2785,-12192.5998 2930.3933,-12103.0911 2986.376,-12115.4583 3129,-12021.5998 3209.7532,-11968.4575 3293.794,-11890.0727 3332.5366,-11852.3923"/>
+<polygon fill="#000000" stroke="#000000" points="3333.7852,-11853.619 3336.1428,-11848.8751 3331.3414,-11851.1134 3333.7852,-11853.619"/>
+</g>
+<!-- github.com/ulikunitz/xz -->
+<g id="node82" class="node">
+<title>github.com/ulikunitz/xz</title>
+<g id="a_node82"><a xlink:href="https://godoc.org/github.com/ulikunitz/xz" xlink:title="github.com/ulikunitz/xz" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3018.5,-13238.5998C3018.5,-13238.5998 2895.5,-13238.5998 2895.5,-13238.5998 2889.5,-13238.5998 2883.5,-13232.5998 2883.5,-13226.5998 2883.5,-13226.5998 2883.5,-13214.5998 2883.5,-13214.5998 2883.5,-13208.5998 2889.5,-13202.5998 2895.5,-13202.5998 2895.5,-13202.5998 3018.5,-13202.5998 3018.5,-13202.5998 3024.5,-13202.5998 3030.5,-13208.5998 3030.5,-13214.5998 3030.5,-13214.5998 3030.5,-13226.5998 3030.5,-13226.5998 3030.5,-13232.5998 3024.5,-13238.5998 3018.5,-13238.5998"/>
+<text text-anchor="middle" x="2957" y="-13216.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ulikunitz/xz</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/ulikunitz/xz -->
+<g id="edge117" class="edge">
+<title>github.com/containers/image/v5/pkg/compression&#45;&gt;github.com/ulikunitz/xz</title>
+<path fill="none" stroke="#000000" d="M2694.4765,-12366.6534C2706.4933,-12372.2085 2717.6823,-12379.3742 2727,-12388.5998 2757.5922,-12418.8896 2918.6126,-13065.2953 2951.2333,-13197.2165"/>
+<polygon fill="#000000" stroke="#000000" points="2949.6164,-13197.9687 2952.5151,-13202.4028 2953.0142,-13197.1289 2949.6164,-13197.9687"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/strslice&#45;&gt;encoding/json -->
+<g id="edge136" class="edge">
+<title>github.com/containers/image/v5/pkg/strslice&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M2971.1259,-17522.9023C3016.4642,-17581.3942 3157.0914,-17760.7883 3187,-17774.5998 3347.5926,-17848.7597 3845.645,-17886.0596 3983,-17774.5998 4064.8629,-17708.1704 4089.637,-17355.2528 4094.8474,-17260.8423"/>
+<polygon fill="#000000" stroke="#000000" points="4096.5991,-17260.8559 4095.1222,-17255.7686 4093.1042,-17260.6666 4096.5991,-17260.8559"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;bytes -->
+<g id="edge172" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3785.0891,-16846.3275C3807.8692,-16729.4795 3932.9629,-16074.6783 3983,-15534.5998 4017.564,-15161.5313 3967.125,-14216.9107 4041,-13849.5998 4050.0331,-13804.6868 4071.3565,-13755.4192 4084.6415,-13727.4278"/>
+<polygon fill="#000000" stroke="#000000" points="4086.2819,-13728.0539 4086.8625,-13722.7884 4083.125,-13726.5426 4086.2819,-13728.0539"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto -->
+<g id="edge173" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto</title>
+<path fill="none" stroke="#000000" d="M3814.6144,-16846.3908C3858.7032,-16820.6107 3936.5095,-16769.5298 3983,-16706.5998 4025.718,-16648.7763 4013.1203,-16621.8653 4041,-16555.5998 4055.4142,-16521.3395 4073.6402,-16482.3246 4085.0625,-16458.331"/>
+<polygon fill="#000000" stroke="#000000" points="4086.68,-16459.0047 4087.2531,-16453.7384 4083.5209,-16457.4979 4086.68,-16459.0047"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto/ecdsa -->
+<g id="edge174" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto/ecdsa</title>
+<path fill="none" stroke="#000000" d="M3871.6964,-16864.8796C3909.6514,-16860.7126 3952.0842,-16849.8945 3983,-16824.5998 4034.8089,-16782.2108 3997.2222,-16736.2406 4041,-16685.5998 4044.8119,-16681.1903 4049.3644,-16677.2044 4054.1952,-16673.6532"/>
+<polygon fill="#000000" stroke="#000000" points="4055.2167,-16675.0743 4058.3084,-16670.7726 4053.209,-16672.2074 4055.2167,-16675.0743"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto/elliptic -->
+<g id="edge175" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto/elliptic</title>
+<path fill="none" stroke="#000000" d="M3871.5135,-16863.2271C3909.8865,-16867.1218 3952.6977,-16878.0292 3983,-16904.5998 4043.3409,-16957.5098 3990.1996,-17013.4724 4041,-17075.5998 4044.2517,-17079.5766 4048.1368,-17083.1644 4052.3178,-17086.3735"/>
+<polygon fill="#000000" stroke="#000000" points="4051.5073,-17087.9468 4056.585,-17089.4566 4053.5571,-17085.1098 4051.5073,-17087.9468"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto/rand -->
+<g id="edge176" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto/rand</title>
+<path fill="none" stroke="#000000" d="M3784.927,-16882.8288C3806.2209,-16996.696 3921.5295,-17622.096 3983,-18136.5998 4020.9274,-18454.0497 4001.0757,-18536.3949 4041,-18853.5998 4055.1304,-18965.8684 4080.78,-19098.7776 4091.3032,-19151.4457"/>
+<polygon fill="#000000" stroke="#000000" points="4089.6345,-19152.0254 4092.3328,-19156.5841 4093.0662,-19151.3377 4089.6345,-19152.0254"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto/rsa -->
+<g id="edge177" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto/rsa</title>
+<path fill="none" stroke="#000000" d="M3871.7378,-16866.0503C3909.1506,-16870.5964 3951.1545,-16881.2319 3983,-16904.5998 4026.2965,-16936.3704 4003.5288,-16972.1309 4041,-17010.5998 4045.484,-17015.2032 4050.7701,-17019.3669 4056.2623,-17023.0529"/>
+<polygon fill="#000000" stroke="#000000" points="4055.3414,-17024.5413 4060.4925,-17025.7779 4057.2368,-17021.599 4055.3414,-17024.5413"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto/sha256 -->
+<g id="edge178" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto/sha256</title>
+<path fill="none" stroke="#000000" d="M3871.6333,-16866.7233C3910.2607,-16863.0117 3953.2402,-16852.0405 3983,-16824.5998 4052.2972,-16760.7027 3983.6786,-16695.4276 4041,-16620.5998 4044.37,-16616.2006 4048.5082,-16612.2647 4052.9886,-16608.7782"/>
+<polygon fill="#000000" stroke="#000000" points="4054.188,-16610.069 4057.1873,-16605.7023 4052.1196,-16607.2455 4054.188,-16610.069"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto/sha512 -->
+<g id="edge179" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto/sha512</title>
+<path fill="none" stroke="#000000" d="M3871.6879,-16874.2561C3907.3634,-16880.1704 3948.099,-16889.6109 3983,-16904.5998 4012.0062,-16917.0571 4014.4423,-16928.5344 4041,-16945.5998 4046.7767,-16949.3118 4052.9905,-16953.1077 4059.0887,-16956.7279"/>
+<polygon fill="#000000" stroke="#000000" points="4058.5242,-16958.4265 4063.7203,-16959.4578 4060.3014,-16955.4113 4058.5242,-16958.4265"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto/tls -->
+<g id="edge180" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto/tls</title>
+<path fill="none" stroke="#000000" d="M3785.1114,-16882.808C3807.5191,-16996.5497 3928.3814,-17621.3236 3983,-18136.5998 4008.1239,-18373.6207 3943.8652,-18989.9419 4041,-19207.5998 4046.7328,-19220.4458 4056.9072,-19231.9443 4066.9591,-19241.065"/>
+<polygon fill="#000000" stroke="#000000" points="4066.0343,-19242.582 4070.945,-19244.5689 4068.3452,-19239.9533 4066.0343,-19242.582"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto/x509 -->
+<g id="edge181" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto/x509</title>
+<path fill="none" stroke="#000000" d="M3789.4609,-16882.9264C3819.6234,-16953.1094 3927.9478,-17212.0587 3983,-17435.5998 3992.3708,-17473.6503 4075.2746,-18067.6848 4092.7679,-18193.358"/>
+<polygon fill="#000000" stroke="#000000" points="4091.0346,-18193.6003 4093.4572,-18198.3113 4094.5012,-18193.1178 4091.0346,-18193.6003"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;crypto/x509/pkix -->
+<g id="edge182" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;crypto/x509/pkix</title>
+<path fill="none" stroke="#000000" d="M3871.6231,-16859.7283C3924.3374,-16856.8789 3989.4416,-16853.3597 4035.9196,-16850.8474"/>
+<polygon fill="#000000" stroke="#000000" points="4036.0263,-16852.5943 4040.9245,-16850.5769 4035.8373,-16849.0994 4036.0263,-16852.5943"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;encoding/base32 -->
+<g id="edge183" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;encoding/base32</title>
+<path fill="none" stroke="#000000" d="M3871.5046,-16849.9154C3906.5954,-16843.3639 3946.9679,-16834.8017 3983,-16824.5998 4004.626,-16818.4768 4027.9933,-16810.0999 4047.8353,-16802.4377"/>
+<polygon fill="#000000" stroke="#000000" points="4048.5105,-16804.0529 4052.5367,-16800.6102 4047.2424,-16800.7907 4048.5105,-16804.0529"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;encoding/base64 -->
+<g id="edge184" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;encoding/base64</title>
+<path fill="none" stroke="#000000" d="M3871.6231,-16878.3547C3924.8628,-16886.4803 3990.7406,-16896.5348 4037.3038,-16903.6414"/>
+<polygon fill="#000000" stroke="#000000" points="4037.1083,-16905.3818 4042.3152,-16904.4063 4037.6365,-16901.9218 4037.1083,-16905.3818"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;encoding/binary -->
+<g id="edge185" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M3785.1119,-16846.3295C3808.033,-16729.4946 3933.8456,-16074.7593 3983,-15534.5998 4058.0338,-14710.0499 4013.506,-14500.1001 4041,-13672.5998 4061.7862,-13046.9856 4089.9044,-12280.1632 4095.1417,-12137.8827"/>
+<polygon fill="#000000" stroke="#000000" points="4096.8971,-12137.7666 4095.3323,-12132.7055 4093.3995,-12137.6377 4096.8971,-12137.7666"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;encoding/json -->
+<g id="edge186" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3871.738,-16861.212C3910.8588,-16864.5872 3954.2051,-16875.6431 3983,-16904.5998 4079.0649,-17001.2048 3962.1618,-17094.4894 4041,-17205.5998 4043.8767,-17209.6541 4047.4416,-17213.2719 4051.3637,-17216.4813"/>
+<polygon fill="#000000" stroke="#000000" points="4050.3561,-17217.9137 4055.3927,-17219.5556 4052.4793,-17215.1312 4050.3561,-17217.9137"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;encoding/pem -->
+<g id="edge187" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;encoding/pem</title>
+<path fill="none" stroke="#000000" d="M3789.9712,-16882.8105C3821.9775,-16952.5747 3936.0919,-17210.2091 3983,-17435.5998 4056.9002,-17790.6863 3987.7663,-17890.8327 4041,-18249.5998 4053.0927,-18331.0981 4077.8082,-18425.8719 4089.5411,-18468.614"/>
+<polygon fill="#000000" stroke="#000000" points="4087.8898,-18469.2091 4090.9058,-18473.5641 4091.2639,-18468.2788 4087.8898,-18469.2091"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;errors -->
+<g id="edge188" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3785.2815,-16846.3439C3809.2517,-16729.598 3940.4139,-16075.3168 3983,-15534.5998 4057.0592,-14594.2675 3940.7405,-7983.5005 4041,-7045.5998 4050.5963,-6955.8293 4077.1187,-6851.4736 4089.4894,-6805.8958"/>
+<polygon fill="#000000" stroke="#000000" points="4091.2267,-6806.1766 4090.8548,-6800.8922 4087.8502,-6805.2551 4091.2267,-6806.1766"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;fmt -->
+<g id="edge189" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3785.2466,-16846.3412C3809.0013,-16729.578 3939.0645,-16075.2088 3983,-15534.5998 4071.2998,-14448.1058 4018.6838,-11719.4476 4041,-10629.5998 4056.674,-9864.1353 4089.5071,-8924.4748 4095.162,-8765.0867"/>
+<polygon fill="#000000" stroke="#000000" points="4096.923,-8764.8065 4095.3516,-8759.7475 4093.4252,-8764.6822 4096.923,-8764.8065"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;io -->
+<g id="edge190" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3785.8471,-16846.5723C3810.7274,-16742.476 3935.6214,-16207.414 3983,-15762.5998 3993.8792,-15660.4604 3988.9954,-14003.1794 4041,-13914.5998 4046.3295,-13905.5221 4055.2225,-13898.6488 4064.3206,-13893.5897"/>
+<polygon fill="#000000" stroke="#000000" points="4065.171,-13895.1199 4068.7887,-13891.2502 4063.5474,-13892.0192 4065.171,-13895.1199"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;io/ioutil -->
+<g id="edge191" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M3818.2837,-16846.4671C3864.5696,-16821.6192 3942.852,-16772.4558 3983,-16706.5998 4054.3709,-16589.528 3995.3014,-16532.8718 4041,-16403.5998 4051.3015,-16374.4591 4068.8416,-16343.4939 4081.3439,-16323.2141"/>
+<polygon fill="#000000" stroke="#000000" points="4082.9337,-16323.9716 4084.0888,-16318.8016 4079.9618,-16322.1228 4082.9337,-16323.9716"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;sort -->
+<g id="edge198" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3785.2918,-16846.3447C3809.3255,-16729.6038 3940.8119,-16075.348 3983,-15534.5998 4076.5122,-14336.0026 3918.0602,-5910.5369 4041,-4714.5998 4050.6019,-4621.194 4077.4622,-4512.3577 4089.737,-4465.7252"/>
+<polygon fill="#000000" stroke="#000000" points="4091.432,-4466.1605 4091.0192,-4460.8792 4088.0484,-4465.2652 4091.432,-4466.1605"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;strings -->
+<g id="edge199" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3785.2758,-16846.3435C3809.2112,-16729.5948 3940.1957,-16075.2996 3983,-15534.5998 4049.5734,-14693.6507 4011.0011,-8785.6464 4041,-7942.5998 4052.9073,-7607.9726 4085.6689,-7201.5354 4094.0348,-7100.946"/>
+<polygon fill="#000000" stroke="#000000" points="4095.7887,-7100.9706 4094.4601,-7095.8425 4092.3008,-7100.6798 4095.7887,-7100.9706"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;sync -->
+<g id="edge200" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3785.2946,-16846.345C3809.3462,-16729.6054 3940.9235,-16075.3567 3983,-15534.5998 4084.0481,-14235.9537 3980.4511,-5112.7633 4041,-3811.5998 4051.9271,-3576.7811 4083.7079,-3293.5658 4093.2558,-3211.7512"/>
+<polygon fill="#000000" stroke="#000000" points="4095.009,-3211.8257 4093.8521,-3206.6562 4091.5327,-3211.4188 4095.009,-3211.8257"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;time -->
+<g id="edge201" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3785.2209,-16846.339C3808.8162,-16729.5628 3938.0669,-16075.1268 3983,-15534.5998 4120.6458,-13878.7778 3917.7074,-13453.5524 4041,-11796.5998 4052.0413,-11648.2138 4081.106,-11471.5199 4091.8932,-11408.956"/>
+<polygon fill="#000000" stroke="#000000" points="4093.6307,-11409.1785 4092.7589,-11403.9533 4090.1819,-11408.5817 4093.6307,-11409.1785"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;unicode -->
+<g id="edge202" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;unicode</title>
+<path fill="none" stroke="#000000" d="M3785.293,-16846.3448C3809.3344,-16729.6045 3940.8597,-16075.3517 3983,-15534.5998 4031.2958,-14914.8593 3959.2798,-4961.8243 4041,-4345.5998 4050.1169,-4276.8521 4074.9391,-4198.4175 4087.8906,-4160.5073"/>
+<polygon fill="#000000" stroke="#000000" points="4089.5896,-4160.948 4089.5608,-4155.6506 4086.2799,-4159.8097 4089.5896,-4160.948"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;net/url -->
+<g id="edge194" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M3785.2987,-16846.3453C3809.3752,-16729.6077 3941.0794,-16075.3688 3983,-15534.5998 4096.9501,-14064.6625 3983.1768,-3738.8129 4041,-2265.5998 4052.1239,-1982.1862 4084.7108,-1639.0008 4093.6874,-1547.7768"/>
+<polygon fill="#000000" stroke="#000000" points="4095.446,-1547.7753 4094.1954,-1542.6276 4091.9629,-1547.4316 4095.446,-1547.7753"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;os -->
+<g id="edge195" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3790.1713,-16882.7704C3822.9005,-16952.3894 3939.2848,-17209.5681 3983,-17435.5998 4011.5721,-17583.3336 3963.2057,-18659.7989 4041,-18788.5998 4046.3991,-18797.5389 4055.3103,-18804.2396 4064.4037,-18809.1361"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6165,-18810.699 4068.8678,-18811.3968 4065.1978,-18807.5766 4063.6165,-18810.699"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;path -->
+<g id="edge196" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M3785.2965,-16846.3451C3809.3594,-16729.6064 3940.9943,-16075.3622 3983,-15534.5998 4089.5144,-14163.3821 3947.0252,-4528.7339 4041,-3156.5998 4051.264,-3006.735 4080.9365,-2828.2925 4091.8814,-2765.6787"/>
+<polygon fill="#000000" stroke="#000000" points="4093.6187,-2765.9031 4092.7592,-2760.6759 4090.1714,-2765.2982 4093.6187,-2765.9031"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;path/filepath -->
+<g id="edge197" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M3785.1399,-16882.805C3807.7192,-16996.5287 3929.438,-17621.2127 3983,-18136.5998 4009.6566,-18393.0962 3961.8035,-19050.184 4041,-19295.5998 4048.5639,-19319.0391 4064.3258,-19342.2853 4077.0449,-19358.5273"/>
+<polygon fill="#000000" stroke="#000000" points="4075.724,-19359.6779 4080.2061,-19362.5015 4078.4631,-19357.4991 4075.724,-19359.6779"/>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;net -->
+<g id="edge193" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M3871.5857,-16861.935C3910.4377,-16865.4994 3953.6103,-16876.5044 3983,-16904.5998 4061.0745,-16979.2359 3976.2874,-17054.1218 4041,-17140.5998 4046.9755,-17148.5851 4055.5767,-17154.918 4064.2084,-17159.7709"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7545,-17161.5117 4068.9905,-17162.3158 4065.3987,-17158.4219 4063.7545,-17161.5117"/>
+</g>
+<!-- math/big -->
+<g id="node88" class="node">
+<title>math/big</title>
+<g id="a_node88"><a xlink:href="https://godoc.org/math/big" xlink:title="math/big" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4116.5,-16735.5998C4116.5,-16735.5998 4075.5,-16735.5998 4075.5,-16735.5998 4069.5,-16735.5998 4063.5,-16729.5998 4063.5,-16723.5998 4063.5,-16723.5998 4063.5,-16711.5998 4063.5,-16711.5998 4063.5,-16705.5998 4069.5,-16699.5998 4075.5,-16699.5998 4075.5,-16699.5998 4116.5,-16699.5998 4116.5,-16699.5998 4122.5,-16699.5998 4128.5,-16705.5998 4128.5,-16711.5998 4128.5,-16711.5998 4128.5,-16723.5998 4128.5,-16723.5998 4128.5,-16729.5998 4122.5,-16735.5998 4116.5,-16735.5998"/>
+<text text-anchor="middle" x="4096" y="-16713.8998" font-family="Times,serif" font-size="14.00" fill="#000000">math/big</text>
+</a>
+</g>
+</g>
+<!-- github.com/containers/libtrust&#45;&gt;math/big -->
+<g id="edge192" class="edge">
+<title>github.com/containers/libtrust&#45;&gt;math/big</title>
+<path fill="none" stroke="#000000" d="M3871.5021,-16860.3466C3908.2416,-16855.272 3949.8022,-16844.9989 3983,-16824.5998 4018.6029,-16802.7228 4009.673,-16778.2547 4041,-16750.5998 4046.4262,-16745.8096 4052.6427,-16741.3245 4058.8983,-16737.2984"/>
+<polygon fill="#000000" stroke="#000000" points="4059.9186,-16738.7242 4063.2262,-16734.5862 4058.06,-16735.7585 4059.9186,-16738.7242"/>
+</g>
+<!-- github.com/docker/docker/api/types/versions&#45;&gt;strconv -->
+<g id="edge302" class="edge">
+<title>github.com/docker/docker/api/types/versions&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3802.5551,-5811.4643C3842.2215,-5776.407 3928.89,-5695.4635 3983,-5612.5998 4035.8773,-5531.6239 4073.9889,-5421.7758 4088.9765,-5374.6356"/>
+<polygon fill="#000000" stroke="#000000" points="4090.6865,-5375.0317 4090.5223,-5369.7369 4087.3487,-5373.9785 4090.6865,-5375.0317"/>
+</g>
+<!-- github.com/docker/docker/api/types/versions&#45;&gt;strings -->
+<g id="edge303" class="edge">
+<title>github.com/docker/docker/api/types/versions&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3807.3322,-5847.8478C3852.3283,-5881.3125 3944.0026,-5957.3216 3983,-6046.5998 4051.5987,-6203.6455 4016.8993,-6645.9287 4041,-6815.5998 4053.6503,-6904.659 4078.6089,-7008.8074 4090.0348,-7054.3264"/>
+<polygon fill="#000000" stroke="#000000" points="4088.3753,-7054.9029 4091.294,-7059.3237 4091.7692,-7054.0477 4088.3753,-7054.9029"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go&#45;&gt;fmt -->
+<g id="edge491" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3922.0726,-9518.5508C3944.9759,-9511.7599 3966.5478,-9500.8067 3983,-9483.5998 4034.2426,-9430.0067 4083.6945,-8885.1314 4094.0305,-8764.9651"/>
+<polygon fill="#000000" stroke="#000000" points="4095.782,-8765.0216 4094.4654,-8759.8904 4092.2947,-8764.7227 4095.782,-8765.0216"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression/internal&#45;&gt;io -->
+<g id="edge120" class="edge">
+<title>github.com/containers/image/v5/pkg/compression/internal&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3949.3171,-14530.7998C3961.9131,-14524.4138 3973.4193,-14516.1701 3983,-14505.5998 4071.6236,-14407.8224 3970.4445,-14026.1188 4041,-13914.5998 4046.4755,-13905.9453 4055.1153,-13899.25 4063.9391,-13894.2233"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2582,-13895.5023 4068.8412,-13891.6004 4063.607,-13892.4163 4065.2582,-13895.5023"/>
+</g>
+<!-- github.com/containers/image/v5/pkg/compression/types&#45;&gt;github.com/containers/image/v5/pkg/compression/internal -->
+<g id="edge121" class="edge">
+<title>github.com/containers/image/v5/pkg/compression/types&#45;&gt;github.com/containers/image/v5/pkg/compression/internal</title>
+<path fill="none" stroke="#000000" d="M3455.0915,-14445.6585C3477.5914,-14451.3251 3500.9986,-14458.5415 3522,-14467.5998 3550.2977,-14479.8051 3551.4729,-14493.9408 3580,-14505.5998 3601.1901,-14514.2602 3624.4212,-14521.0924 3647.3376,-14526.4686"/>
+<polygon fill="#000000" stroke="#000000" points="3646.9569,-14528.1767 3652.2221,-14527.5939 3647.7426,-14524.766 3646.9569,-14528.1767"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;bytes -->
+<g id="edge447" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2963.9657,-11607.0273C2991.1564,-11679.9411 3090.5388,-11955.8374 3129,-12192.5998 3152.1483,-12335.098 3094.8699,-13379.453 3187,-13490.5998 3245.4114,-13561.0681 3908.0938,-13673.734 4063.5184,-13699.3173"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6139,-13701.1064 4068.8316,-13700.1905 4064.1815,-13697.6527 4063.6139,-13701.1064"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;crypto/rand -->
+<g id="edge448" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;crypto/rand</title>
+<path fill="none" stroke="#000000" d="M2964.2179,-11606.9886C2992.3519,-11679.7578 3094.7893,-11955.186 3129,-12192.5998 3155.1131,-12373.8185 3085.64,-18631.1261 3187,-18783.5998 3291.2419,-18940.4088 3883.4849,-19115.3678 4051.5414,-19162.4126"/>
+<polygon fill="#000000" stroke="#000000" points="4051.1023,-19164.1069 4056.3889,-19163.767 4052.0442,-19160.736 4051.1023,-19164.1069"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;encoding/binary -->
+<g id="edge449" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M2963.9367,-11607.032C2991.019,-11679.9635 3090.0502,-11955.9173 3129,-12192.5998 3150.4599,-12323.0031 3091.2994,-13289.4575 3187,-13380.5998 3443.1854,-13624.583 3721.958,-13619.3796 3983,-13380.5998 4078.436,-13293.3028 4093.8439,-12304.04 4095.7615,-12138.1384"/>
+<polygon fill="#000000" stroke="#000000" points="4097.5125,-12138.0469 4095.8193,-12133.0274 4094.0128,-12138.0072 4097.5125,-12138.0469"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;encoding/hex -->
+<g id="edge450" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;encoding/hex</title>
+<path fill="none" stroke="#000000" d="M2975.2678,-11570.4623C3012.4956,-11532.1096 3096.3739,-11438.2543 3129,-11340.5998 3182.0639,-11181.7722 3079.6415,-9963.1149 3187,-9834.5998 3284.7551,-9717.5805 3418.9187,-9872.9556 3522,-9760.5998 3655.0491,-9615.5799 3445.4039,-9465.1851 3580,-9321.5998 3618.2827,-9280.7603 3685.3637,-9274.9623 3731.5414,-9276.5892"/>
+<polygon fill="#000000" stroke="#000000" points="3731.7155,-9278.348 3736.7844,-9276.8088 3731.862,-9274.851 3731.7155,-9278.348"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;errors -->
+<g id="edge451" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2963.5039,-11570.4911C2990.435,-11494.4684 3093.4599,-11193.1272 3129,-10935.5998 3155.3354,-10744.7704 3112.452,-7646.2287 3187,-7468.5998 3269.0941,-7272.9904 3419.7416,-7314.4645 3522,-7128.5998 3576.9823,-7028.6642 3497.5902,-6957.4592 3580,-6878.5998 3584.4605,-6874.3315 3951.4688,-6808.4365 4063.736,-6788.3607"/>
+<polygon fill="#000000" stroke="#000000" points="4064.2731,-6790.0425 4068.887,-6787.4397 4063.657,-6786.5971 4064.2731,-6790.0425"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;fmt -->
+<g id="edge452" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2975.2291,-11570.4494C3012.3846,-11532.0724 3096.1351,-11438.1742 3129,-11340.5998 3176.428,-11199.7883 3090.3289,-10117.4359 3187,-10004.5998 3285.7877,-9889.2932 3415.9825,-10046.2962 3522,-9937.5998 3626.8938,-9830.0555 3470.949,-9707.9263 3580,-9604.5998 3710.9363,-9480.5369 3854.2341,-9682.9139 3983,-9556.5998 4041.3992,-9499.3127 4085.7645,-8891.972 4094.4509,-8764.8492"/>
+<polygon fill="#000000" stroke="#000000" points="4096.198,-8764.9497 4094.7915,-8759.8425 4092.7061,-8764.7121 4096.198,-8764.9497"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;io -->
+<g id="edge458" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2964.0715,-11607.0105C2991.658,-11679.8615 3092.3223,-11955.5546 3129,-12192.5998 3145.6489,-12300.2004 3120.7301,-14065.209 3187,-14151.5998 3297.9987,-14296.3 3399.566,-14243.0962 3580,-14269.5998 3757.2096,-14295.6298 3846.52,-14385.5912 3983,-14269.5998 4104.8183,-14166.0691 3951.2377,-14046.8914 4041,-13914.5998 4046.6974,-13906.203 4055.3075,-13899.6028 4064.0349,-13894.584"/>
+<polygon fill="#000000" stroke="#000000" points="4065.3171,-13895.8794 4068.8777,-13891.9571 4063.6482,-13892.8028 4065.3171,-13895.8794"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;io/ioutil -->
+<g id="edge459" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2964.1593,-11606.9972C2992.0741,-11679.7985 3093.8016,-11955.3304 3129,-12192.5998 3156.616,-12378.7568 3094.0299,-15415.9733 3187,-15579.5998 3289.7878,-15760.5052 3842.979,-15918.6966 3983,-16072.5998 4040.3968,-16135.6872 4075.4893,-16233.2907 4089.2755,-16277.471"/>
+<polygon fill="#000000" stroke="#000000" points="4087.6297,-16278.073 4090.7746,-16282.336 4090.9746,-16277.0423 4087.6297,-16278.073"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;math -->
+<g id="edge461" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M2963.5642,-11570.4993C2990.7355,-11494.5092 3094.583,-11193.2797 3129,-10935.5998 3142.3166,-10835.8986 3135.1927,-3779.8185 3187,-3693.5998 3375.6247,-3379.6871 3688.0554,-3263.5096 3983,-3480.5998 4039.713,-3522.3427 4080.3899,-3746.6903 4092.3624,-3820.2359"/>
+<polygon fill="#000000" stroke="#000000" points="4090.6499,-3820.6089 4093.1746,-3825.266 4094.1051,-3820.0509 4090.6499,-3820.6089"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;strconv -->
+<g id="edge465" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M2963.5144,-11570.4925C2990.4874,-11494.4756 3093.6559,-11193.1542 3129,-10935.5998 3157.8108,-10725.6541 3101.7328,-7315.6018 3187,-7121.5998 3269.4782,-6933.9434 3431.6884,-6988.6145 3522,-6804.5998 3636.3339,-6571.6385 3462.4948,-6454.9777 3580,-6223.5998 3688.8833,-6009.1992 3861.6499,-6070.1989 3983,-5862.5998 4033.2946,-5776.5586 4080.3726,-5462.7219 4092.8122,-5374.6955"/>
+<polygon fill="#000000" stroke="#000000" points="4094.5502,-5374.9025 4093.5141,-5369.7074 4091.0844,-5374.4147 4094.5502,-5374.9025"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;strings -->
+<g id="edge466" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2975.2826,-11570.4673C3012.5381,-11532.1238 3096.4652,-11438.2848 3129,-11340.5998 3240.2338,-11006.6226 3067.1105,-10088.5684 3187,-9757.5998 3284.2205,-9489.2116 3385.1196,-9458.18 3580,-9249.5998 3743.5599,-9074.5419 3878.1991,-9107.0387 3983,-8891.5998 4029.2118,-8796.6023 4034.426,-8048.0363 4041,-7942.5998 4061.8368,-7608.4098 4088.0046,-7201.6498 4094.493,-7100.9684"/>
+<polygon fill="#000000" stroke="#000000" points="4096.2469,-7100.9624 4094.8222,-7095.8602 4092.7542,-7100.7373 4096.2469,-7100.9624"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;sync -->
+<g id="edge467" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2963.5664,-11570.4996C2990.7466,-11494.5107 3094.6247,-11193.2853 3129,-10935.5998 3156.7212,-10727.7956 3090.3995,-3574.6627 3187,-3388.5998 3236.47,-3293.3153 3477.7683,-3140.3886 3580,-3107.5998 3750.5535,-3052.8981 3812.4791,-3052.7966 3983,-3107.5998 4019.1279,-3119.2109 4053.4993,-3147.1094 4074.6462,-3166.9114"/>
+<polygon fill="#000000" stroke="#000000" points="4073.6064,-3168.3373 4078.4383,-3170.5089 4076.0153,-3165.7981 4073.6064,-3168.3373"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;math/bits -->
+<g id="edge462" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;math/bits</title>
+<path fill="none" stroke="#000000" d="M3064.777,-11578.1643C3088.1907,-11571.5151 3111.234,-11561.0256 3129,-11544.5998 3187.6172,-11490.4043 3125.5398,-11425.5486 3187,-11374.5998 3302.7957,-11278.6083 3404.6102,-11420.6352 3522,-11326.5998 3578.7572,-11281.1343 3526.6241,-11222.9912 3580,-11173.5998 3716.982,-11046.8435 3851.3977,-11187.9329 3983,-11055.5998 4036.7802,-11001.521 4079.7233,-10761.7661 4092.2715,-10685.1934"/>
+<polygon fill="#000000" stroke="#000000" points="4094.0461,-10685.1832 4093.1222,-10679.967 4090.5916,-10684.6209 4094.0461,-10685.1832"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;runtime -->
+<g id="edge463" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M2964.1905,-11606.9926C2992.2223,-11679.7767 3094.3285,-11955.2529 3129,-12192.5998 3275.9021,-13198.2307 3036.119,-15760.5582 3187,-16765.5998 3265.5271,-17288.6811 3396.2386,-17393.825 3522,-17907.5998 3553.6661,-18036.9657 3477.113,-18117.0272 3580,-18201.5998 3649.1827,-18258.4676 3911.8738,-18256.0172 3983,-18201.5998 4047.4122,-18152.3192 4083.6361,-17888.7355 4093.3319,-17808.0725"/>
+<polygon fill="#000000" stroke="#000000" points="4095.079,-17808.1999 4093.9322,-17803.0281 4091.6036,-17807.7862 4095.079,-17808.1999"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;log -->
+<g id="edge460" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;log</title>
+<path fill="none" stroke="#000000" d="M3056.1569,-11570.5525C3169.3009,-11549.5059 3359.6772,-11512.7108 3522,-11474.5998 3730.0471,-11425.7535 3977.1862,-11355.1386 4063.9711,-11329.9553"/>
+<polygon fill="#000000" stroke="#000000" points="4064.6515,-11331.58 4068.965,-11328.5049 4063.6753,-11328.2189 4064.6515,-11331.58"/>
+</g>
+<!-- github.com/klauspost/compress/huff0 -->
+<g id="node115" class="node">
+<title>github.com/klauspost/compress/huff0</title>
+<g id="a_node115"><a xlink:href="https://godoc.org/github.com/klauspost/compress/huff0" xlink:title="github.com/klauspost/compress/huff0" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3453.5,-10054.5998C3453.5,-10054.5998 3255.5,-10054.5998 3255.5,-10054.5998 3249.5,-10054.5998 3243.5,-10048.5998 3243.5,-10042.5998 3243.5,-10042.5998 3243.5,-10030.5998 3243.5,-10030.5998 3243.5,-10024.5998 3249.5,-10018.5998 3255.5,-10018.5998 3255.5,-10018.5998 3453.5,-10018.5998 3453.5,-10018.5998 3459.5,-10018.5998 3465.5,-10024.5998 3465.5,-10030.5998 3465.5,-10030.5998 3465.5,-10042.5998 3465.5,-10042.5998 3465.5,-10048.5998 3459.5,-10054.5998 3453.5,-10054.5998"/>
+<text text-anchor="middle" x="3354.5" y="-10032.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/huff0</text>
+</a>
+</g>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;github.com/klauspost/compress/huff0 -->
+<g id="edge453" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;github.com/klauspost/compress/huff0</title>
+<path fill="none" stroke="#000000" d="M2975.1941,-11570.4375C3012.2842,-11532.0384 3095.9189,-11438.1011 3129,-11340.5998 3172.3177,-11212.9275 3115.594,-10242.9581 3187,-10128.5998 3209.0303,-10093.3179 3249.6553,-10070.5283 3285.3786,-10056.4572"/>
+<polygon fill="#000000" stroke="#000000" points="3286.1228,-10058.0459 3290.1585,-10054.6143 3284.8637,-10054.7802 3286.1228,-10058.0459"/>
+</g>
+<!-- github.com/klauspost/compress/snappy -->
+<g id="node116" class="node">
+<title>github.com/klauspost/compress/snappy</title>
+<g id="a_node116"><a xlink:href="https://godoc.org/github.com/klauspost/compress/snappy" xlink:title="github.com/klauspost/compress/snappy" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3458,-12173.5998C3458,-12173.5998 3251,-12173.5998 3251,-12173.5998 3245,-12173.5998 3239,-12167.5998 3239,-12161.5998 3239,-12161.5998 3239,-12149.5998 3239,-12149.5998 3239,-12143.5998 3245,-12137.5998 3251,-12137.5998 3251,-12137.5998 3458,-12137.5998 3458,-12137.5998 3464,-12137.5998 3470,-12143.5998 3470,-12149.5998 3470,-12149.5998 3470,-12161.5998 3470,-12161.5998 3470,-12167.5998 3464,-12173.5998 3458,-12173.5998"/>
+<text text-anchor="middle" x="3354.5" y="-12151.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/snappy</text>
+</a>
+</g>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;github.com/klauspost/compress/snappy -->
+<g id="edge454" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;github.com/klauspost/compress/snappy</title>
+<path fill="none" stroke="#000000" d="M2986.5079,-11606.6914C3025.8753,-11632.517 3094.6006,-11683.8424 3129,-11746.5998 3200.1568,-11876.4165 3097.6805,-11956.5418 3187,-12074.5998 3209.2907,-12104.0626 3245.0981,-12123.3671 3277.8949,-12135.7094"/>
+<polygon fill="#000000" stroke="#000000" points="3277.4132,-12137.3967 3282.7099,-12137.4802 3278.6213,-12134.1119 3277.4132,-12137.3967"/>
+</g>
+<!-- hash/crc32 -->
+<g id="node117" class="node">
+<title>hash/crc32</title>
+<g id="a_node117"><a xlink:href="https://godoc.org/hash/crc32" xlink:title="hash/crc32" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3807,-12173.5998C3807,-12173.5998 3756,-12173.5998 3756,-12173.5998 3750,-12173.5998 3744,-12167.5998 3744,-12161.5998 3744,-12161.5998 3744,-12149.5998 3744,-12149.5998 3744,-12143.5998 3750,-12137.5998 3756,-12137.5998 3756,-12137.5998 3807,-12137.5998 3807,-12137.5998 3813,-12137.5998 3819,-12143.5998 3819,-12149.5998 3819,-12149.5998 3819,-12161.5998 3819,-12161.5998 3819,-12167.5998 3813,-12173.5998 3807,-12173.5998"/>
+<text text-anchor="middle" x="3781.5" y="-12151.8998" font-family="Times,serif" font-size="14.00" fill="#000000">hash/crc32</text>
+</a>
+</g>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;hash/crc32 -->
+<g id="edge457" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;hash/crc32</title>
+<path fill="none" stroke="#000000" d="M2985.1798,-11606.6339C3023.6567,-11632.7599 3092.0597,-11684.8307 3129,-11746.5998 3182.4455,-11835.9678 3117.0087,-11896.5006 3187,-11973.5998 3263.3011,-12057.6497 3615.5432,-12126.4461 3738.7819,-12148.3156"/>
+<polygon fill="#000000" stroke="#000000" points="3738.545,-12150.0508 3743.7733,-12149.1979 3739.1543,-12146.6043 3738.545,-12150.0508"/>
+</g>
+<!-- github.com/klauspost/compress/zstd/internal/xxhash -->
+<g id="node118" class="node">
+<title>github.com/klauspost/compress/zstd/internal/xxhash</title>
+<g id="a_node118"><a xlink:href="https://godoc.org/github.com/klauspost/compress/zstd/internal/xxhash" xlink:title="github.com/klauspost/compress/zstd/internal/xxhash" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3920.5,-11223.5998C3920.5,-11223.5998 3642.5,-11223.5998 3642.5,-11223.5998 3636.5,-11223.5998 3630.5,-11217.5998 3630.5,-11211.5998 3630.5,-11211.5998 3630.5,-11199.5998 3630.5,-11199.5998 3630.5,-11193.5998 3636.5,-11187.5998 3642.5,-11187.5998 3642.5,-11187.5998 3920.5,-11187.5998 3920.5,-11187.5998 3926.5,-11187.5998 3932.5,-11193.5998 3932.5,-11199.5998 3932.5,-11199.5998 3932.5,-11211.5998 3932.5,-11211.5998 3932.5,-11217.5998 3926.5,-11223.5998 3920.5,-11223.5998"/>
+<text text-anchor="middle" x="3781.5" y="-11201.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/zstd/internal/xxhash</text>
+</a>
+</g>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;github.com/klauspost/compress/zstd/internal/xxhash -->
+<g id="edge455" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;github.com/klauspost/compress/zstd/internal/xxhash</title>
+<path fill="none" stroke="#000000" d="M3048.4615,-11570.5792C3074.7353,-11564.5305 3103.2404,-11557.1206 3129,-11548.5998 3309.8477,-11488.7789 3354.6396,-11468.5665 3522,-11377.5998 3610.0453,-11329.7439 3706.6717,-11260.9889 3753.2685,-11226.7079"/>
+<polygon fill="#000000" stroke="#000000" points="3754.3629,-11228.0752 3757.3492,-11223.6997 3752.2861,-11225.2579 3754.3629,-11228.0752"/>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;hash -->
+<g id="edge456" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;hash</title>
+<path fill="none" stroke="#000000" d="M3064.8418,-11585.4779C3088.3908,-11589.7326 3111.478,-11598.4073 3129,-11614.5998 3212.4515,-11691.7196 3098.0036,-11792.9511 3187,-11863.5998 3459.639,-12080.0312 3936.6535,-11897.8219 4063.9967,-11842.9857"/>
+<polygon fill="#000000" stroke="#000000" points="4064.8412,-11844.5271 4068.7344,-11840.9347 4063.4507,-11841.3152 4064.8412,-11844.5271"/>
+</g>
+<!-- runtime/debug -->
+<g id="node120" class="node">
+<title>runtime/debug</title>
+<g id="a_node120"><a xlink:href="https://godoc.org/runtime/debug" xlink:title="runtime/debug" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3390.5,-11311.5998C3390.5,-11311.5998 3318.5,-11311.5998 3318.5,-11311.5998 3312.5,-11311.5998 3306.5,-11305.5998 3306.5,-11299.5998 3306.5,-11299.5998 3306.5,-11287.5998 3306.5,-11287.5998 3306.5,-11281.5998 3312.5,-11275.5998 3318.5,-11275.5998 3318.5,-11275.5998 3390.5,-11275.5998 3390.5,-11275.5998 3396.5,-11275.5998 3402.5,-11281.5998 3402.5,-11287.5998 3402.5,-11287.5998 3402.5,-11299.5998 3402.5,-11299.5998 3402.5,-11305.5998 3396.5,-11311.5998 3390.5,-11311.5998"/>
+<text text-anchor="middle" x="3354.5" y="-11289.8998" font-family="Times,serif" font-size="14.00" fill="#000000">runtime/debug</text>
+</a>
+</g>
+</g>
+<!-- github.com/klauspost/compress/zstd&#45;&gt;runtime/debug -->
+<g id="edge464" class="edge">
+<title>github.com/klauspost/compress/zstd&#45;&gt;runtime/debug</title>
+<path fill="none" stroke="#000000" d="M3064.5366,-11581.9197C3088.2356,-11575.8025 3111.4667,-11565.53 3129,-11548.5998 3200.1973,-11479.8517 3116.0065,-11402.5583 3187,-11333.5998 3217.0477,-11304.4134 3264.378,-11295.193 3301.0727,-11292.8262"/>
+<polygon fill="#000000" stroke="#000000" points="3301.5128,-11294.5542 3306.4062,-11292.5252 3301.3155,-11291.0598 3301.5128,-11294.5542"/>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;bufio -->
+<g id="edge471" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3365.759,-11848.9197C3444.3587,-11977.1737 3910.0224,-12743.0252 3983,-13003.5998 4037.7072,-13198.938 3934.3286,-13742.0568 4041,-13914.5998 4046.4915,-13923.4824 4055.4269,-13930.1683 4064.514,-13935.0687"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7222,-13936.6293 4068.9727,-13937.3327 4065.3068,-13933.5086 4063.7222,-13936.6293"/>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;bytes -->
+<g id="edge472" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3438.1999,-11842.7167C3593.1351,-11866.9979 3915.0715,-11926.7425 3983,-12012.5998 4037.2921,-12081.2216 4088.2131,-13482.0157 4095.1888,-13681.134"/>
+<polygon fill="#000000" stroke="#000000" points="4093.4495,-13681.4738 4095.3732,-13686.4096 4096.9474,-13681.3515 4093.4495,-13681.4738"/>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;errors -->
+<g id="edge473" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3362.7075,-11812.338C3391.2911,-11747.6968 3486.6441,-11523.0692 3522,-11326.5998 3538.1961,-11236.5998 3524.4665,-9754.252 3580,-9681.5998 3693.8836,-9532.6107 3870.0236,-9706.278 3983,-9556.5998 4067.0638,-9445.2269 4025.0406,-7184.2214 4041,-7045.5998 4051.3259,-6955.9103 4077.4747,-6851.5131 4089.6197,-6805.9102"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3573,-6806.1864 4090.9598,-6800.9039 4087.9763,-6805.2813 4091.3573,-6806.1864"/>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;fmt -->
+<g id="edge474" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3370.0403,-11812.4518C3404.9118,-11770.4693 3488.9217,-11662.0024 3522,-11554.5998 3594.8989,-11317.9024 3481.7744,-10668.9578 3580,-10441.5998 3682.7275,-10203.8217 3876.0027,-10256.4874 3983,-10020.5998 4036.9727,-9901.611 4086.8645,-8927.8016 4094.8804,-8764.7257"/>
+<polygon fill="#000000" stroke="#000000" points="4096.6296,-8764.7819 4095.1266,-8759.7022 4093.1338,-8764.6105 4096.6296,-8764.7819"/>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;io -->
+<g id="edge478" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3382.6101,-11848.7048C3424.5804,-11875.3239 3506.5643,-11925.7408 3580,-11961.5998 3753.4648,-12046.3037 3872.0574,-11965.6234 3983,-12123.5998 4086.1312,-12270.4532 4005.2817,-13561.7414 4041,-13737.5998 4050.0637,-13782.225 4071.3771,-13831.1456 4084.6518,-13858.9366"/>
+<polygon fill="#000000" stroke="#000000" points="4083.1242,-13859.7979 4086.871,-13863.5428 4086.2773,-13858.2787 4083.1242,-13859.7979"/>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;sync -->
+<g id="edge479" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3362.9029,-11812.3718C3392.1353,-11747.8427 3489.3797,-11523.5419 3522,-11326.5998 3539.7166,-11219.6375 3518.1917,-3609.6759 3580,-3520.5998 3689.3097,-3363.0662 3834.5207,-3487.9231 3983,-3366.5998 4035.7618,-3323.4878 4071.5388,-3248.9619 4087.1356,-3211.4988"/>
+<polygon fill="#000000" stroke="#000000" points="4088.8326,-3211.9731 4089.114,-3206.6831 4085.5952,-3210.643 4088.8326,-3211.9731"/>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;time -->
+<g id="edge480" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3438.2131,-11827.8517C3570.0793,-11819.1667 3827.1634,-11783.9978 3983,-11643.5998 4055.179,-11578.5715 4083.3846,-11459.0838 4092.3971,-11408.8332"/>
+<polygon fill="#000000" stroke="#000000" points="4094.1212,-11409.1326 4093.2588,-11403.9058 4090.6735,-11408.5296 4094.1212,-11409.1326"/>
+</g>
+<!-- github.com/klauspost/compress/flate -->
+<g id="node113" class="node">
+<title>github.com/klauspost/compress/flate</title>
+<g id="a_node113"><a xlink:href="https://godoc.org/github.com/klauspost/compress/flate" xlink:title="github.com/klauspost/compress/flate" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3878,-10491.5998C3878,-10491.5998 3685,-10491.5998 3685,-10491.5998 3679,-10491.5998 3673,-10485.5998 3673,-10479.5998 3673,-10479.5998 3673,-10467.5998 3673,-10467.5998 3673,-10461.5998 3679,-10455.5998 3685,-10455.5998 3685,-10455.5998 3878,-10455.5998 3878,-10455.5998 3884,-10455.5998 3890,-10461.5998 3890,-10467.5998 3890,-10467.5998 3890,-10479.5998 3890,-10479.5998 3890,-10485.5998 3884,-10491.5998 3878,-10491.5998"/>
+<text text-anchor="middle" x="3781.5" y="-10469.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/flate</text>
+</a>
+</g>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;github.com/klauspost/compress/flate -->
+<g id="edge475" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;github.com/klauspost/compress/flate</title>
+<path fill="none" stroke="#000000" d="M3370.0159,-11812.4443C3404.8368,-11770.4461 3488.7487,-11661.949 3522,-11554.5998 3556.5063,-11443.1989 3502.065,-10593.3579 3580,-10506.5998 3602.0927,-10482.006 3634.8306,-10470.9516 3667.5256,-10466.8359"/>
+<polygon fill="#000000" stroke="#000000" points="3668.0031,-10468.5426 3672.7709,-10466.2339 3667.604,-10465.0655 3668.0031,-10468.5426"/>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;hash/crc32 -->
+<g id="edge477" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;hash/crc32</title>
+<path fill="none" stroke="#000000" d="M3370.8306,-11848.85C3401.5378,-11883.4338 3469.5132,-11961.2677 3522,-12030.5998 3549.6047,-12067.064 3541.845,-12090.3838 3580,-12115.5998 3627.8919,-12147.2508 3695.2162,-12154.9139 3738.6049,-12156.2413"/>
+<polygon fill="#000000" stroke="#000000" points="3738.7317,-12157.995 3743.7746,-12156.3729 3738.8208,-12154.4961 3738.7317,-12157.995"/>
+</g>
+<!-- github.com/klauspost/pgzip&#45;&gt;hash -->
+<g id="edge476" class="edge">
+<title>github.com/klauspost/pgzip&#45;&gt;hash</title>
+<path fill="none" stroke="#000000" d="M3438.1181,-11830.3743C3601.0712,-11829.9348 3954.7033,-11828.9809 4063.4933,-11828.6875"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7964,-11830.4368 4068.7916,-11828.6732 4063.7869,-11826.9368 4063.7964,-11830.4368"/>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;bytes -->
+<g id="edge625" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3030.5591,-13213.2406C3064.8423,-13213.9657 3103.5828,-13221.4064 3129,-13246.5998 3244.7249,-13361.3058 3068.7345,-13496.515 3187,-13608.5998 3441.1923,-13849.5078 3932.6623,-13746.3993 4063.3997,-13713.3736"/>
+<polygon fill="#000000" stroke="#000000" points="4064.2137,-13714.9722 4068.6268,-13712.0418 4063.3495,-13711.5806 4064.2137,-13714.9722"/>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;crypto/sha256 -->
+<g id="edge626" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;crypto/sha256</title>
+<path fill="none" stroke="#000000" d="M2959.5047,-13238.9553C2976.8536,-13366.8048 3079.9022,-14137.7625 3129,-14768.5998 3140.1935,-14912.42 3111.9561,-15942.401 3187,-16065.5998 3388.4719,-16396.3542 3883.1555,-16537.9306 4044.3232,-16576.2844"/>
+<polygon fill="#000000" stroke="#000000" points="4044.1862,-16578.0502 4049.4547,-16577.4974 4044.9914,-16574.644 4044.1862,-16578.0502"/>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;errors -->
+<g id="edge627" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2961.7276,-13202.4414C2985.5576,-13109.895 3092.7897,-12681.1083 3129,-12322.5998 3142.4054,-12189.8765 3118.5115,-7635.0748 3187,-7520.5998 3274.323,-7374.644 3425.6294,-7475.7465 3522,-7335.5998 3627.7728,-7181.7801 3448.1298,-7051.731 3580,-6919.5998 3707.7847,-6791.5622 3814.8951,-6929.4079 3983,-6862.5998 4017.2767,-6848.9776 4051.0619,-6822.9425 4072.6294,-6804.2698"/>
+<polygon fill="#000000" stroke="#000000" points="4073.9007,-6805.4828 4076.5117,-6800.8735 4071.5962,-6802.8485 4073.9007,-6805.4828"/>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;fmt -->
+<g id="edge628" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2961.0521,-13202.3563C2981.5826,-13109.3947 3075.2447,-12678.9 3129,-12322.5998 3163.9551,-12090.9107 3089.0607,-12011.4604 3187,-11798.5998 3278.0833,-11600.64 3437.8496,-11641.6047 3522,-11440.5998 3583.1089,-11294.6328 3484.2659,-10143.5984 3580,-10017.5998 3693.8576,-9867.7482 3864.8781,-10034.1136 3983,-9887.5998 4056.1657,-9796.8479 4089.8088,-8920.2197 4095.2139,-8765.0234"/>
+<polygon fill="#000000" stroke="#000000" points="4096.9702,-8764.8669 4095.3943,-8759.8094 4093.4723,-8764.7458 4096.9702,-8764.8669"/>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;io -->
+<g id="edge634" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2959.7612,-13238.6286C2980.2146,-13371.1816 3109.5246,-14192.0032 3187,-14269.5998 3294.734,-14377.5024 3383.6067,-14279.593 3522,-14343.5998 3551.3672,-14357.1822 3549.3326,-14377.2836 3580,-14387.5998 3749.7635,-14444.7063 3848.8632,-14506.2932 3983,-14387.5998 4062.3076,-14317.4231 3983.3026,-14003.4001 4041,-13914.5998 4046.5798,-13906.0122 4055.2489,-13899.3357 4064.0675,-13894.3056"/>
+<polygon fill="#000000" stroke="#000000" points="4065.3849,-13895.5848 4068.9634,-13891.6788 4063.7301,-13892.5007 4065.3849,-13895.5848"/>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;hash/crc32 -->
+<g id="edge632" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;hash/crc32</title>
+<path fill="none" stroke="#000000" d="M2970.9648,-13202.5616C3069.2419,-13075.6179 3656.1197,-12317.5526 3764.3479,-12177.7551"/>
+<polygon fill="#000000" stroke="#000000" points="3765.7322,-12178.8256 3767.4093,-12173.8006 3762.9647,-12176.683 3765.7322,-12178.8256"/>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;hash -->
+<g id="edge631" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;hash</title>
+<path fill="none" stroke="#000000" d="M3030.8434,-13222.2892C3223.8082,-13222.2512 3744.6343,-13192.6807 3983,-12888.5998 4038.3927,-12817.9359 4028.245,-12171.4764 4041,-12082.5998 4053.3742,-11996.3763 4078.3646,-11895.7455 4089.9049,-11851.4752"/>
+<polygon fill="#000000" stroke="#000000" points="4091.6047,-11851.8921 4091.1778,-11846.6119 4088.2187,-11851.0058 4091.6047,-11851.8921"/>
+</g>
+<!-- github.com/ulikunitz/xz/internal/xlog -->
+<g id="node132" class="node">
+<title>github.com/ulikunitz/xz/internal/xlog</title>
+<g id="a_node132"><a xlink:href="https://godoc.org/github.com/ulikunitz/xz/internal/xlog" xlink:title="github.com/ulikunitz/xz/internal/xlog" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3879.5,-13300.5998C3879.5,-13300.5998 3683.5,-13300.5998 3683.5,-13300.5998 3677.5,-13300.5998 3671.5,-13294.5998 3671.5,-13288.5998 3671.5,-13288.5998 3671.5,-13276.5998 3671.5,-13276.5998 3671.5,-13270.5998 3677.5,-13264.5998 3683.5,-13264.5998 3683.5,-13264.5998 3879.5,-13264.5998 3879.5,-13264.5998 3885.5,-13264.5998 3891.5,-13270.5998 3891.5,-13276.5998 3891.5,-13276.5998 3891.5,-13288.5998 3891.5,-13288.5998 3891.5,-13294.5998 3885.5,-13300.5998 3879.5,-13300.5998"/>
+<text text-anchor="middle" x="3781.5" y="-13278.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ulikunitz/xz/internal/xlog</text>
+</a>
+</g>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;github.com/ulikunitz/xz/internal/xlog -->
+<g id="edge629" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;github.com/ulikunitz/xz/internal/xlog</title>
+<path fill="none" stroke="#000000" d="M3030.7026,-13222.8748C3138.6731,-13226.579 3346.1156,-13235.1048 3522,-13250.5998 3569.2411,-13254.7617 3621.1426,-13260.8561 3666.0329,-13266.6125"/>
+<polygon fill="#000000" stroke="#000000" points="3666.0086,-13268.3738 3671.1911,-13267.2765 3666.4555,-13264.9024 3666.0086,-13268.3738"/>
+</g>
+<!-- github.com/ulikunitz/xz/lzma -->
+<g id="node133" class="node">
+<title>github.com/ulikunitz/xz/lzma</title>
+<g id="a_node133"><a xlink:href="https://godoc.org/github.com/ulikunitz/xz/lzma" xlink:title="github.com/ulikunitz/xz/lzma" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3431.5,-13365.5998C3431.5,-13365.5998 3277.5,-13365.5998 3277.5,-13365.5998 3271.5,-13365.5998 3265.5,-13359.5998 3265.5,-13353.5998 3265.5,-13353.5998 3265.5,-13341.5998 3265.5,-13341.5998 3265.5,-13335.5998 3271.5,-13329.5998 3277.5,-13329.5998 3277.5,-13329.5998 3431.5,-13329.5998 3431.5,-13329.5998 3437.5,-13329.5998 3443.5,-13335.5998 3443.5,-13341.5998 3443.5,-13341.5998 3443.5,-13353.5998 3443.5,-13353.5998 3443.5,-13359.5998 3437.5,-13365.5998 3431.5,-13365.5998"/>
+<text text-anchor="middle" x="3354.5" y="-13343.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ulikunitz/xz/lzma</text>
+</a>
+</g>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;github.com/ulikunitz/xz/lzma -->
+<g id="edge630" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;github.com/ulikunitz/xz/lzma</title>
+<path fill="none" stroke="#000000" d="M2993.9994,-13238.6309C3038.7538,-13259.791 3116.8341,-13294.6435 3187,-13315.5998 3210.4382,-13322.6001 3236.1948,-13328.415 3260.332,-13333.0805"/>
+<polygon fill="#000000" stroke="#000000" points="3260.2171,-13334.8401 3265.4566,-13334.0587 3260.8734,-13331.4022 3260.2171,-13334.8401"/>
+</g>
+<!-- hash/crc64 -->
+<g id="node134" class="node">
+<title>hash/crc64</title>
+<g id="a_node134"><a xlink:href="https://godoc.org/hash/crc64" xlink:title="hash/crc64" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3380,-13300.5998C3380,-13300.5998 3329,-13300.5998 3329,-13300.5998 3323,-13300.5998 3317,-13294.5998 3317,-13288.5998 3317,-13288.5998 3317,-13276.5998 3317,-13276.5998 3317,-13270.5998 3323,-13264.5998 3329,-13264.5998 3329,-13264.5998 3380,-13264.5998 3380,-13264.5998 3386,-13264.5998 3392,-13270.5998 3392,-13276.5998 3392,-13276.5998 3392,-13288.5998 3392,-13288.5998 3392,-13294.5998 3386,-13300.5998 3380,-13300.5998"/>
+<text text-anchor="middle" x="3354.5" y="-13278.8998" font-family="Times,serif" font-size="14.00" fill="#000000">hash/crc64</text>
+</a>
+</g>
+</g>
+<!-- github.com/ulikunitz/xz&#45;&gt;hash/crc64 -->
+<g id="edge633" class="edge">
+<title>github.com/ulikunitz/xz&#45;&gt;hash/crc64</title>
+<path fill="none" stroke="#000000" d="M3030.8351,-13232.1162C3113.4393,-13245.0004 3244.4559,-13265.4357 3311.6545,-13275.917"/>
+<polygon fill="#000000" stroke="#000000" points="3311.5646,-13277.6741 3316.7746,-13276.7156 3312.104,-13274.2159 3311.5646,-13277.6741"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;bytes -->
+<g id="edge287" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3418.309,-15418.5633C3564.2286,-15375.9285 3914.9533,-15265.1037 3983,-15166.5998 4066.2524,-15046.0842 4011.4295,-13993.0591 4041,-13849.5998 4050.2486,-13804.7307 4071.5011,-13755.4486 4084.7142,-13727.4426"/>
+<polygon fill="#000000" stroke="#000000" points="4086.3548,-13728.0676 4086.9228,-13722.8007 4083.1943,-13726.5638 4086.3548,-13728.0676"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;encoding/json -->
+<g id="edge288" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3361.6124,-15454.7227C3388.4731,-15524.121 3484.1329,-15780.1337 3522,-16000.5998 3543.1882,-16123.9594 3498.9565,-17030.2139 3580,-17125.5998 3696.7388,-17262.9981 3938.6726,-17255.3842 4044.5262,-17244.4781"/>
+<polygon fill="#000000" stroke="#000000" points="4044.9814,-17246.1898 4049.7696,-17243.9236 4044.6132,-17242.7092 4044.9814,-17246.1898"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;fmt -->
+<g id="edge289" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3361.9189,-15418.1565C3389.4625,-15348.6567 3486.1067,-15095.4049 3522,-14876.5998 3552.1994,-14692.5046 3493.9291,-13364.1138 3580,-13198.5998 3683.5109,-12999.5488 3880.3809,-13094.112 3983,-12894.5998 4040.5742,-12782.664 4037.5562,-10755.4273 4041,-10629.5998 4061.9466,-9864.2615 4090.4347,-8924.497 4095.2844,-8765.0896"/>
+<polygon fill="#000000" stroke="#000000" points="4097.0439,-8764.8007 4095.4469,-8759.7498 4093.5455,-8764.6942 4097.0439,-8764.8007"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;io -->
+<g id="edge291" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3502.3049,-15450.4121C3666.9235,-15462.174 3918.4421,-15468.139 3983,-15402.5998 4099.1113,-15284.7236 3956.6896,-14056.9669 4041,-13914.5998 4046.3638,-13905.5424 4055.2659,-13898.6744 4064.3617,-13893.6139"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2115,-13895.1443 4068.8278,-13891.2732 4063.5868,-13892.0443 4065.2115,-13895.1443"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;strings -->
+<g id="edge294" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3362.0703,-15418.1805C3390.1514,-15348.7659 3488.458,-15095.7777 3522,-14876.5998 3540.6377,-14754.8127 3499.7316,-10535.0688 3580,-10441.5998 3698.2931,-10303.8527 3863.9671,-10511.7081 3983,-10374.5998 4027.3005,-10323.5722 4038.0499,-8010.1102 4041,-7942.5998 4055.6182,-7608.0801 4086.378,-7201.5635 4094.1739,-7100.9515"/>
+<polygon fill="#000000" stroke="#000000" points="4095.9278,-7100.9673 4094.5701,-7095.8468 4092.4383,-7100.6964 4095.9278,-7100.9673"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;os -->
+<g id="edge292" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3361.7994,-15454.6919C3389.3357,-15523.9789 3487.1172,-15779.6419 3522,-16000.5998 3542.5382,-16130.6947 3516.4705,-18255.2287 3580,-18370.5998 3683.8743,-18559.2382 3848.1416,-18471.7084 3983,-18639.5998 4027.502,-18695.0024 3994.8121,-18734.5947 4041,-18788.5998 4047.3007,-18795.9669 4055.7441,-18802.0432 4064.121,-18806.8482"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5281,-18808.5188 4068.754,-18809.3869 4065.2101,-18805.4494 4063.5281,-18808.5188"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/credentials -->
+<g id="edge290" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/credentials</title>
+<path fill="none" stroke="#000000" d="M3472.8763,-15454.6196C3530.3761,-15463.3725 3599.4467,-15473.8868 3657.6535,-15482.7473"/>
+<polygon fill="#000000" stroke="#000000" points="3657.687,-15484.5225 3662.8935,-15483.545 3658.2138,-15481.0624 3657.687,-15484.5225"/>
+</g>
+<!-- os/exec -->
+<g id="node99" class="node">
+<title>os/exec</title>
+<g id="a_node99"><a xlink:href="https://godoc.org/os/exec" xlink:title="os/exec" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3798,-19490.5998C3798,-19490.5998 3765,-19490.5998 3765,-19490.5998 3759,-19490.5998 3753,-19484.5998 3753,-19478.5998 3753,-19478.5998 3753,-19466.5998 3753,-19466.5998 3753,-19460.5998 3759,-19454.5998 3765,-19454.5998 3765,-19454.5998 3798,-19454.5998 3798,-19454.5998 3804,-19454.5998 3810,-19460.5998 3810,-19466.5998 3810,-19466.5998 3810,-19478.5998 3810,-19478.5998 3810,-19484.5998 3804,-19490.5998 3798,-19490.5998"/>
+<text text-anchor="middle" x="3781.5" y="-19468.8998" font-family="Times,serif" font-size="14.00" fill="#000000">os/exec</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;os/exec -->
+<g id="edge293" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;os/exec</title>
+<path fill="none" stroke="#000000" d="M3361.8182,-15454.689C3389.4222,-15523.9653 3487.4164,-15779.5949 3522,-16000.5998 3613.5805,-16585.8402 3501.3166,-18078.4864 3580,-18665.5998 3622.4466,-18982.324 3742.074,-19354.4879 3773.7328,-19449.5844"/>
+<polygon fill="#000000" stroke="#000000" points="3772.104,-19450.232 3775.3468,-19454.421 3775.424,-19449.1241 3772.104,-19450.232"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;bufio -->
+<g id="edge295" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3943.3471,-15490.6342C3958.4301,-15483.7586 3972.112,-15474.345 3983,-15461.5998 4090.0389,-15336.3029 3957.0178,-14121.3871 4041,-13979.5998 4046.3646,-13970.5428 4055.2668,-13963.6749 4064.3625,-13958.6144"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2124,-13960.1448 4068.8286,-13956.2737 4063.5876,-13957.0448 4065.2124,-13960.1448"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;bytes -->
+<g id="edge296" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3861.6228,-15483.5694C3905.2657,-15469.2944 3955.7845,-15444.5295 3983,-15402.5998 4077.0122,-15257.7597 4006.7053,-14018.8358 4041,-13849.5998 4050.0987,-13804.7001 4071.4005,-13755.428 4084.6636,-13727.4323"/>
+<polygon fill="#000000" stroke="#000000" points="4086.3041,-13728.0581 4086.8808,-13722.7921 4083.1461,-13726.549 4086.3041,-13728.0581"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;encoding/json -->
+<g id="edge297" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3784.2468,-15519.8167C3811.8246,-15702.6926 4037.8386,-17200.9538 4041,-17205.5998 4043.6355,-17209.4729 4046.907,-17212.9409 4050.5315,-17216.0317"/>
+<polygon fill="#000000" stroke="#000000" points="4049.9187,-17217.7758 4054.9316,-17219.4886 4052.081,-17215.0235 4049.9187,-17217.7758"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;fmt -->
+<g id="edge298" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3791.562,-15483.5958C3826.5737,-15419.8104 3943.1069,-15197.678 3983,-14997.5998 4030.4548,-14759.5966 4035.8553,-10872.2333 4041,-10629.5998 4057.2303,-9864.1469 4089.605,-8924.4769 4095.1749,-8765.087"/>
+<polygon fill="#000000" stroke="#000000" points="4096.9357,-8764.8058 4095.3617,-8759.7477 4093.4379,-8764.6834 4096.9357,-8764.8058"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;io -->
+<g id="edge299" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3943.368,-15490.6521C3958.4472,-15483.7731 3972.1225,-15474.3539 3983,-15461.5998 4094.6191,-15330.724 3953.4615,-14062.6685 4041,-13914.5998 4046.3571,-13905.5384 4055.2574,-13898.6694 4064.3536,-13893.6092"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2036,-13895.1395 4068.8201,-13891.2687 4063.5791,-13892.0394 4065.2036,-13895.1395"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;strings -->
+<g id="edge301" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3791.8428,-15483.1586C3827.2693,-15418.8158 3943.7039,-15197.3159 3983,-14997.5998 4058.6707,-14613.0159 4026.9476,-8334.3055 4041,-7942.5998 4053.0046,-7607.9761 4085.6943,-7201.5363 4094.0398,-7100.9462"/>
+<polygon fill="#000000" stroke="#000000" points="4095.7937,-7100.9704 4094.4641,-7095.8426 4092.3057,-7100.6804 4095.7937,-7100.9704"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;os -->
+<g id="edge300" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3785.1222,-15519.883C3808.1068,-15636.801 3934.2438,-16292.0039 3983,-16832.5998 3992.7653,-16940.8748 3985.4095,-18695.1733 4041,-18788.5998 4046.34,-18797.5743 4055.2358,-18804.2843 4064.3332,-18809.1784"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5489,-18810.7428 4068.8007,-18811.4371 4065.1282,-18807.6193 4063.5489,-18810.7428"/>
+</g>
+<!-- github.com/docker/docker/pkg/homedir&#45;&gt;os -->
+<g id="edge306" class="edge">
+<title>github.com/docker/docker/pkg/homedir&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M1848.6097,-15546.9899C1849.781,-15678.3161 1863.454,-16486.6598 2023,-17117.5998 2122.8506,-17512.4687 2273.4782,-17570.4216 2368,-17966.5998 2440.6527,-18271.116 2345.8636,-18365.967 2426,-18668.5998 2522.8599,-19034.3885 2549.5816,-19144.3543 2785,-19440.5998 3030.4645,-19749.4872 3187.4928,-19795.5713 3580,-19835.5998 3758.1869,-19853.7716 3851.1736,-19956.854 3983,-19835.5998 4039.8704,-19783.2903 4031.264,-19219.2532 4041,-19142.5998 4055.3007,-19030.0076 4080.8544,-18896.6696 4091.3275,-18843.8298"/>
+<polygon fill="#000000" stroke="#000000" points="4093.0938,-18843.9199 4092.3521,-18838.6746 4089.6609,-18843.2375 4093.0938,-18843.9199"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools -->
+<g id="node100" class="node">
+<title>github.com/docker/docker/pkg/idtools</title>
+<g id="a_node100"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/idtools" xlink:title="github.com/docker/docker/pkg/idtools" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2295.5,-15166.5998C2295.5,-15166.5998 2095.5,-15166.5998 2095.5,-15166.5998 2089.5,-15166.5998 2083.5,-15160.5998 2083.5,-15154.5998 2083.5,-15154.5998 2083.5,-15142.5998 2083.5,-15142.5998 2083.5,-15136.5998 2089.5,-15130.5998 2095.5,-15130.5998 2095.5,-15130.5998 2295.5,-15130.5998 2295.5,-15130.5998 2301.5,-15130.5998 2307.5,-15136.5998 2307.5,-15142.5998 2307.5,-15142.5998 2307.5,-15154.5998 2307.5,-15154.5998 2307.5,-15160.5998 2301.5,-15166.5998 2295.5,-15166.5998"/>
+<text text-anchor="middle" x="2195.5" y="-15144.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/idtools</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/docker/pkg/homedir&#45;&gt;github.com/docker/docker/pkg/idtools -->
+<g id="edge304" class="edge">
+<title>github.com/docker/docker/pkg/homedir&#45;&gt;github.com/docker/docker/pkg/idtools</title>
+<path fill="none" stroke="#000000" d="M1865.0031,-15510.5273C1922.3157,-15447.7641 2112.6467,-15239.3325 2175.3054,-15170.715"/>
+<polygon fill="#000000" stroke="#000000" points="2176.8165,-15171.6553 2178.8959,-15166.783 2174.232,-15169.2952 2176.8165,-15171.6553"/>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user -->
+<g id="node101" class="node">
+<title>github.com/opencontainers/runc/libcontainer/user</title>
+<g id="a_node101"><a xlink:href="https://godoc.org/github.com/opencontainers/runc/libcontainer/user" xlink:title="github.com/opencontainers/runc/libcontainer/user" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M3486,-14861.5998C3486,-14861.5998 3223,-14861.5998 3223,-14861.5998 3217,-14861.5998 3211,-14855.5998 3211,-14849.5998 3211,-14849.5998 3211,-14837.5998 3211,-14837.5998 3211,-14831.5998 3217,-14825.5998 3223,-14825.5998 3223,-14825.5998 3486,-14825.5998 3486,-14825.5998 3492,-14825.5998 3498,-14831.5998 3498,-14837.5998 3498,-14837.5998 3498,-14849.5998 3498,-14849.5998 3498,-14855.5998 3492,-14861.5998 3486,-14861.5998"/>
+<text text-anchor="middle" x="3354.5" y="-14839.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/runc/libcontainer/user</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/docker/pkg/homedir&#45;&gt;github.com/opencontainers/runc/libcontainer/user -->
+<g id="edge305" class="edge">
+<title>github.com/docker/docker/pkg/homedir&#45;&gt;github.com/opencontainers/runc/libcontainer/user</title>
+<path fill="none" stroke="#000000" d="M1850.4456,-15510.3956C1858.364,-15445.7497 1894.1661,-15225.2986 2023,-15116.5998 2366.8589,-14826.4816 2937.0652,-14817.075 3205.7482,-14831.1744"/>
+<polygon fill="#000000" stroke="#000000" points="3205.7741,-14832.9282 3210.8604,-14831.4474 3205.9608,-14829.4332 3205.7741,-14832.9282"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;crypto/tls -->
+<g id="edge351" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;crypto/tls</title>
+<path fill="none" stroke="#000000" d="M2959.9564,-9464.6054C2979.6311,-9585.423 3091.9734,-10290.9441 3129,-10870.5998 3135.9619,-10979.589 3134.2587,-18637.9679 3187,-18733.5998 3284.85,-18911.0239 3896.2286,-19178.4036 4057.198,-19246.418"/>
+<polygon fill="#000000" stroke="#000000" points="4056.541,-19248.0402 4061.828,-19248.3719 4057.9018,-19244.8155 4056.541,-19248.0402"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;errors -->
+<g id="edge352" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2959.7777,-9428.5488C2977.6557,-9311.7473 3077.4019,-8650.6719 3129,-8108.5998 3138.8633,-8004.9796 3125.8949,-7257.8648 3187,-7173.5998 3281.0416,-7043.9148 3415.7986,-7160.5301 3522,-7040.5998 3589.3222,-6964.5747 3501.8664,-6884.4631 3580,-6819.5998 3723.9524,-6700.0965 3975.2941,-6750.5781 4063.9397,-6773.5326"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5108,-6775.2292 4068.7911,-6774.8037 4064.3979,-6771.8435 4063.5108,-6775.2292"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;fmt -->
+<g id="edge353" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3081.655,-9461.5215C3209.4206,-9469.3862 3405.415,-9459.6491 3522,-9347.5998 3609.8663,-9263.1519 3492.4781,-9164.4047 3580,-9079.5998 3710.7822,-8952.8777 3840.7129,-9119.2522 3983,-9005.5998 4060.9579,-8943.3307 4086.0866,-8817.0814 4093.3419,-8764.9387"/>
+<polygon fill="#000000" stroke="#000000" points="4095.0963,-8765.0228 4094.0276,-8759.8343 4091.6274,-8764.5567 4095.0963,-8765.0228"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;strings -->
+<g id="edge359" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2996.6182,-9428.4042C3122.1656,-9370.5936 3504.0068,-9193.5572 3522,-9170.5998 3597.1774,-9074.6816 3491.6651,-8986.5575 3580,-8902.5998 3711.556,-8777.5628 3861.1141,-8971.0806 3983,-8836.5998 3983.9573,-8835.5436 4081.218,-7309.5942 4094.5032,-7101.0941"/>
+<polygon fill="#000000" stroke="#000000" points="4096.2651,-7100.9617 4094.8366,-7095.8605 4092.7721,-7100.7391 4096.2651,-7100.9617"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;sync -->
+<g id="edge360" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2959.6048,-9428.1923C2978.0813,-9296.6608 3089.7299,-8485.7022 3129,-7821.5998 3136.5039,-7694.7002 3132.2342,-3360.3192 3187,-3245.5998 3270.3533,-3070.9971 3409.8832,-3125.2821 3522,-2967.5998 3564.623,-2907.6544 3519.559,-2854.5171 3580,-2812.5998 3653.59,-2761.5634 3911.1372,-2759.1589 3983,-2812.5998 4041.6217,-2856.194 4081.2984,-3090.4023 4092.6559,-3165.4479"/>
+<polygon fill="#000000" stroke="#000000" points="4090.952,-3165.8863 4093.4246,-3170.5713 4094.4132,-3165.3669 4090.952,-3165.8863"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;time -->
+<g id="edge362" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2965.9244,-9464.8982C2994.0386,-9523.251 3081.6997,-9710.7162 3129,-9874.5998 3168.9599,-10013.0507 3085.8545,-10090.9595 3187,-10193.5998 3293.5142,-10301.6881 3416.742,-10150.2879 3522,-10259.5998 3636.5419,-10378.5531 3463.1342,-10509.9289 3580,-10626.5998 3708.2959,-10754.6817 3859.2391,-10557.1309 3983,-10689.5998 4084.117,-10797.8316 3961.6342,-11228.5405 4041,-11353.5998 4046.444,-11362.1781 4055.0749,-11368.7229 4063.9003,-11373.5881"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5587,-11375.3812 4068.8043,-11376.1204 4065.1646,-11372.2713 4063.5587,-11375.3812"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;net/http -->
+<g id="edge356" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M2959.6206,-9428.1932C2978.2068,-9296.6681 3090.4772,-8485.7459 3129,-7821.5998 3137.9502,-7667.2952 3136.83,-2403.7948 3187,-2257.5998 3266.7793,-2025.1237 3434.2403,-2047.1823 3522,-1817.5998 3579.9474,-1666.0074 3497.3539,-1229.2702 3580,-1089.5998 3689.165,-905.113 3843.0401,-986.9686 3983,-824.5998 4042.431,-755.6533 4076.8839,-650.1833 4089.9308,-603.8247"/>
+<polygon fill="#000000" stroke="#000000" points="4091.6928,-604.0196 4091.3441,-598.7337 4088.3203,-603.0833 4091.6928,-604.0196"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;net/url -->
+<g id="edge357" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M2959.61,-9428.1926C2978.1227,-9296.6632 3089.9766,-8485.7167 3129,-7821.5998 3144.8422,-7551.9901 3125.654,-3223.6151 3187,-2960.5998 3265.3429,-2624.7123 3431.9452,-2593.5385 3522,-2260.5998 3563.102,-2108.643 3467.3672,-1664.573 3580,-1554.5998 3714.2202,-1423.5491 3973.3152,-1486.5239 4063.8399,-1514.0322"/>
+<polygon fill="#000000" stroke="#000000" points="4063.4979,-1515.7578 4068.7914,-1515.5528 4064.5254,-1512.412 4063.4979,-1515.7578"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;os -->
+<g id="edge358" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2959.956,-9464.6054C2979.6279,-9585.4232 3091.9559,-10290.9453 3129,-10870.5998 3135.9085,-10978.7017 3118.8719,-18585.3839 3187,-18669.5998 3243.8024,-18739.8156 3907.7144,-18803.6522 4063.4514,-18817.7268"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6383,-18819.5006 4068.7752,-18818.2064 4063.9524,-18816.0147 4063.6383,-18819.5006"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;net -->
+<g id="edge355" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M2959.9304,-9464.607C2979.436,-9585.4356 3090.8756,-10291.0153 3129,-10870.5998 3147.9447,-11158.6065 3122.7005,-15784.224 3187,-16065.5998 3265.1783,-16407.7091 3414.0314,-16446.6935 3522,-16780.5998 3564.5875,-16912.3071 3481.0422,-16989.8121 3580,-17086.5998 3715.5928,-17219.2192 3973.3546,-17192.6948 4063.7225,-17178.4215"/>
+<polygon fill="#000000" stroke="#000000" points="4064.2632,-17180.107 4068.9214,-17177.5844 4063.7068,-17176.6515 4064.2632,-17180.107"/>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;syscall -->
+<g id="edge361" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;syscall</title>
+<path fill="none" stroke="#000000" d="M2959.9554,-9464.6054C2979.6239,-9585.4234 3091.9333,-10290.9467 3129,-10870.5998 3135.8409,-10977.5788 3118.0802,-18506.494 3187,-18588.5998 3302.8413,-18726.6043 3915.0341,-18750.9984 4063.531,-18754.9072"/>
+<polygon fill="#000000" stroke="#000000" points="4063.573,-18756.6587 4068.6163,-18755.0377 4063.6629,-18753.1599 4063.573,-18756.6587"/>
+</g>
+<!-- golang.org/x/net/proxy -->
+<g id="node106" class="node">
+<title>golang.org/x/net/proxy</title>
+<g id="a_node106"><a xlink:href="https://godoc.org/golang.org/x/net/proxy" xlink:title="golang.org/x/net/proxy" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3413,-7570.5998C3413,-7570.5998 3296,-7570.5998 3296,-7570.5998 3290,-7570.5998 3284,-7564.5998 3284,-7558.5998 3284,-7558.5998 3284,-7546.5998 3284,-7546.5998 3284,-7540.5998 3290,-7534.5998 3296,-7534.5998 3296,-7534.5998 3413,-7534.5998 3413,-7534.5998 3419,-7534.5998 3425,-7540.5998 3425,-7546.5998 3425,-7546.5998 3425,-7558.5998 3425,-7558.5998 3425,-7564.5998 3419,-7570.5998 3413,-7570.5998"/>
+<text text-anchor="middle" x="3354.5" y="-7548.8998" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/net/proxy</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/go&#45;connections/sockets&#45;&gt;golang.org/x/net/proxy -->
+<g id="edge354" class="edge">
+<title>github.com/docker/go&#45;connections/sockets&#45;&gt;golang.org/x/net/proxy</title>
+<path fill="none" stroke="#000000" d="M2972.3057,-9428.3133C3006.7608,-9386.0434 3090.4082,-9277.0201 3129,-9170.5998 3242.8797,-8856.5662 3338.1129,-7750.5161 3352.5947,-7575.8921"/>
+<polygon fill="#000000" stroke="#000000" points="3354.3505,-7575.8938 3353.0188,-7570.7665 3350.8624,-7575.6052 3354.3505,-7575.8938"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;context -->
+<g id="edge203" class="edge">
+<title>github.com/docker/distribution&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M2605.1925,-5756.4673C2641.3647,-5731.7305 2701.8558,-5683.6585 2727,-5624.5998 2867.6519,-5294.237 2650.4357,-2726.4887 2785,-2393.5998 2869.0281,-2185.7289 3029.4816,-2225.5155 3129,-2024.5998 3196.8464,-1887.6261 3133.2271,-1828.6852 3187,-1685.5998 3287.2879,-1418.7417 3400.0854,-1396.2968 3522,-1138.5998 3555.601,-1067.5758 3517.644,-1019.4032 3580,-971.5998 3727.6061,-858.442 3974.0576,-904.1454 4062.8497,-925.7422"/>
+<polygon fill="#000000" stroke="#000000" points="4062.4415,-927.4438 4067.7149,-926.94 4063.2783,-924.0453 4062.4415,-927.4438"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;errors -->
+<g id="edge204" class="edge">
+<title>github.com/docker/distribution&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2645.8994,-5756.5222C2671.4335,-5749.7742 2700.5649,-5741.962 2727,-5734.5998 2932.1354,-5677.4695 2976.1929,-5630.6792 3187,-5600.5998 3539.2686,-5550.3357 3741.87,-5424.9207 3983,-5686.5998 4058.4217,-5768.449 4090.1042,-6607.3592 4095.2389,-6759.1277"/>
+<polygon fill="#000000" stroke="#000000" points="4093.4934,-6759.2959 4095.4103,-6764.2343 4096.9915,-6759.1784 4093.4934,-6759.2959"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;fmt -->
+<g id="edge205" class="edge">
+<title>github.com/docker/distribution&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2578.4504,-5792.8643C2590.9802,-5907.073 2663.5011,-6522.608 2785,-6659.5998 2893.0381,-6781.4144 3032.5626,-6654.4097 3129,-6785.5998 3208.3925,-6893.6026 3097.9474,-7890.413 3187,-7990.5998 3287.2875,-8103.4261 3402.1948,-7954.7627 3522,-8046.5998 3571.2477,-8084.3508 3530.2368,-8136.531 3580,-8173.5998 3725.0196,-8281.6256 3850.1736,-8106.8912 3983,-8229.5998 4056.5875,-8297.582 4087.171,-8627.1798 4094.2992,-8718.2467"/>
+<polygon fill="#000000" stroke="#000000" points="4092.5674,-8718.5507 4094.6974,-8723.401 4096.057,-8718.2811 4092.5674,-8718.5507"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;io -->
+<g id="edge209" class="edge">
+<title>github.com/docker/distribution&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2579.7836,-5792.8339C2598.7705,-5899.2701 2694.773,-6451.2519 2727,-6906.5998 2751.8946,-7258.3458 2720.6573,-12906.8939 2785,-13253.5998 2796.6344,-13316.2908 3138.4456,-14287.272 3187,-14328.5998 3302.6245,-14427.0152 3384.5735,-14331.0353 3522,-14395.5998 3552.0299,-14409.7082 3548.7261,-14431.5192 3580,-14442.5998 3748.8274,-14502.4169 3849.6138,-14562.1361 3983,-14442.5998 4070.9051,-14363.8222 3977.3129,-14013.9839 4041,-13914.5998 4046.5255,-13905.9772 4055.1794,-13899.2909 4064.0007,-13894.2625"/>
+<polygon fill="#000000" stroke="#000000" points="4065.319,-13895.5416 4068.8999,-13891.6378 4063.6661,-13892.4565 4065.319,-13895.5416"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;strings -->
+<g id="edge212" class="edge">
+<title>github.com/docker/distribution&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2669.1699,-5766.6858C2788.0336,-5761.5496 2994.8174,-5770.1394 3129,-5873.5998 3177.5015,-5910.9965 3143.1005,-5955.8944 3187,-5998.5998 3463.1245,-6267.2135 3752.067,-6033.2688 3983,-6341.5998 4046.6158,-6426.5367 4023.639,-6710.9106 4041,-6815.5998 4055.7529,-6904.5617 4079.7361,-7009.1713 4090.484,-7054.6089"/>
+<polygon fill="#000000" stroke="#000000" points="4088.8097,-7055.1332 4091.6664,-7059.5944 4092.2152,-7054.3255 4088.8097,-7055.1332"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;time -->
+<g id="edge213" class="edge">
+<title>github.com/docker/distribution&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2579.691,-5792.8408C2598.1569,-5899.3156 2691.7229,-6451.4781 2727,-6906.5998 2749.099,-7191.7062 2703.4738,-9205.5059 2785,-9479.5998 2867.6286,-9757.3999 3013.5823,-9767.7443 3129,-10033.5998 3168.4132,-10124.385 3124.5871,-10171.7885 3187,-10248.5998 3292.2892,-10378.1788 3422.8183,-10284.2885 3522,-10418.5998 3612.0212,-10540.506 3470.074,-10650.2867 3580,-10754.5998 3710.8426,-10878.7615 3856.329,-10674.1851 3983,-10802.5998 4069.4625,-10890.2525 3974.1082,-11250.2352 4041,-11353.5998 4046.5199,-11362.1295 4055.1722,-11368.6605 4063.9938,-11373.5281"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6483,-11375.3197 4068.8933,-11376.0633 4065.2568,-11372.2112 4063.6483,-11375.3197"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge207" class="edge">
+<title>github.com/docker/distribution&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M2579.7187,-5792.8387C2598.3405,-5899.3016 2692.6354,-6451.4083 2727,-6906.5998 2740.0927,-7080.025 2715.2341,-9877.2874 2785,-10036.5998 2884.2374,-10263.2112 2998.3321,-10261.5835 3187,-10421.5998 3328.8849,-10541.9378 3415.3343,-10520.1696 3522,-10672.5998 3573.1081,-10745.6356 3529.6968,-10791.0073 3580,-10864.5998 3622.4428,-10926.6929 3696.797,-10975.7237 3742.344,-11001.8365"/>
+<polygon fill="#000000" stroke="#000000" points="3741.7208,-11003.4953 3746.9323,-11004.4454 3743.4508,-11000.4527 3741.7208,-11003.4953"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge208" class="edge">
+<title>github.com/docker/distribution&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M2579.7233,-5792.8383C2598.3713,-5899.2993 2692.7886,-6451.3967 2727,-6906.5998 2740.5256,-7086.5655 2727.3138,-9983.5943 2785,-10154.5998 2867.6278,-10399.5422 3002.2963,-10398.2772 3129,-10623.5998 3236.9678,-10815.6036 3322.4999,-11069.7663 3347.2425,-11146.6294"/>
+<polygon fill="#000000" stroke="#000000" points="3345.5899,-11147.2071 3348.784,-11151.4333 3348.9226,-11146.1377 3345.5899,-11147.2071"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;mime -->
+<g id="edge210" class="edge">
+<title>github.com/docker/distribution&#45;&gt;mime</title>
+<path fill="none" stroke="#000000" d="M2669.3155,-5765.9795C2690.5857,-5759.9738 2711.4781,-5750.2384 2727,-5734.5998 2799.7483,-5661.3046 2734.7763,-5599.8332 2785,-5509.5998 2800.7082,-5481.378 3158.9841,-5105.6725 3187,-5089.5998 3229.761,-5065.068 3286.9277,-5055.5434 3322.2022,-5051.8712"/>
+<polygon fill="#000000" stroke="#000000" points="3322.4409,-5053.6062 3327.2435,-5051.3708 3322.0951,-5050.1233 3322.4409,-5053.6062"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;net/http -->
+<g id="edge211" class="edge">
+<title>github.com/docker/distribution&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M2605.2093,-5756.4744C2641.3998,-5731.7454 2701.9105,-5683.6817 2727,-5624.5998 2805.2498,-5440.3336 2731.843,-2214.6061 2785,-2021.5998 2872.3069,-1704.5998 3388.9596,-1039.2853 3522,-738.5998 3556.0085,-661.7369 3512.0621,-608.0865 3580,-558.5998 3729.3753,-449.7934 3972.6862,-530.4451 4061.858,-566.0179"/>
+<polygon fill="#000000" stroke="#000000" points="4061.4567,-567.7427 4066.7486,-567.9856 4062.7632,-564.4957 4061.4567,-567.7427"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;github.com/docker/distribution/reference -->
+<g id="edge206" class="edge">
+<title>github.com/docker/distribution&#45;&gt;github.com/docker/distribution/reference</title>
+<path fill="none" stroke="#000000" d="M2669.0961,-5768.12C2691.0244,-5762.1513 2712.286,-5751.8983 2727,-5734.5998 2811.7846,-5634.9227 2698.667,-5246.9388 2785,-5148.5998 2797.572,-5134.2795 2814.1786,-5124.5551 2832.1195,-5118.0389"/>
+<polygon fill="#000000" stroke="#000000" points="2833.0552,-5119.5676 2837.2129,-5116.285 2831.9155,-5116.2584 2833.0552,-5119.5676"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;errors -->
+<g id="edge220" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2963.1631,-5126.9211C2983.8622,-5185.2406 3056.9943,-5367.1485 3187,-5442.5998 3494.0618,-5620.809 3731.3848,-5259.1298 3983,-5509.5998 4075.1683,-5601.3486 4093.2939,-6592.7803 4095.692,-6759.0154"/>
+<polygon fill="#000000" stroke="#000000" points="4093.9439,-6759.162 4095.7647,-6764.1367 4097.4435,-6759.1123 4093.9439,-6759.162"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;fmt -->
+<g id="edge221" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2960.7555,-5126.898C2981.8009,-5230.2843 3085.6952,-5751.9987 3129,-6183.5998 3136.7826,-6261.1659 3139.7299,-7523.6111 3187,-7585.5998 3281.7755,-7709.8859 3412.5152,-7581.0538 3522,-7692.5998 3585.0386,-7756.8252 3511.1357,-7828.6648 3580,-7886.5998 3718.0278,-8002.7215 3854.149,-7808.3726 3983,-7934.5998 4040.8177,-7991.2402 4085.5656,-8591.5074 4094.4074,-8718.2024"/>
+<polygon fill="#000000" stroke="#000000" points="4092.6854,-8718.6688 4094.7778,-8723.5355 4096.177,-8718.4262 4092.6854,-8718.6688"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;strings -->
+<g id="edge226" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2968.1783,-5126.6212C2998.393,-5176.0712 3082.5365,-5318.781 3129,-5447.5998 3168.5824,-5557.3409 3115.1996,-5611.651 3187,-5703.5998 3430.9274,-6015.9776 3761.7309,-5776.783 3983,-6105.5998 4071.3791,-6236.9356 4018.2495,-6658.9398 4041,-6815.5998 4053.9276,-6904.6191 4078.7443,-7008.788 4090.0843,-7054.3192"/>
+<polygon fill="#000000" stroke="#000000" points="4088.4235,-7054.8917 4091.3339,-7059.318 4091.819,-7054.0428 4088.4235,-7054.8917"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge223" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M2960.9389,-5126.8808C2982.9821,-5230.1737 3091.4006,-5751.4643 3129,-6183.5998 3138.3578,-6291.15 3116.9835,-9987.4275 3187,-10069.5998 3285.0499,-10184.6725 3419.5677,-10017.4102 3522,-10128.5998 3616.6584,-10231.351 3529.6699,-10624.2737 3580,-10754.5998 3620.4081,-10859.2336 3714.1414,-10958.184 3758.0349,-11000.7296"/>
+<polygon fill="#000000" stroke="#000000" points="3757.0557,-11002.2163 3761.8702,-11004.4262 3759.4847,-10999.6963 3757.0557,-11002.2163"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;path -->
+<g id="edge224" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M2965.265,-5090.5182C2994.4989,-5025.5126 3093.1749,-4796.9035 3129,-4596.5998 3204.9745,-4171.8146 3083.6823,-3074.5748 3187,-2655.5998 3282.4194,-2268.6544 3242.0335,-2060.8112 3580,-1849.5998 3655.9447,-1802.1383 3915.9913,-1790.1858 3983,-1849.5998 4063.5575,-1921.0271 4027.4322,-2223.795 4041,-2330.5998 4059.8404,-2478.9108 4084.2079,-2657.0045 4092.8173,-2719.5418"/>
+<polygon fill="#000000" stroke="#000000" points="4091.0896,-2719.8244 4093.5056,-2724.5389 4094.5569,-2719.3468 4091.0896,-2719.8244"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;regexp -->
+<g id="edge225" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M2965.3241,-5090.5287C2994.7573,-5025.5583 3094.0231,-4797.0533 3129,-4596.5998 3154.8756,-4448.3059 3091.3668,-2003.8536 3187,-1887.5998 3300.0298,-1750.1983 3843.9201,-1690.6418 3983,-1801.5998 4058.8974,-1862.1509 4087.778,-2184.5674 4094.4182,-2274.3682"/>
+<polygon fill="#000000" stroke="#000000" points="4092.68,-2274.5944 4094.7888,-2279.4539 4096.1707,-2274.34 4092.68,-2274.5944"/>
+</g>
+<!-- github.com/docker/distribution/digestset -->
+<g id="node91" class="node">
+<title>github.com/docker/distribution/digestset</title>
+<g id="a_node91"><a xlink:href="https://godoc.org/github.com/docker/distribution/digestset" xlink:title="github.com/docker/distribution/digestset" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3460.5,-5427.5998C3460.5,-5427.5998 3248.5,-5427.5998 3248.5,-5427.5998 3242.5,-5427.5998 3236.5,-5421.5998 3236.5,-5415.5998 3236.5,-5415.5998 3236.5,-5403.5998 3236.5,-5403.5998 3236.5,-5397.5998 3242.5,-5391.5998 3248.5,-5391.5998 3248.5,-5391.5998 3460.5,-5391.5998 3460.5,-5391.5998 3466.5,-5391.5998 3472.5,-5397.5998 3472.5,-5403.5998 3472.5,-5403.5998 3472.5,-5415.5998 3472.5,-5415.5998 3472.5,-5421.5998 3466.5,-5427.5998 3460.5,-5427.5998"/>
+<text text-anchor="middle" x="3354.5" y="-5405.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/digestset</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;github.com/docker/distribution/digestset -->
+<g id="edge222" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;github.com/docker/distribution/digestset</title>
+<path fill="none" stroke="#000000" d="M2980.9702,-5126.7509C3050.4201,-5179.3406 3251.5592,-5331.6497 3326.1911,-5388.1634"/>
+<polygon fill="#000000" stroke="#000000" points="3325.4559,-5389.8018 3330.4985,-5391.4251 3327.5689,-5387.0115 3325.4559,-5389.8018"/>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;errors -->
+<g id="edge214" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3463.5414,-5391.5501C3483.5992,-5386.0235 3503.9012,-5378.8604 3522,-5369.5998 3552.2983,-5354.0972 3548.0967,-5331.453 3580,-5319.5998 3747.8974,-5257.22 3851.5631,-5197.9235 3983,-5319.5998 4038.2748,-5370.7699 4087.8778,-6576.434 4095.0988,-6759.416"/>
+<polygon fill="#000000" stroke="#000000" points="4093.3547,-6759.6019 4095.3,-6764.5292 4096.852,-6759.4642 4093.3547,-6759.6019"/>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;sort -->
+<g id="edge216" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3412.1778,-5391.5947C3450.2963,-5376.4431 3497.5791,-5350.7027 3522,-5310.5998 3580.8678,-5213.9297 3497.7202,-4371.3209 3580,-4293.5998 3645.1032,-4232.1037 3906.9002,-4246.3873 3983,-4293.5998 4032.3184,-4324.1971 4001.3521,-4368.2142 4041,-4410.5998 4047.5585,-4417.6112 4056.0026,-4423.55 4064.2961,-4428.3364"/>
+<polygon fill="#000000" stroke="#000000" points="4063.654,-4429.9814 4068.8752,-4430.8765 4065.3518,-4426.9207 4063.654,-4429.9814"/>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;strings -->
+<g id="edge217" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3441.7659,-5427.6156C3583.2511,-5461.9468 3859.3103,-5551.3467 3983,-5745.5998 4046.9495,-5846.0317 4025.5021,-6697.5494 4041,-6815.5998 4052.7088,-6904.7876 4078.1495,-7008.8702 4089.8666,-7054.3493"/>
+<polygon fill="#000000" stroke="#000000" points="4088.2118,-7054.9401 4091.1586,-7059.3422 4091.6002,-7054.0632 4088.2118,-7054.9401"/>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;sync -->
+<g id="edge218" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3367.655,-5391.2616C3401.1305,-5343.3837 3489.213,-5209.5119 3522,-5082.5998 3546.9984,-4985.8359 3514.8893,-3360.4205 3580,-3284.5998 3704.5368,-3139.5781 3971.4744,-3167.7711 4063.7968,-3182.677"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6259,-3184.4224 4068.8437,-3183.5069 4064.1939,-3180.9687 4063.6259,-3184.4224"/>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge215" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M3358.2706,-5427.9056C3379.602,-5532.4579 3485.4896,-6064.8705 3522,-6505.5998 3541.4901,-6740.8715 3503.6857,-10531.1972 3580,-10754.5998 3616.2399,-10860.6886 3711.9907,-10958.8301 3757.204,-11000.9419"/>
+<polygon fill="#000000" stroke="#000000" points="3756.0372,-11002.2465 3760.8949,-11004.3596 3758.4152,-10999.6783 3756.0372,-11002.2465"/>
+</g>
+<!-- github.com/docker/distribution/metrics -->
+<g id="node92" class="node">
+<title>github.com/docker/distribution/metrics</title>
+<g id="a_node92"><a xlink:href="https://godoc.org/github.com/docker/distribution/metrics" xlink:title="github.com/docker/distribution/metrics" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1629,-6018.5998C1629,-6018.5998 1423,-6018.5998 1423,-6018.5998 1417,-6018.5998 1411,-6012.5998 1411,-6006.5998 1411,-6006.5998 1411,-5994.5998 1411,-5994.5998 1411,-5988.5998 1417,-5982.5998 1423,-5982.5998 1423,-5982.5998 1629,-5982.5998 1629,-5982.5998 1635,-5982.5998 1641,-5988.5998 1641,-5994.5998 1641,-5994.5998 1641,-6006.5998 1641,-6006.5998 1641,-6012.5998 1635,-6018.5998 1629,-6018.5998"/>
+<text text-anchor="middle" x="1526" y="-5996.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/metrics</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/go&#45;metrics -->
+<g id="node93" class="node">
+<title>github.com/docker/go&#45;metrics</title>
+<g id="a_node93"><a xlink:href="https://godoc.org/github.com/docker/go-metrics" xlink:title="github.com/docker/go&#45;metrics" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1927.5,-6499.5998C1927.5,-6499.5998 1769.5,-6499.5998 1769.5,-6499.5998 1763.5,-6499.5998 1757.5,-6493.5998 1757.5,-6487.5998 1757.5,-6487.5998 1757.5,-6475.5998 1757.5,-6475.5998 1757.5,-6469.5998 1763.5,-6463.5998 1769.5,-6463.5998 1769.5,-6463.5998 1927.5,-6463.5998 1927.5,-6463.5998 1933.5,-6463.5998 1939.5,-6469.5998 1939.5,-6475.5998 1939.5,-6475.5998 1939.5,-6487.5998 1939.5,-6487.5998 1939.5,-6493.5998 1933.5,-6499.5998 1927.5,-6499.5998"/>
+<text text-anchor="middle" x="1848.5" y="-6477.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go&#45;metrics</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/metrics&#45;&gt;github.com/docker/go&#45;metrics -->
+<g id="edge219" class="edge">
+<title>github.com/docker/distribution/metrics&#45;&gt;github.com/docker/go&#45;metrics</title>
+<path fill="none" stroke="#000000" d="M1538.2486,-6018.8682C1588.4918,-6093.8047 1778.7204,-6377.5254 1833.5972,-6459.3727"/>
+<polygon fill="#000000" stroke="#000000" points="1832.1733,-6460.3915 1836.4113,-6463.5699 1835.0804,-6458.4423 1832.1733,-6460.3915"/>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;fmt -->
+<g id="edge371" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1855.5192,-6499.7094C1898.2538,-6608.7657 2131.4377,-7187.035 2426,-7594.5998 2681.0548,-7947.5009 2787.1672,-8039.1879 3187,-8211.5998 3328.4622,-8272.5997 3385.7532,-8228.7015 3522,-8300.5998 3552.1001,-8316.4839 3549.3029,-8335.9026 3580,-8350.5998 3744.2504,-8429.2399 3844.3046,-8306.5911 3983,-8424.5998 4029.7303,-8464.3602 4075.843,-8652.2507 4090.8976,-8718.4501"/>
+<polygon fill="#000000" stroke="#000000" points="4089.2007,-8718.8806 4092.0104,-8723.3715 4092.6145,-8718.1086 4089.2007,-8718.8806"/>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;sync -->
+<g id="edge375" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M1851.0133,-6463.4739C1880.4719,-6250.8478 2162.6845,-4209.3247 2368,-2555.5998 2381.7815,-2444.5959 2343.4119,-2131.0386 2426,-2055.5998 2553.7333,-1938.9237 3856.6396,-1934.4383 3983,-2052.5998 4041.8644,-2107.6448 4032.4556,-2695.4626 4041,-2775.5998 4056.8884,-2924.6145 4083.0819,-3102.9279 4092.4952,-3165.5234"/>
+<polygon fill="#000000" stroke="#000000" points="4090.7732,-3165.8415 4093.2487,-3170.5249 4094.2342,-3165.32 4090.7732,-3165.8415"/>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;time -->
+<g id="edge376" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M1852.5817,-6499.6465C1871.3354,-6582.3956 1951.382,-6933.7844 2023,-7220.5998 2168.86,-7804.7391 2241.8452,-7941.8904 2368,-8530.5998 2400.9467,-8684.3479 2328.9381,-8755.8947 2426,-8879.5998 2515.8441,-8994.1059 2644.1438,-8888.9401 2727,-9008.5998 2807.9208,-9125.4645 2699.9341,-10172.7169 2785,-10286.5998 2881.8146,-10416.2115 3010.3726,-10295.6008 3129,-10405.5998 3176.5856,-10449.7243 3140.4166,-10494.4186 3187,-10539.5998 3301.9915,-10651.13 3417.583,-10551.1127 3522,-10672.5998 3596.6299,-10759.4302 3496.5558,-10845.2021 3580,-10923.5998 3712.3282,-11047.925 3855.0164,-10861.8066 3983,-10990.5998 4098.1621,-11106.4904 3948.5943,-11218.8626 4041,-11353.5998 4046.6936,-11361.9017 4055.3027,-11368.348 4064.0302,-11373.2077"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6323,-11374.9749 4068.8733,-11375.7461 4065.2571,-11371.8749 4063.6323,-11374.9749"/>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;net/http -->
+<g id="edge374" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M1848.6874,-6463.592C1851.399,-6211.7224 1884.687,-3389.9018 2023,-2570.5998 2127.3037,-1952.7527 2169.5374,-1791.3002 2426,-1219.5998 2632.5925,-759.0687 2707.5642,-534.4311 3187,-376.5998 3523.1775,-265.9295 3682.0972,-167.2648 3983,-353.5998 4056.8484,-399.3307 4083.9876,-509.4166 4092.5499,-557.4887"/>
+<polygon fill="#000000" stroke="#000000" points="4090.8382,-557.8609 4093.4132,-562.4904 4094.2872,-557.2656 4090.8382,-557.8609"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus -->
+<g id="node107" class="node">
+<title>github.com/prometheus/client_golang/prometheus</title>
+<g id="a_node107"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus" xlink:title="github.com/prometheus/client_golang/prometheus" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2710,-6956.5998C2710,-6956.5998 2443,-6956.5998 2443,-6956.5998 2437,-6956.5998 2431,-6950.5998 2431,-6944.5998 2431,-6944.5998 2431,-6932.5998 2431,-6932.5998 2431,-6926.5998 2437,-6920.5998 2443,-6920.5998 2443,-6920.5998 2710,-6920.5998 2710,-6920.5998 2716,-6920.5998 2722,-6926.5998 2722,-6932.5998 2722,-6932.5998 2722,-6944.5998 2722,-6944.5998 2722,-6950.5998 2716,-6956.5998 2710,-6956.5998"/>
+<text text-anchor="middle" x="2576.5" y="-6934.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;github.com/prometheus/client_golang/prometheus -->
+<g id="edge372" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;github.com/prometheus/client_golang/prometheus</title>
+<path fill="none" stroke="#000000" d="M1877.4877,-6499.7968C1993.0219,-6572.3231 2420.3372,-6840.5691 2543.2368,-6917.7189"/>
+<polygon fill="#000000" stroke="#000000" points="2542.6602,-6919.4232 2547.8254,-6920.5994 2544.5211,-6916.4589 2542.6602,-6919.4232"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp -->
+<g id="node108" class="node">
+<title>github.com/prometheus/client_golang/prometheus/promhttp</title>
+<g id="a_node108"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp" xlink:title="github.com/prometheus/client_golang/prometheus/promhttp" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2356,-6499.5998C2356,-6499.5998 2035,-6499.5998 2035,-6499.5998 2029,-6499.5998 2023,-6493.5998 2023,-6487.5998 2023,-6487.5998 2023,-6475.5998 2023,-6475.5998 2023,-6469.5998 2029,-6463.5998 2035,-6463.5998 2035,-6463.5998 2356,-6463.5998 2356,-6463.5998 2362,-6463.5998 2368,-6469.5998 2368,-6475.5998 2368,-6475.5998 2368,-6487.5998 2368,-6487.5998 2368,-6493.5998 2362,-6499.5998 2356,-6499.5998"/>
+<text text-anchor="middle" x="2195.5" y="-6477.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus/promhttp</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;github.com/prometheus/client_golang/prometheus/promhttp -->
+<g id="edge373" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;github.com/prometheus/client_golang/prometheus/promhttp</title>
+<path fill="none" stroke="#000000" d="M1939.6702,-6481.5998C1963.7722,-6481.5998 1990.6336,-6481.5998 2017.6361,-6481.5998"/>
+<polygon fill="#000000" stroke="#000000" points="2017.6791,-6483.3499 2022.6791,-6481.5998 2017.679,-6479.8499 2017.6791,-6483.3499"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;bytes -->
+<g id="edge411" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3785.6013,-2377.6292C3810.4091,-2487.7026 3940.7503,-3080.7476 3983,-3572.5998 4064.5444,-4521.9035 4020.9577,-11195.0111 4041,-12147.5998 4054.1587,-12773.0196 4088.4311,-13539.1827 4094.9283,-13681.3377"/>
+<polygon fill="#000000" stroke="#000000" points="4093.1882,-13681.5955 4095.165,-13686.5103 4096.6845,-13681.4355 4093.1882,-13681.5955"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;context -->
+<g id="edge412" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M3792.3236,-2341.3067C3826.8154,-2282.052 3934.7053,-2089.1057 3983,-1914.5998 4009.3841,-1819.2646 4080.1778,-1097.8593 4093.7374,-958.032"/>
+<polygon fill="#000000" stroke="#000000" points="4095.4914,-958.0746 4094.2319,-952.9291 4092.0077,-957.737 4095.4914,-958.0746"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;errors -->
+<g id="edge413" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3785.4274,-2377.6452C3809.2099,-2487.8133 3934.5481,-3081.3198 3983,-3572.5998 4062.082,-4374.4554 4012.2734,-4579.3663 4041,-5384.5998 4060.9014,-5942.4553 4089.3952,-6625.3544 4095.016,-6759.2216"/>
+<polygon fill="#000000" stroke="#000000" points="4093.2781,-6759.548 4095.2365,-6764.4701 4096.775,-6759.401 4093.2781,-6759.548"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;fmt -->
+<g id="edge414" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3785.5578,-2377.633C3810.109,-2487.7289 3939.1979,-3080.8834 3983,-3572.5998 4070.4545,-4554.3521 3997.7103,-7022.9111 4041,-8007.5998 4053.3264,-8287.983 4084.9835,-8627.5102 4093.7309,-8718.3362"/>
+<polygon fill="#000000" stroke="#000000" points="4092.0036,-8718.6568 4094.226,-8723.4655 4095.4874,-8718.3205 4092.0036,-8718.6568"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;strconv -->
+<g id="edge419" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3785.378,-2377.6502C3808.8693,-2487.8475 3932.7869,-3081.4967 3983,-3572.5998 4049.9574,-4227.4684 3968.3809,-4398.3349 4041,-5052.5998 4052.5399,-5156.5691 4079.1772,-5278.7668 4090.6186,-5328.6518"/>
+<polygon fill="#000000" stroke="#000000" points="4088.9149,-5329.0522 4091.7424,-5333.5319 4092.3256,-5328.2667 4088.9149,-5329.0522"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;strings -->
+<g id="edge420" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3785.5247,-2377.636C3809.8805,-2487.7495 3938.0165,-3080.9901 3983,-3572.5998 4048.6789,-4290.3831 3960.1539,-6099.3663 4041,-6815.5998 4051.0895,-6904.9853 4077.3594,-7008.9666 4089.5775,-7054.3846"/>
+<polygon fill="#000000" stroke="#000000" points="4087.9312,-7055.0009 4090.9258,-7059.3707 4091.3098,-7054.0872 4087.9312,-7055.0009"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;net/http -->
+<g id="edge415" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M3792.7404,-2341.4156C3828.489,-2282.4894 3939.746,-2090.4229 3983,-1914.5998 4090.622,-1477.1275 3990.105,-1350.2316 4041,-902.5998 4053.8218,-789.8296 4080.2088,-656.5919 4091.1162,-603.8044"/>
+<polygon fill="#000000" stroke="#000000" points="4092.8823,-603.9056 4092.184,-598.6544 4089.4552,-603.1949 4092.8823,-603.9056"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;net/url -->
+<g id="edge416" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M3790.9166,-2341.345C3821.572,-2281.4289 3920.1536,-2084.8606 3983,-1914.5998 4033.3351,-1778.234 4075.8966,-1608.5165 4090.5501,-1547.6497"/>
+<polygon fill="#000000" stroke="#000000" points="4092.2542,-1548.0479 4091.7193,-1542.7776 4088.8508,-1547.2311 4092.2542,-1548.0479"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;path -->
+<g id="edge417" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M3854.6512,-2356.8052C3896.8542,-2359.0445 3948.2731,-2368.8795 3983,-2399.5998 4032.6574,-2443.5281 4077.3897,-2648.9715 4091.4434,-2719.083"/>
+<polygon fill="#000000" stroke="#000000" points="4089.7858,-2719.72 4092.4789,-2724.2819 4093.2184,-2719.0363 4089.7858,-2719.72"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;regexp -->
+<g id="edge418" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M3854.8394,-2345.1418C3920.8623,-2332.1262 4014.7947,-2313.6085 4063.6569,-2303.9759"/>
+<polygon fill="#000000" stroke="#000000" points="4064.248,-2305.6431 4068.8151,-2302.959 4063.571,-2302.2092 4064.248,-2305.6431"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;fmt -->
+<g id="edge264" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3793.0834,-1899.7489C3829.8665,-1958.5781 3943.8945,-2150.4619 3983,-2327.5998 4051.0285,-2635.7521 4027.5918,-7692.3128 4041,-8007.5998 4052.9246,-8288.0003 4084.8693,-8627.5152 4093.7065,-8718.3372"/>
+<polygon fill="#000000" stroke="#000000" points="4091.9796,-8718.6599 4094.2068,-8723.4664 4095.4631,-8718.3201 4091.9796,-8718.6599"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;strings -->
+<g id="edge267" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3793.0635,-1899.7533C3829.7867,-1958.5958 3943.6544,-2150.5151 3983,-2327.5998 4091.1678,-2814.4362 3986.2919,-6319.9013 4041,-6815.5998 4050.8678,-6905.01 4077.2512,-7008.9787 4089.5379,-7054.3891"/>
+<polygon fill="#000000" stroke="#000000" points="4087.8928,-7055.0089 4090.8939,-7059.3742 4091.2701,-7054.0902 4087.8928,-7055.0089"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;sync -->
+<g id="edge268" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3955.2151,-1899.1271C3965.5571,-1905.1011 3974.9921,-1912.4939 3983,-1921.5998 4045.8069,-1993.0192 4031.4954,-2680.9685 4041,-2775.5998 4055.9762,-2924.709 4082.7339,-3102.964 4092.3956,-3165.5337"/>
+<polygon fill="#000000" stroke="#000000" points="4090.6751,-3165.8596 4093.1693,-3170.5332 4094.134,-3165.3243 4090.6751,-3165.8596"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;net/http -->
+<g id="edge265" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M3785.9459,-1863.2085C3820.9176,-1718.5402 4051.4116,-765.0498 4090.4019,-603.7574"/>
+<polygon fill="#000000" stroke="#000000" points="4092.13,-604.0566 4091.6039,-598.7854 4088.7279,-603.2342 4092.13,-604.0566"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;net/url -->
+<g id="edge266" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M3799.191,-1863.4173C3834.8096,-1826.6217 3917.3697,-1740.3824 3983,-1664.5998 4018.2764,-1623.8665 4057.3244,-1574.455 4079.0407,-1546.5596"/>
+<polygon fill="#000000" stroke="#000000" points="4080.429,-1547.6251 4082.1165,-1542.6036 4077.6658,-1545.4767 4080.429,-1547.6251"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;errors -->
+<g id="edge269" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3808.5422,-4702.7569C3854.1096,-4735.1095 3944.9386,-4807.5674 3983,-4894.5998 4022.2668,-4984.3886 4086.5805,-6548.9387 4095.0656,-6759.282"/>
+<polygon fill="#000000" stroke="#000000" points="4093.3282,-6759.6305 4095.2781,-6764.5559 4096.8253,-6759.4895 4093.3282,-6759.6305"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;fmt -->
+<g id="edge270" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3809.292,-4702.8521C3855.5524,-4735.0869 3946.8338,-4807.0363 3983,-4894.5998 4049.0327,-5054.4743 4032.9232,-7834.814 4041,-8007.5998 4054.1048,-8287.9477 4085.2047,-8627.5002 4093.778,-8718.334"/>
+<polygon fill="#000000" stroke="#000000" points="4092.0501,-8718.6509 4094.2632,-8723.4638 4095.5345,-8718.3212 4092.0501,-8718.6509"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;io -->
+<g id="edge271" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3809.4057,-4702.8055C3855.8356,-4734.9709 3947.3629,-4806.8196 3983,-4894.5998 4075.4023,-5122.2023 3995.6451,-13496.179 4041,-13737.5998 4049.4077,-13782.3532 4070.9371,-13831.2316 4084.4305,-13858.9799"/>
+<polygon fill="#000000" stroke="#000000" points="4082.9135,-13859.8612 4086.6875,-13863.5787 4086.0555,-13858.3191 4082.9135,-13859.8612"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;strconv -->
+<g id="edge274" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3854.473,-4702.7349C3898.2579,-4717.381 3951.4114,-4742.4967 3983,-4783.5998 3991.595,-4794.7837 4071.9897,-5223.0212 4091.6685,-5328.3695"/>
+<polygon fill="#000000" stroke="#000000" points="4089.9745,-5328.8315 4092.6126,-5333.4253 4093.415,-5328.189 4089.9745,-5328.8315"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;sync -->
+<g id="edge275" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3823.558,-4666.5418C3871.3799,-4643.5368 3947.4175,-4598.8957 3983,-4534.5998 4061.0469,-4393.5729 4027.4682,-3972.2136 4041,-3811.5998 4060.7351,-3577.3569 4086.4262,-3293.7435 4093.885,-3211.7924"/>
+<polygon fill="#000000" stroke="#000000" points="4095.6391,-3211.8268 4094.3497,-3206.6887 4092.1535,-3211.5094 4095.6391,-3211.8268"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;net/http -->
+<g id="edge272" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M3825.153,-4666.5244C3873.7092,-4643.767 3949.6943,-4599.6144 3983,-4534.5998 4075.0094,-4354.9921 4022.3455,-1103.5393 4041,-902.5998 4051.4915,-789.589 4079.1916,-656.4869 4090.7832,-603.77"/>
+<polygon fill="#000000" stroke="#000000" points="4092.5495,-603.8868 4091.9191,-598.627 4089.1318,-603.1319 4092.5495,-603.8868"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;regexp -->
+<g id="edge273" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M3824.969,-4666.4292C3873.3544,-4643.5834 3949.1811,-4599.349 3983,-4534.5998 4029.9371,-4444.7347 4032.6867,-2811.643 4041,-2710.5998 4053.2882,-2561.2451 4081.7086,-2383.142 4092.1023,-2320.6391"/>
+<polygon fill="#000000" stroke="#000000" points="4093.8387,-2320.8649 4092.9354,-2315.6451 4090.3864,-2320.289 4093.8387,-2320.8649"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;context -->
+<g id="edge276" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M1178.5166,-5388.4093C1207.5222,-5345.4665 1279.9125,-5233.6417 1320,-5130.5998 1381.3006,-4973.0313 1918.3749,-2302.4124 2023,-2169.5998 2129.3943,-2034.5415 2270.8707,-2136.4673 2368,-1994.5998 2514.8524,-1780.1067 2262.4065,-1614.6149 2426,-1412.5998 2514.3689,-1303.4766 2626.8075,-1414.9787 2727,-1316.5998 2784.2631,-1260.3732 2734.3787,-1207.8733 2785,-1145.5998 2915.8256,-984.6603 2991.4411,-977.6912 3187,-908.5998 3353.4742,-849.7842 3403.915,-863.5234 3580,-850.5998 3758.6306,-837.4894 3812.96,-794.3221 3983,-850.5998 4019.9213,-862.8196 4054.5954,-892.2942 4075.5553,-912.8571"/>
+<polygon fill="#000000" stroke="#000000" points="4074.5271,-914.3026 4079.3068,-916.5868 4076.9948,-911.8205 4074.5271,-914.3026"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;fmt -->
+<g id="edge277" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1170.8908,-5424.7273C1191.6354,-5500.6696 1275.9868,-5800.1029 1378,-6033.5998 1742.1732,-6867.1516 1820.8131,-7112.4979 2426,-7791.5998 2568.4007,-7951.3928 2603.6025,-7998.9922 2785,-8112.5998 3104.6665,-8312.8041 3214.8054,-8315.2528 3580,-8409.5998 3756.3167,-8455.1508 3840.2378,-8370.5448 3983,-8483.5998 4059.5331,-8544.2073 4085.4205,-8667.1943 4093.1093,-8718.4064"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3874,-8718.727 4093.8384,-8723.4234 4094.851,-8718.2236 4091.3874,-8718.727"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge280" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M1166.3081,-5424.601C1170.2624,-5647.4906 1213.7067,-7869.2701 1378,-8102.5998 1461.034,-8220.525 1550.1266,-8161.7327 1674,-8235.5998 2007.5691,-8434.5109 2148.5022,-8446.2021 2368,-8766.5998 2420.0662,-8842.6 2362.2778,-8899.0686 2426,-8965.5998 2523.7023,-9067.609 2642.1565,-8954.6698 2727,-9067.5998 2813.8485,-9183.1985 2695.1821,-10254.2928 2785,-10367.5998 2883.6778,-10492.0838 3004.3369,-10366.1483 3129,-10464.5998 3170.534,-10497.4009 3148.5459,-10532.2368 3187,-10568.5998 3307.3069,-10682.3643 3418.6217,-10602.2591 3522,-10731.5998 3592.4037,-10819.6848 3499.6425,-10899.4903 3580,-10978.5998 3603.2521,-11001.4908 3635.4818,-11013.6604 3667.3517,-11019.8409"/>
+<polygon fill="#000000" stroke="#000000" points="3667.2278,-11021.5975 3672.4622,-11020.7822 3667.8619,-11018.1554 3667.2278,-11021.5975"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/docker/distribution -->
+<g id="edge278" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/docker/distribution</title>
+<path fill="none" stroke="#000000" d="M1235.4658,-5424.7235C1473.7809,-5486.9 2256.9607,-5691.2319 2502.3163,-5755.2453"/>
+<polygon fill="#000000" stroke="#000000" points="2502.0259,-5756.978 2507.3058,-5756.547 2502.9096,-5753.5914 2502.0259,-5756.978"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/docker/distribution/metrics -->
+<g id="edge279" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/docker/distribution/metrics</title>
+<path fill="none" stroke="#000000" d="M1176.9707,-5424.7014C1229.4012,-5511.2117 1454.2901,-5882.2785 1512.3032,-5978"/>
+<polygon fill="#000000" stroke="#000000" points="1510.85,-5978.9789 1514.9382,-5982.3478 1513.8432,-5977.1648 1510.85,-5978.9789"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;context -->
+<g id="edge281" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M779.0747,-5208.48C829.9121,-4985.356 1357.5931,-2751.9221 2426,-1313.5998 2566.8729,-1123.9523 2578.4602,-1036.2848 2785,-921.5998 3252.3499,-662.0952 3492.2549,-602.6379 3983,-814.5998 4028.8833,-834.4177 4064.7565,-883.05 4082.9212,-911.9548"/>
+<polygon fill="#000000" stroke="#000000" points="4081.5636,-913.0867 4085.6833,-916.4171 4084.5396,-911.2446 4081.5636,-913.0867"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;sync -->
+<g id="edge286" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M879.621,-5208.586C1019.2309,-5182.6247 1256.2959,-5131.2067 1320,-5071.5998 2385.7178,-4074.4242 1256.6283,-2852.9172 2426,-1979.5998 2495.3069,-1927.8396 3920.3844,-1909.9186 3983,-1969.5998 4047.9938,-2031.5477 4031.8473,-2686.2804 4041,-2775.5998 4056.2763,-2924.6785 4082.8484,-3102.9523 4092.4284,-3165.5304"/>
+<polygon fill="#000000" stroke="#000000" points="4090.7074,-3165.8537 4093.1954,-3170.5305 4094.1669,-3165.3229 4090.7074,-3165.8537"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge285" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M776.7697,-5244.6463C798.7518,-5466.0653 1024.1747,-7659.6716 1378,-8190.5998 1552.7996,-8452.8932 1762.8593,-8346.749 1965,-8588.5998 2001.0319,-8631.7101 2741.9973,-10401.4396 2785,-10437.5998 2905.6183,-10539.0256 3010.6382,-10419.5497 3129,-10523.5998 3182.0609,-10570.2448 3136.2468,-10622.4539 3187,-10671.5998 3300.5083,-10781.5134 3412.6495,-10676.5489 3522,-10790.5998 3582.5162,-10853.7173 3515.6202,-10919.4283 3580,-10978.5998 3603.7733,-11000.4499 3635.8003,-11012.3661 3667.3166,-11018.6357"/>
+<polygon fill="#000000" stroke="#000000" points="3667.1305,-11020.3815 3672.369,-11019.5941 3667.7828,-11016.9428 3667.1305,-11020.3815"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution -->
+<g id="edge282" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution</title>
+<path fill="none" stroke="#000000" d="M954.0531,-5226.5998C1106.2058,-5226.5998 1330.4271,-5226.5998 1526,-5226.5998 1526,-5226.5998 1526,-5226.5998 1848.5,-5226.5998 2083.298,-5226.5998 2201.3917,-5157.1556 2368,-5322.5998 2464.9821,-5418.9044 2361.8699,-5503.9042 2426,-5624.5998 2455.1912,-5679.5389 2511.2352,-5727.1901 2546.0842,-5753.3132"/>
+<polygon fill="#000000" stroke="#000000" points="2545.1688,-5754.8132 2550.2258,-5756.3912 2547.2566,-5752.0041 2545.1688,-5754.8132"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution/reference -->
+<g id="edge283" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution/reference</title>
+<path fill="none" stroke="#000000" d="M954.0256,-5216.9183C1383.669,-5193.6837 2464.7375,-5135.2208 2832.372,-5115.3396"/>
+<polygon fill="#000000" stroke="#000000" points="2832.567,-5117.0817 2837.4652,-5115.0641 2832.378,-5113.5868 2832.567,-5117.0817"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution/registry/storage/cache -->
+<g id="edge284" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution/registry/storage/cache</title>
+<path fill="none" stroke="#000000" d="M814.2808,-5244.683C887.6158,-5278.4434 1044.8745,-5350.8387 1121.7965,-5386.2504"/>
+<polygon fill="#000000" stroke="#000000" points="1121.4717,-5388.0273 1126.7453,-5388.5286 1122.9353,-5384.848 1121.4717,-5388.0273"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;bufio -->
+<g id="edge307" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M2200.7678,-15166.6267C2221.123,-15234.147 2299.842,-15476.4896 2426,-15638.5998 2552.8327,-15801.577 2590.5796,-15863.9664 2785,-15933.5998 3093.3734,-16044.0465 3194.9808,-15952.3364 3522,-15933.5998 3624.9504,-15927.7013 3913.2082,-15956.5122 3983,-15880.5998 4126.0231,-15725.0339 3934.0904,-14161.8818 4041,-13979.5998 4046.3255,-13970.5198 4055.2175,-13963.6459 4064.3159,-13958.5869"/>
+<polygon fill="#000000" stroke="#000000" points="4065.1663,-13960.1171 4068.7842,-13956.2475 4063.5429,-13957.0163 4065.1663,-13960.1171"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;bytes -->
+<g id="edge308" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2307.5408,-15157.7955C2663.6919,-15184.3153 3759.5543,-15244.1641 3983,-14997.5998 4068.7646,-14902.9617 4014.8117,-13974.6043 4041,-13849.5998 4050.3937,-13804.7608 4071.5984,-13755.4688 4084.7631,-13727.4528"/>
+<polygon fill="#000000" stroke="#000000" points="4086.4039,-13728.0769 4086.9633,-13722.8091 4083.2409,-13726.5783 4086.4039,-13728.0769"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;fmt -->
+<g id="edge309" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2196.3983,-15130.3701C2206.9696,-14917.5802 2309.3846,-12902.9242 2426,-12316.5998 2511.16,-11888.4285 2628.2913,-11806.8521 2727,-11381.5998 2747.2091,-11294.5358 2721.6082,-11046.608 2785,-10983.5998 2894.4869,-10874.7755 3025.0881,-11049.7595 3129,-10935.5998 3226.0285,-10829.0024 3100.6382,-9755.0085 3187,-9639.5998 3281.3143,-9513.5639 3420.1876,-9644.6601 3522,-9524.5998 3622.5626,-9406.0133 3468.5008,-9287.9674 3580,-9179.5998 3709.6757,-9053.5663 3845.9381,-9241.5587 3983,-9123.5998 4039.1826,-9075.2476 4080.5049,-8840.0426 4092.4622,-8764.7987"/>
+<polygon fill="#000000" stroke="#000000" points="4094.2219,-8764.874 4093.2722,-8759.6624 4090.7647,-8764.3287 4094.2219,-8764.874"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;io -->
+<g id="edge312" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2233.133,-15166.7416C2277.8784,-15187.2172 2355.4,-15219.2761 2426,-15231.5998 2557.7851,-15254.6039 2593.2294,-15232.9842 2727,-15231.5998 3006.156,-15228.7108 3783.2224,-15400.6006 3983,-15205.5998 4085.7539,-15105.3028 3967.4669,-14037.9316 4041,-13914.5998 4046.3907,-13905.5584 4055.2998,-13898.6945 4064.3937,-13893.633"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2432,-13895.1636 4068.8583,-13891.2913 4063.6175,-13892.064 4065.2432,-13895.1636"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;sort -->
+<g id="edge317" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M2196.5319,-15130.4539C2208.5884,-14917.5945 2323.0493,-12873.8655 2368,-11219.5998 2370.0928,-11142.5824 2375.6879,-5730.9502 2426,-5672.5998 2514.4626,-5570.0036 2634.3511,-5723.4319 2727,-5624.5998 2821.3373,-5523.9667 2759.9109,-5142.2357 2785,-5006.5998 2905.8595,-4353.2127 2657.7361,-3968.3478 3187,-3566.5998 3244.9102,-3522.642 3929.523,-3522.0411 3983,-3572.5998 4118.6433,-3700.8408 3942.4024,-4252.0964 4041,-4410.5998 4046.3665,-4419.2268 4054.9755,-4425.7853 4063.8048,-4430.6481"/>
+<polygon fill="#000000" stroke="#000000" points="4063.4671,-4432.4427 4068.7134,-4433.1775 4065.0704,-4429.3315 4063.4671,-4432.4427"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;strconv -->
+<g id="edge318" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M2196.531,-15130.4539C2208.5766,-14917.5942 2322.9388,-12873.8625 2368,-11219.5998 2370.0714,-11143.5543 2378.0955,-5801.6961 2426,-5742.5998 2512.3032,-5636.134 2636.2031,-5778.2601 2727,-5675.5998 2815.5986,-5575.425 2695.074,-5175.5847 2785,-5076.5998 2889.7383,-4961.3106 2977.027,-5043.7458 3129,-5009.5998 3330.8265,-4964.2526 3374.7515,-4920.3558 3580,-4894.5998 3757.7173,-4872.2986 3841.9657,-4784.1909 3983,-4894.5998 4018.3743,-4922.2927 4076.3398,-5239.5242 4091.9968,-5328.5159"/>
+<polygon fill="#000000" stroke="#000000" points="4090.2937,-5328.9361 4092.8816,-5333.5584 4093.741,-5328.3311 4090.2937,-5328.9361"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;strings -->
+<g id="edge319" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2196.4436,-15130.4512C2207.4822,-14917.561 2312.6571,-12873.5505 2368,-11219.5998 2370.3407,-11149.6482 2381.5889,-8754.6956 2426,-8700.5998 2512.9633,-8594.6725 2607.0756,-8699.9425 2727,-8633.5998 2838.2377,-8572.0627 3069.6522,-8339.0208 3129,-8226.5998 3191.9916,-8107.2764 3119.5993,-8045.4894 3187,-7928.5998 3285.661,-7757.497 3403.8677,-7794.8873 3522,-7636.5998 3559.1139,-7586.8703 3530.2368,-7546.6686 3580,-7509.5998 3725.0196,-7401.5741 3845.6828,-7571.2613 3983,-7453.5998 4038.3485,-7406.174 4080.0841,-7175.5694 4092.3227,-7100.9477"/>
+<polygon fill="#000000" stroke="#000000" points="4094.0765,-7101.0654 4093.1527,-7095.8491 4090.622,-7100.5029 4094.0765,-7101.0654"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;sync -->
+<g id="edge320" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2196.5602,-15130.4547C2208.9424,-14917.6038 2326.375,-12873.9525 2368,-11219.5998 2369.5744,-11157.028 2385.2799,-2254.1348 2426,-2206.5998 2503.7675,-2115.8173 2837.4624,-2120.5998 2957,-2120.5998 2957,-2120.5998 2957,-2120.5998 3354.5,-2120.5998 3495.2936,-2120.5998 3884.7504,-2099.7542 3983,-2200.5998 4072.6193,-2292.5871 4026.0771,-2648.0436 4041,-2775.5998 4058.4134,-2924.444 4083.6636,-3102.8629 4092.6616,-3165.5048"/>
+<polygon fill="#000000" stroke="#000000" points="4090.9374,-3165.8102 4093.3814,-3170.5101 4094.4018,-3165.3119 4090.9374,-3165.8102"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;os -->
+<g id="edge313" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2196.5776,-15166.6539C2210.6891,-15402.4503 2359.7256,-17873.8367 2426,-18189.5998 2524.8049,-18660.3544 2542.595,-18795.1338 2785,-19210.5998 2921.8285,-19445.1148 2959.0697,-19523.0631 3187,-19670.5998 3337.173,-19767.8051 3850.9893,-19911.3225 3983,-19790.5998 4036.345,-19741.8162 4031.6848,-19214.285 4041,-19142.5998 4055.6255,-19030.0493 4080.9961,-18896.6878 4091.3739,-18843.8358"/>
+<polygon fill="#000000" stroke="#000000" points="4093.1402,-18843.9233 4092.389,-18838.6794 4089.7061,-18843.2471 4093.1402,-18843.9233"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;path/filepath -->
+<g id="edge315" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M2196.1113,-15166.9648C2204.1929,-15407.1466 2291.8464,-17934.3682 2426,-18668.5998 2524.2989,-19206.5966 2436.4968,-19424.1159 2785,-19845.5998 2964.7214,-20062.957 3771.3047,-20450.9571 3983,-20264.5998 4049.7944,-20205.8001 4087.7966,-19538.7495 4094.812,-19404.1766"/>
+<polygon fill="#000000" stroke="#000000" points="4096.5747,-19403.9743 4095.0858,-19398.8904 4093.0794,-19403.7932 4096.5747,-19403.9743"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;regexp -->
+<g id="edge316" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M2196.5606,-15130.4547C2208.9479,-14917.6039 2326.4265,-12873.9538 2368,-11219.5998 2371.1755,-11093.2344 2370.3848,-2232.1131 2426,-2118.5998 2473.3142,-2022.0293 3085.7326,-1556.7857 3187,-1520.5998 3520.1644,-1401.55 3715.5997,-1280.9353 3983,-1512.5998 4042.4743,-1564.1259 4085.8372,-2149.5912 4094.4377,-2274.32"/>
+<polygon fill="#000000" stroke="#000000" points="4092.71,-2274.7056 4094.7982,-2279.574 4096.2018,-2274.4659 4092.71,-2274.7056"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;os/exec -->
+<g id="edge314" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;os/exec</title>
+<path fill="none" stroke="#000000" d="M2196.0533,-15166.6566C2200.8932,-15314.4629 2240.3867,-16340.138 2426,-17146.5998 2483.2617,-17395.3933 3000.5297,-19140.2269 3187,-19314.5998 3351.3756,-19468.3114 3647.9664,-19476.024 3747.7231,-19473.945"/>
+<polygon fill="#000000" stroke="#000000" points="3747.929,-19475.6908 3752.8869,-19473.8248 3747.8475,-19472.1917 3747.929,-19475.6908"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;github.com/opencontainers/runc/libcontainer/user -->
+<g id="edge311" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;github.com/opencontainers/runc/libcontainer/user</title>
+<path fill="none" stroke="#000000" d="M2231.5653,-15130.4493C2276.0604,-15108.9488 2354.5967,-15073.8759 2426,-15056.5998 2731.0582,-14982.7908 2832.5665,-15092.728 3129,-14989.5998 3209.9632,-14961.433 3290.9106,-14898.4504 3330.1288,-14865.1659"/>
+<polygon fill="#000000" stroke="#000000" points="3331.5815,-14866.2266 3334.2475,-14861.6489 3329.3087,-14863.5649 3331.5815,-14866.2266"/>
+</g>
+<!-- github.com/docker/docker/pkg/system -->
+<g id="node102" class="node">
+<title>github.com/docker/docker/pkg/system</title>
+<g id="a_node102"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/system" xlink:title="github.com/docker/docker/pkg/system" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2677.5,-15106.5998C2677.5,-15106.5998 2475.5,-15106.5998 2475.5,-15106.5998 2469.5,-15106.5998 2463.5,-15100.5998 2463.5,-15094.5998 2463.5,-15094.5998 2463.5,-15082.5998 2463.5,-15082.5998 2463.5,-15076.5998 2469.5,-15070.5998 2475.5,-15070.5998 2475.5,-15070.5998 2677.5,-15070.5998 2677.5,-15070.5998 2683.5,-15070.5998 2689.5,-15076.5998 2689.5,-15082.5998 2689.5,-15082.5998 2689.5,-15094.5998 2689.5,-15094.5998 2689.5,-15100.5998 2683.5,-15106.5998 2677.5,-15106.5998"/>
+<text text-anchor="middle" x="2576.5" y="-15084.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/system</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;github.com/docker/docker/pkg/system -->
+<g id="edge310" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;github.com/docker/docker/pkg/system</title>
+<path fill="none" stroke="#000000" d="M2307.7519,-15130.9224C2355.0861,-15123.4682 2410.2214,-15114.7854 2458.1898,-15107.2313"/>
+<polygon fill="#000000" stroke="#000000" points="2458.7372,-15108.9168 2463.4041,-15106.4102 2458.1927,-15105.4594 2458.7372,-15108.9168"/>
+</g>
+<!-- github.com/docker/docker/pkg/idtools&#45;&gt;syscall -->
+<g id="edge321" class="edge">
+<title>github.com/docker/docker/pkg/idtools&#45;&gt;syscall</title>
+<path fill="none" stroke="#000000" d="M2195.6592,-15166.6317C2197.4442,-15334.6269 2217.8441,-16642.937 2426,-17675.5998 2530.3769,-18193.4141 2586.1305,-18319.2359 2785,-18808.5998 2934.9603,-19177.6116 2849.6196,-19397.8636 3187,-19609.5998 3336.8311,-19703.6322 3853.6156,-19736.2279 3983,-19615.5998 4117.7496,-19489.9696 3944.6747,-18945.6407 4041,-18788.5998 4046.5039,-18779.6268 4055.4425,-18772.7809 4064.5288,-18767.7146"/>
+<polygon fill="#000000" stroke="#000000" points="4065.3768,-18769.246 4068.9868,-18765.369 4063.747,-18766.1485 4065.3768,-18769.246"/>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user&#45;&gt;bufio -->
+<g id="edge495" class="edge">
+<title>github.com/opencontainers/runc/libcontainer/user&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3444.7196,-14861.6551C3599.7965,-14890.347 3907.5487,-14936.2734 3983,-14865.5998 4127.0043,-14730.7142 3938.2264,-14148.0307 4041,-13979.5998 4046.483,-13970.614 4055.4162,-13963.7647 4064.5039,-13958.6994"/>
+<polygon fill="#000000" stroke="#000000" points="4065.3521,-13960.2306 4068.9631,-13956.3545 4063.7231,-13957.1328 4065.3521,-13960.2306"/>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user&#45;&gt;errors -->
+<g id="edge496" class="edge">
+<title>github.com/opencontainers/runc/libcontainer/user&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3365.6584,-14825.3738C3397.7022,-14771.8567 3489.7636,-14609.5566 3522,-14460.5998 3546.3739,-14347.9736 3520.1248,-10411.0565 3580,-10312.5998 3685.5908,-10138.9698 3876.9732,-10269.9639 3983,-10096.5998 4071.4516,-9951.9729 4021.9543,-7214.0572 4041,-7045.5998 4051.1426,-6955.8894 4077.3853,-6851.5029 4089.587,-6805.9065"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3245,-6806.1838 4090.9334,-6800.9009 4087.9446,-6805.2747 4091.3245,-6806.1838"/>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user&#45;&gt;fmt -->
+<g id="edge497" class="edge">
+<title>github.com/opencontainers/runc/libcontainer/user&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3365.469,-14825.3315C3396.9992,-14771.6997 3487.8075,-14609.1198 3522,-14460.5998 3557.9006,-14304.6603 3504.9758,-13162.9411 3580,-13021.5998 3684.3168,-12825.073 3879.924,-12921.7802 3983,-12724.5998 4036.9401,-12621.4145 4037.6937,-10745.9863 4041,-10629.5998 4062.7412,-9864.2836 4090.5745,-8924.5009 4095.3028,-8765.0901"/>
+<polygon fill="#000000" stroke="#000000" points="4097.0621,-8764.7999 4095.4613,-8759.7502 4093.5637,-8764.696 4097.0621,-8764.7999"/>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user&#45;&gt;io -->
+<g id="edge499" class="edge">
+<title>github.com/opencontainers/runc/libcontainer/user&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3498.1496,-14844.9619C3673.0401,-14845.1285 3949.3974,-14839.9971 3983,-14806.5998 4123.89,-14666.5706 3937.5706,-14084.1891 4041,-13914.5998 4046.481,-13905.6128 4055.4137,-13898.7632 4064.5015,-13893.6979"/>
+<polygon fill="#000000" stroke="#000000" points="4065.3498,-13895.2291 4068.9608,-13891.3531 4063.7208,-13892.1313 4065.3498,-13895.2291"/>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user&#45;&gt;strconv -->
+<g id="edge502" class="edge">
+<title>github.com/opencontainers/runc/libcontainer/user&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3365.7063,-14825.3841C3397.8797,-14771.8948 3490.2576,-14609.6627 3522,-14460.5998 3544.8242,-14353.4167 3517.725,-6660.7717 3580,-6570.5998 3689.144,-6412.5635 3866.2041,-6567.0682 3983,-6414.5998 4048.5326,-6329.0519 4088.169,-5523.7217 4094.9559,-5375.1151"/>
+<polygon fill="#000000" stroke="#000000" points="4096.7226,-5374.7855 4095.2014,-5369.7112 4093.2262,-5374.6266 4096.7226,-5374.7855"/>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user&#45;&gt;strings -->
+<g id="edge503" class="edge">
+<title>github.com/opencontainers/runc/libcontainer/user&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3365.6565,-14825.3734C3397.6951,-14771.8552 3489.7439,-14609.5524 3522,-14460.5998 3545.9362,-14350.067 3506.9935,-10475.974 3580,-10389.5998 3697.5553,-10250.5199 3864.7376,-10454.079 3983,-10315.5998 4025.8201,-10265.4596 4038.1019,-8008.4724 4041,-7942.5998 4055.7173,-7608.0844 4086.4039,-7201.5647 4094.179,-7100.9517"/>
+<polygon fill="#000000" stroke="#000000" points="4095.9329,-7100.9672 4094.5741,-7095.847 4092.4433,-7100.697 4095.9329,-7100.9672"/>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user&#45;&gt;os -->
+<g id="edge500" class="edge">
+<title>github.com/opencontainers/runc/libcontainer/user&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3362.0161,-14862.062C3389.9048,-14931.6357 3487.6165,-15185.1845 3522,-15404.5998 3546.0618,-15558.1482 3492.1908,-18073.3593 3580,-18201.5998 3689.4412,-18361.4327 3864.4645,-18214.3899 3983,-18367.5998 4098.5784,-18516.9876 3935.8559,-18631.6929 4041,-18788.5998 4046.6039,-18796.9626 4055.1871,-18803.4263 4063.9186,-18808.2834"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5249,-18810.0523 4068.7667,-18810.8183 4065.1467,-18806.9507 4063.5249,-18810.0523"/>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user&#45;&gt;golang.org/x/sys/unix -->
+<g id="edge498" class="edge">
+<title>github.com/opencontainers/runc/libcontainer/user&#45;&gt;golang.org/x/sys/unix</title>
+<path fill="none" stroke="#000000" d="M3364.5761,-14825.5419C3394.0583,-14771.8332 3480.6703,-14607.6709 3522,-14460.5998 3547.7878,-14368.8345 3513.5044,-14103.8948 3580,-14035.5998 3612.7932,-14001.9192 3665.6807,-13994.2772 3709.0452,-13994.735"/>
+<polygon fill="#000000" stroke="#000000" points="3709.2535,-13996.489 3714.2844,-13994.8298 3709.3169,-13992.9896 3709.2535,-13996.489"/>
+</g>
+<!-- os/user -->
+<g id="node122" class="node">
+<title>os/user</title>
+<g id="a_node122"><a xlink:href="https://godoc.org/os/user" xlink:title="os/user" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3797,-14982.5998C3797,-14982.5998 3766,-14982.5998 3766,-14982.5998 3760,-14982.5998 3754,-14976.5998 3754,-14970.5998 3754,-14970.5998 3754,-14958.5998 3754,-14958.5998 3754,-14952.5998 3760,-14946.5998 3766,-14946.5998 3766,-14946.5998 3797,-14946.5998 3797,-14946.5998 3803,-14946.5998 3809,-14952.5998 3809,-14958.5998 3809,-14958.5998 3809,-14970.5998 3809,-14970.5998 3809,-14976.5998 3803,-14982.5998 3797,-14982.5998"/>
+<text text-anchor="middle" x="3781.5" y="-14960.8998" font-family="Times,serif" font-size="14.00" fill="#000000">os/user</text>
+</a>
+</g>
+</g>
+<!-- github.com/opencontainers/runc/libcontainer/user&#45;&gt;os/user -->
+<g id="edge501" class="edge">
+<title>github.com/opencontainers/runc/libcontainer/user&#45;&gt;os/user</title>
+<path fill="none" stroke="#000000" d="M3398.791,-14861.7282C3443.8099,-14879.625 3515.8612,-14906.8208 3580,-14924.5998 3638.5486,-14940.8292 3708.4017,-14953.1716 3748.5889,-14959.6136"/>
+<polygon fill="#000000" stroke="#000000" points="3748.6059,-14961.3882 3753.8187,-14960.4454 3749.1557,-14957.9316 3748.6059,-14961.3882"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;bufio -->
+<g id="edge332" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M2685.6298,-15106.7203C2700.4059,-15112.1972 2714.6934,-15119.3249 2727,-15128.5998 2767.1136,-15158.8316 2753.4864,-15186.4852 2785,-15225.5998 2938.6731,-15416.3385 2963.961,-15501.3564 3187,-15602.5998 3509.5289,-15749.0044 3728.2111,-15887.6522 3983,-15641.5998 4115.9175,-15513.24 3947.169,-14138.7823 4041,-13979.5998 4046.3454,-13970.5315 4055.2426,-13963.6606 4064.3396,-13958.6009"/>
+<polygon fill="#000000" stroke="#000000" points="4065.1898,-13960.1312 4068.8068,-13956.2608 4063.5656,-13957.0308 4065.1898,-13960.1312"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;errors -->
+<g id="edge333" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2577.803,-15070.3537C2590.186,-14896.1236 2687.5205,-13508.2134 2727,-12381.5998 2734.7736,-12159.7681 2723.5067,-8599.8797 2785,-8386.5998 2866.4469,-8104.1141 3039.2105,-8101.5456 3129,-7821.5998 3182.3553,-7655.2486 3100.4289,-7189.3396 3187,-7037.5998 3276.8725,-6880.0734 3419.1439,-6953.9728 3522,-6804.5998 3582.7669,-6716.3509 3495.6125,-6636.6244 3580,-6570.5998 3721.0654,-6460.2307 3829.4248,-6478.4291 3983,-6570.5998 4052.7292,-6612.449 4081.7928,-6713.5355 4091.6858,-6759.3439"/>
+<polygon fill="#000000" stroke="#000000" points="4090.0047,-6759.854 4092.7454,-6764.3874 4093.4299,-6759.1343 4090.0047,-6759.854"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;fmt -->
+<g id="edge334" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2577.5072,-15070.5981C2592.7974,-14797.3719 2777.1622,-11505.3288 2785,-11482.5998 2872.4572,-11228.982 3042.9073,-11240.6841 3129,-10986.5998 3173.0138,-10856.7025 3104.1156,-9862.8734 3187,-9753.5998 3281.7883,-9628.6324 3421.1132,-9762.6979 3522,-9642.5998 3635.564,-9507.4105 3456.2968,-9375.5778 3580,-9249.5998 3707.2141,-9120.0464 3849.2049,-9305.345 3983,-9182.5998 4046.2576,-9124.5666 4083.5253,-8847.4811 4093.3624,-8764.8865"/>
+<polygon fill="#000000" stroke="#000000" points="4095.1226,-8764.9024 4093.9707,-8759.7318 4091.6467,-8764.4922 4095.1226,-8764.9024"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;io -->
+<g id="edge340" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2661.3378,-15106.6446C2699.0388,-15113.994 2744.064,-15121.86 2785,-15126.5998 2917.3014,-15141.9186 3886.6694,-15258.5712 3983,-15166.5998 4083.7246,-15070.4334 3969.6003,-14034.1636 4041,-13914.5998 4046.397,-13905.5621 4055.3077,-13898.6993 4064.4013,-13893.6374"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2507,-13895.1681 4068.8655,-13891.2956 4063.6247,-13892.0687 4065.2507,-13895.1681"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;io/ioutil -->
+<g id="edge341" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2578.4901,-15106.6476C2588.4752,-15191.4983 2638.57,-15557.3068 2785,-15812.5998 2801.1101,-15840.6869 3158.2699,-16219.6667 3187,-16234.5998 3492.3398,-16393.3069 3934.9361,-16329.9889 4061.2001,-16307.3445"/>
+<polygon fill="#000000" stroke="#000000" points="4061.659,-16309.0399 4066.2672,-16306.427 4061.0353,-16305.5959 4061.659,-16309.0399"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;strconv -->
+<g id="edge346" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M2577.8169,-15070.3542C2590.3296,-14896.1285 2688.6343,-13508.2518 2727,-12381.5998 2735.7453,-12124.784 2725.8145,-8006.6556 2785,-7756.5998 2865.5778,-7416.1628 3011.8498,-7378.245 3129,-7048.5998 3166.7606,-6942.3464 3110.4552,-6884.4041 3187,-6801.5998 3293.097,-6686.8268 3425.7877,-6817.777 3522,-6694.5998 3644.9588,-6537.1799 3491.0885,-5976.4703 3580,-5797.5998 3690.9174,-5574.4583 3968.1769,-5416.7893 4063.3363,-5367.7432"/>
+<polygon fill="#000000" stroke="#000000" points="4064.3478,-5369.1912 4067.9983,-5365.3523 4062.7506,-5366.0768 4064.3478,-5369.1912"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;strings -->
+<g id="edge347" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2577.7827,-15070.353C2589.9763,-14896.1161 2685.8937,-13508.1552 2727,-12381.5998 2733.751,-12196.5838 2739.1623,-9228.9749 2785,-9049.5998 2866.5077,-8730.6388 3018.9488,-8703.8713 3129,-8393.5998 3172.648,-8270.5415 3108.4767,-8209.9194 3187,-8105.5998 3287.8979,-7971.5552 3423.4921,-8067.4105 3522,-7931.5998 3615.094,-7803.2532 3467.9662,-7691.7951 3580,-7579.5998 3708.2965,-7451.1183 3849.0017,-7635.1231 3983,-7512.5998 4045.4747,-7455.4751 4083.2245,-7182.6724 4093.2795,-7100.8494"/>
+<polygon fill="#000000" stroke="#000000" points="4095.0343,-7100.9153 4093.9017,-7095.7404 4091.56,-7100.4921 4095.0343,-7100.9153"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;time -->
+<g id="edge349" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2578.8615,-15070.4768C2594.0728,-14953.2137 2679.1233,-14289.5816 2727,-13746.5998 2735.821,-13646.5588 2726.1378,-12925.971 2785,-12844.5998 2953.1901,-12612.0936 3810.4339,-12591.8769 3983,-12362.5998 4059.0333,-12261.5795 4026.1687,-11922.1634 4041,-11796.5998 4058.4953,-11648.4832 4083.6948,-11470.9241 4092.6705,-11408.5837"/>
+<polygon fill="#000000" stroke="#000000" points="4094.4072,-11408.8011 4093.3885,-11403.6025 4090.943,-11408.3017 4094.4072,-11408.8011"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;unsafe -->
+<g id="edge350" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;unsafe</title>
+<path fill="none" stroke="#000000" d="M2582.1279,-15106.8971C2603.0198,-15173.4116 2680.348,-15407.4924 2785,-15579.5998 2803.2049,-15609.5391 3155.2957,-16050.6796 3187,-16065.5998 3507.548,-16216.4512 3736.8837,-16278.4212 3983,-16023.5998 4055.724,-15948.3036 4091.2445,-14254.7098 4095.5521,-14035.0111"/>
+<polygon fill="#000000" stroke="#000000" points="4097.3057,-14034.8391 4095.6538,-14029.8059 4093.8064,-14034.7708 4097.3057,-14034.8391"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge337" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M2577.4901,-15070.5322C2592.3415,-14799.6831 2769.502,-11574.3165 2785,-11556.5998 2887.5546,-11439.3638 3015.7797,-11596.5713 3129,-11489.5998 3205.0032,-11417.7914 3122.9901,-11344.2779 3187,-11261.5998 3213.5327,-11227.3289 3256.6032,-11204.0778 3292.4243,-11189.5238"/>
+<polygon fill="#000000" stroke="#000000" points="3293.2084,-11191.0951 3297.2031,-11187.6159 3291.9107,-11187.8446 3293.2084,-11191.0951"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;github.com/pkg/errors -->
+<g id="edge338" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M2577.7677,-15070.3524C2589.8209,-14896.1103 2684.6879,-13508.1105 2727,-12381.5998 2730.094,-12299.2255 2737.1294,-9481.708 2785,-9414.5998 2879.5549,-9282.0465 2986.1141,-9366.6679 3129,-9288.5998 3157.3943,-9273.0861 3158.8163,-9260.493 3187,-9244.5998 3326.5365,-9165.9132 3419.0634,-9234.3438 3522,-9111.5998 3604.222,-9013.5565 3486.2131,-8916.6456 3580,-8829.5998 3613.5387,-8798.4718 3664.8294,-8792.2152 3707.2683,-8793.6164"/>
+<polygon fill="#000000" stroke="#000000" points="3707.3314,-8795.3703 3712.3982,-8793.8241 3707.4731,-8791.8731 3707.3314,-8795.3703"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;os -->
+<g id="edge342" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2578.5748,-15106.6789C2593.1428,-15234.2107 2680.8374,-16011.7128 2727,-16646.5998 2736.5677,-16778.1877 2726.7733,-17714.2082 2785,-17832.5998 2873.632,-18012.8142 3040.6814,-17945.2317 3129,-18125.5998 3191.4624,-18253.1633 3097.4193,-19292.376 3187,-19402.5998 3283.9482,-19521.8889 3393.5827,-19404.114 3522,-19488.5998 3557.2027,-19511.7597 3542.1726,-19545.034 3580,-19563.5998 3740.7889,-19642.5157 3847.9831,-19681.2911 3983,-19563.5998 4054.1898,-19501.5452 4026.6034,-19235.9352 4041,-19142.5998 4058.3018,-19030.4295 4082.1644,-18896.8538 4091.7564,-18843.8901"/>
+<polygon fill="#000000" stroke="#000000" points="4093.5231,-18843.9546 4092.6933,-18838.7226 4090.0793,-18843.3302 4093.5231,-18843.9546"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;path/filepath -->
+<g id="edge344" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M2576.7249,-15106.6099C2578.8574,-15246.7272 2599.8209,-16161.9516 2785,-16346.5998 2894.689,-16455.9744 3027.4335,-16285.6437 3129,-16402.5998 3230.7987,-16519.8233 3085.4511,-19079.1599 3187,-19196.5998 3285.7358,-19310.7865 3401.3469,-19161.8794 3522,-19252.5998 3569.6656,-19288.4402 3529.7496,-19341.4837 3580,-19373.5998 3727.7709,-19468.0433 3951.2501,-19422.1492 4048.261,-19395.3141"/>
+<polygon fill="#000000" stroke="#000000" points="4049.0014,-19396.9243 4053.3457,-19393.8929 4048.0592,-19393.5535 4049.0014,-19396.9243"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;golang.org/x/sys/unix -->
+<g id="edge339" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;golang.org/x/sys/unix</title>
+<path fill="none" stroke="#000000" d="M2689.832,-15104.5851C2896.8875,-15126.5103 3332.4224,-15137.3238 3522,-14876.5998 3632.1687,-14725.086 3453.8691,-14174.1082 3580,-14035.5998 3611.9447,-14000.5203 3665.5649,-13993.101 3709.464,-13994.0081"/>
+<polygon fill="#000000" stroke="#000000" points="3709.4474,-13995.7582 3714.4944,-13994.1489 3709.5454,-13992.2596 3709.4474,-13995.7582"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;runtime -->
+<g id="edge345" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M2576.9008,-15106.6531C2579.7976,-15205.1724 2603.0866,-15685.5609 2785,-16016.5998 2889.2813,-16206.3669 3042.6477,-16163.0314 3129,-16361.5998 3270.0019,-16685.8359 3038.6993,-17630.6365 3187,-17951.5998 3349.8587,-18304.0707 3681.0346,-18555.681 3983,-18311.5998 4023.4829,-18278.8772 4078.9643,-17904.8754 4092.7843,-17807.606"/>
+<polygon fill="#000000" stroke="#000000" points="4094.5171,-17807.8502 4093.4858,-17802.6541 4091.0517,-17807.3592 4094.5171,-17807.8502"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;os/exec -->
+<g id="edge343" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;os/exec</title>
+<path fill="none" stroke="#000000" d="M2578.2875,-15106.622C2590.0675,-15223.1838 2659.9695,-15882.1015 2785,-16405.5998 2900.7469,-16890.2279 3043.6243,-16981.7101 3129,-17472.5998 3145.982,-17570.242 3123.6885,-19179.35 3187,-19255.5998 3284.4051,-19372.9107 3399.4866,-19238.8251 3522,-19329.5998 3564.2122,-19360.8764 3537.4549,-19401.7775 3580,-19432.5998 3630.0962,-19468.8926 3704.808,-19474.3682 3747.7826,-19474.1258"/>
+<polygon fill="#000000" stroke="#000000" points="3747.882,-19475.8749 3752.8623,-19474.0696 3747.8432,-19472.3751 3747.882,-19475.8749"/>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;syscall -->
+<g id="edge348" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;syscall</title>
+<path fill="none" stroke="#000000" d="M2578.4639,-15106.6874C2592.2726,-15234.2775 2675.7158,-16012.106 2727,-16646.5998 2742.9855,-16844.3739 2723.867,-17348.8331 2785,-17537.5998 2873.4834,-17810.819 3042.9633,-17810.6004 3129,-18084.5998 3169.9883,-18215.1344 3107.5044,-19203.2456 3187,-19314.5998 3280.0772,-19444.9787 3388.1502,-19359.5869 3522,-19447.5998 3552.4602,-19467.6289 3546.3197,-19491.649 3580,-19505.5998 3745.4771,-19574.1427 3851.3424,-19627.0373 3983,-19505.5998 4100.5023,-19397.2188 3956.6918,-18924.4135 4041,-18788.5998 4046.4013,-18779.8988 4055.0202,-18773.1904 4063.8477,-18768.166"/>
+<polygon fill="#000000" stroke="#000000" points="4065.168,-18769.4449 4068.7542,-18765.5459 4063.5193,-18766.3575 4065.168,-18769.4449"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount -->
+<g id="node104" class="node">
+<title>github.com/docker/docker/pkg/mount</title>
+<g id="a_node104"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/mount" xlink:title="github.com/docker/docker/pkg/mount" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3056.5,-12307.5998C3056.5,-12307.5998 2857.5,-12307.5998 2857.5,-12307.5998 2851.5,-12307.5998 2845.5,-12301.5998 2845.5,-12295.5998 2845.5,-12295.5998 2845.5,-12283.5998 2845.5,-12283.5998 2845.5,-12277.5998 2851.5,-12271.5998 2857.5,-12271.5998 2857.5,-12271.5998 3056.5,-12271.5998 3056.5,-12271.5998 3062.5,-12271.5998 3068.5,-12277.5998 3068.5,-12283.5998 3068.5,-12283.5998 3068.5,-12295.5998 3068.5,-12295.5998 3068.5,-12301.5998 3062.5,-12307.5998 3056.5,-12307.5998"/>
+<text text-anchor="middle" x="2957" y="-12285.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/mount</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;github.com/docker/docker/pkg/mount -->
+<g id="edge335" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;github.com/docker/docker/pkg/mount</title>
+<path fill="none" stroke="#000000" d="M2577.2112,-15070.317C2584.9592,-14874.3768 2655.9151,-13159.8341 2785,-12667.5998 2822.357,-12525.148 2909.5989,-12369.3749 2943.3392,-12312.2252"/>
+<polygon fill="#000000" stroke="#000000" points="2945.0119,-12312.8353 2946.0556,-12307.6417 2942.0009,-12311.0508 2945.0119,-12312.8353"/>
+</g>
+<!-- github.com/docker/go&#45;units -->
+<g id="node105" class="node">
+<title>github.com/docker/go&#45;units</title>
+<g id="a_node105"><a xlink:href="https://godoc.org/github.com/docker/go-units" xlink:title="github.com/docker/go&#45;units" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M3853,-7629.5998C3853,-7629.5998 3710,-7629.5998 3710,-7629.5998 3704,-7629.5998 3698,-7623.5998 3698,-7617.5998 3698,-7617.5998 3698,-7605.5998 3698,-7605.5998 3698,-7599.5998 3704,-7593.5998 3710,-7593.5998 3710,-7593.5998 3853,-7593.5998 3853,-7593.5998 3859,-7593.5998 3865,-7599.5998 3865,-7605.5998 3865,-7605.5998 3865,-7617.5998 3865,-7617.5998 3865,-7623.5998 3859,-7629.5998 3853,-7629.5998"/>
+<text text-anchor="middle" x="3781.5" y="-7607.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go&#45;units</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/docker/pkg/system&#45;&gt;github.com/docker/go&#45;units -->
+<g id="edge336" class="edge">
+<title>github.com/docker/docker/pkg/system&#45;&gt;github.com/docker/go&#45;units</title>
+<path fill="none" stroke="#000000" d="M2577.7736,-15070.3526C2589.8818,-14896.1126 2685.1601,-13508.1282 2727,-12381.5998 2730.1966,-12295.533 2743.6268,-9357.1378 2785,-9281.5998 2873.2299,-9120.5122 3012.2337,-9194.3721 3129,-9052.5998 3417.9232,-8701.8026 3389.0233,-8543.1715 3522,-8108.5998 3560.0776,-7984.161 3519.6674,-7936.9033 3580,-7821.5998 3622.8903,-7739.6309 3707.5018,-7667.4916 3752.2162,-7633.0507"/>
+<polygon fill="#000000" stroke="#000000" points="3753.5091,-7634.2649 3756.4161,-7629.8363 3751.3819,-7631.4855 3753.5091,-7634.2649"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;bufio -->
+<g id="edge322" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M2961.6392,-12307.7881C2985.0374,-12400.4844 3090.4936,-12829.9414 3129,-13188.5998 3142.8342,-13317.4549 3101.9492,-15422.8175 3187,-15520.5998 3303.4748,-15654.51 3855.2598,-15708.81 3983,-15585.5998 4111.5198,-15461.6377 3950.2332,-14133.3702 4041,-13979.5998 4046.3509,-13970.5347 4055.2495,-13963.6647 4064.3462,-13958.6048"/>
+<polygon fill="#000000" stroke="#000000" points="4065.1963,-13960.1351 4068.8131,-13956.2645 4063.5719,-13957.0348 4065.1963,-13960.1351"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;fmt -->
+<g id="edge323" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3068.7474,-12290.0554C3091.5757,-12284.8192 3113.3657,-12274.9938 3129,-12257.5998 3240.3026,-12133.7699 3096.7303,-11650.5052 3187,-11510.5998 3279.0965,-11367.8632 3432.3831,-11470.9061 3522,-11326.5998 3680.4154,-11071.5103 3421.7427,-10231.7875 3580,-9976.5998 3687.7737,-9802.8162 3869.368,-9924.6106 3983,-9754.5998 4039.9163,-9669.4444 4086.3083,-8909.7679 4094.6594,-8765.2758"/>
+<polygon fill="#000000" stroke="#000000" points="4096.4222,-8765.1022 4094.9626,-8760.0098 4092.928,-8764.9009 4096.4222,-8765.1022"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;io -->
+<g id="edge327" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2961.5737,-12307.7953C2984.6523,-12400.5267 3088.7939,-12830.128 3129,-13188.5998 3139.4587,-13281.8482 3129.9723,-14802.0848 3187,-14876.5998 3295.7404,-15018.6851 3853.1449,-15120.6866 3983,-14997.5998 4070.4592,-14914.6991 3978.8432,-14017.8383 4041,-13914.5998 4046.4296,-13905.5816 4055.3488,-13898.7239 4064.4401,-13893.6607"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2891,-13895.1916 4068.9024,-13891.3177 4063.662,-13892.0928 4065.2891,-13895.1916"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;sort -->
+<g id="edge329" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3068.7534,-12291.0348C3091.8677,-12285.8188 3113.7804,-12275.7559 3129,-12257.5998 3251.7764,-12111.1348 3083.5919,-5538.3257 3187,-5377.5998 3275.8656,-5239.477 3429.6465,-5357.4153 3522,-5221.5998 3583.0865,-5131.7657 3500.9373,-4320.1036 3580,-4245.5998 3710.3531,-4122.7631 3836.8502,-4142.0574 3983,-4245.5998 4046.4272,-4290.536 3991.4644,-4350.6957 4041,-4410.5998 4047.2369,-4418.1422 4055.7441,-4424.2967 4064.2031,-4429.1206"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6542,-4430.8139 4068.883,-4431.664 4065.3255,-4427.7387 4063.6542,-4430.8139"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;strconv -->
+<g id="edge330" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3068.7094,-12290.9979C3091.8255,-12285.7834 3113.75,-12275.7304 3129,-12257.5998 3217.0669,-12152.8975 3135.102,-7459.1895 3187,-7332.5998 3268.4364,-7133.9599 3433.7989,-7180.3299 3522,-6984.5998 3587.9472,-6838.254 3489.994,-6397.5097 3580,-6264.5998 3692.119,-6099.0361 3864.6079,-6225.7374 3983,-6064.5998 4066.5563,-5950.8755 4090.5506,-5485.4868 4095.1149,-5375.1427"/>
+<polygon fill="#000000" stroke="#000000" points="4096.875,-5374.9282 4095.3297,-5369.8612 4093.3779,-5374.7859 4096.875,-5374.9282"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;strings -->
+<g id="edge331" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3068.5264,-12290.8428C3091.65,-12285.6347 3113.6239,-12275.6235 3129,-12257.5998 3210.2626,-12162.345 3121.9842,-10111.6046 3187,-10004.5998 3275.3178,-9859.2439 3423.0886,-9957.965 3522,-9819.5998 3608.0917,-9699.1678 3488.536,-9608.0042 3580,-9491.5998 3700.8643,-9337.7783 3872.606,-9476.1003 3983,-9314.5998 4026.0517,-9251.6176 4036.9681,-8018.7835 4041,-7942.5998 4058.6959,-7608.2288 4087.183,-7201.6024 4094.3318,-7100.9591"/>
+<polygon fill="#000000" stroke="#000000" points="4096.0858,-7100.9644 4094.6949,-7095.8529 4092.5946,-7100.7161 4096.0858,-7100.9644"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;github.com/pkg/errors -->
+<g id="edge324" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M3068.55,-12290.863C3091.6726,-12285.654 3113.6401,-12275.6374 3129,-12257.5998 3216.2978,-12155.0834 3148.8672,-9963.737 3187,-9834.5998 3267.6275,-9561.5536 3380.3374,-9535.5547 3522,-9288.5998 3622.464,-9113.4649 3733.9852,-8896.8022 3769.7751,-8826.6621"/>
+<polygon fill="#000000" stroke="#000000" points="3771.5075,-8827.1171 3772.22,-8821.8678 3768.3895,-8825.527 3771.5075,-8827.1171"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;github.com/sirupsen/logrus -->
+<g id="edge325" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;github.com/sirupsen/logrus</title>
+<path fill="none" stroke="#000000" d="M3068.519,-12294.0886C3131.1611,-12296.6101 3208.2258,-12299.7121 3266.5666,-12302.0604"/>
+<polygon fill="#000000" stroke="#000000" points="3266.7305,-12303.8183 3271.7969,-12302.2709 3266.8713,-12300.3211 3266.7305,-12303.8183"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;os -->
+<g id="edge328" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2961.7393,-12307.7777C2985.6268,-12400.4233 3093.0949,-12829.6717 3129,-13188.5998 3136.7353,-13265.9264 3140.2958,-18721.4874 3187,-18783.5998 3294.4697,-18926.5248 3812.7544,-18956.3146 3983,-18901.5998 4019.1279,-18889.9888 4053.4993,-18862.0902 4074.6462,-18842.2882"/>
+<polygon fill="#000000" stroke="#000000" points="4076.0153,-18843.4015 4078.4383,-18838.6907 4073.6064,-18840.8623 4076.0153,-18843.4015"/>
+</g>
+<!-- github.com/docker/docker/pkg/mount&#45;&gt;golang.org/x/sys/unix -->
+<g id="edge326" class="edge">
+<title>github.com/docker/docker/pkg/mount&#45;&gt;golang.org/x/sys/unix</title>
+<path fill="none" stroke="#000000" d="M2961.1868,-12307.8442C2982.3752,-12400.8145 3078.743,-12831.3984 3129,-13188.5998 3148.8654,-13329.7929 3096.5134,-13717.4077 3187,-13827.5998 3316.6406,-13985.4725 3581.0009,-14005.4292 3708.9326,-14005.2046"/>
+<polygon fill="#000000" stroke="#000000" points="3709.1802,-14006.9537 3714.1732,-14005.1839 3709.1663,-14003.4537 3709.1802,-14006.9537"/>
+</g>
+<!-- github.com/docker/go&#45;units&#45;&gt;fmt -->
+<g id="edge377" class="edge">
+<title>github.com/docker/go&#45;units&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3865.2377,-7606.5713C3906.5537,-7608.8486 3953.6906,-7619.4405 3983,-7651.5998 4020.3619,-7692.5947 4083.4409,-8563.6475 4094.3644,-8718.2456"/>
+<polygon fill="#000000" stroke="#000000" points="4092.6333,-8718.5759 4094.7309,-8723.4404 4096.1246,-8718.3296 4092.6333,-8718.5759"/>
+</g>
+<!-- github.com/docker/go&#45;units&#45;&gt;strconv -->
+<g id="edge379" class="edge">
+<title>github.com/docker/go&#45;units&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3786.0688,-7593.4549C3810.3878,-7496.1833 3925.6695,-7026.3297 3983,-6635.5998 4057.3381,-6128.9569 4089.0659,-5502.1522 4094.9636,-5374.8016"/>
+<polygon fill="#000000" stroke="#000000" points="4096.7122,-5374.8706 4095.194,-5369.7954 4093.2159,-5374.7096 4096.7122,-5374.8706"/>
+</g>
+<!-- github.com/docker/go&#45;units&#45;&gt;strings -->
+<g id="edge380" class="edge">
+<title>github.com/docker/go&#45;units&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3865.1873,-7615.0131C3905.724,-7612.1636 3952.1748,-7601.5464 3983,-7571.5998 4052.2954,-7504.2795 4085.8615,-7188.8799 4094.0012,-7100.6089"/>
+<polygon fill="#000000" stroke="#000000" points="4095.7459,-7100.7462 4094.4575,-7095.6079 4092.2603,-7100.4282 4095.7459,-7100.7462"/>
+</g>
+<!-- github.com/docker/go&#45;units&#45;&gt;time -->
+<g id="edge381" class="edge">
+<title>github.com/docker/go&#45;units&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3809.3128,-7629.8435C3855.6042,-7662.0655 3946.9305,-7733.9964 3983,-7821.5998 4057.7172,-8003.0685 3941.7666,-11184.2886 4041,-11353.5998 4046.2805,-11362.6094 4055.1608,-11369.3286 4064.2622,-11374.2203"/>
+<polygon fill="#000000" stroke="#000000" points="4063.481,-11375.7862 4068.7332,-11376.4769 4065.0581,-11372.6616 4063.481,-11375.7862"/>
+</g>
+<!-- github.com/docker/go&#45;units&#45;&gt;regexp -->
+<g id="edge378" class="edge">
+<title>github.com/docker/go&#45;units&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M3786.5728,-7593.5195C3813.4875,-7496.5805 3939.9669,-7028.1616 3983,-6635.5998 4078.055,-5768.4768 3976.8689,-3580.5567 4041,-2710.5998 4052.0174,-2561.146 4081.2239,-2383.1042 4091.9636,-2320.6283"/>
+<polygon fill="#000000" stroke="#000000" points="4093.6992,-2320.8612 4092.8248,-2315.6365 4090.2502,-2320.2662 4093.6992,-2320.8612"/>
+</g>
+<!-- golang.org/x/net/proxy&#45;&gt;context -->
+<g id="edge655" class="edge">
+<title>golang.org/x/net/proxy&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M3359.345,-7534.371C3383.0484,-7444.1641 3487.063,-7036.1982 3522,-6694.5998 3536.8415,-6549.4868 3514.8399,-1574.1074 3580,-1443.5998 3680.9417,-1241.426 3834.7175,-1304.1161 3983,-1133.5998 4031.7782,-1077.5076 4069.6159,-997.1098 4086.4499,-957.8961"/>
+<polygon fill="#000000" stroke="#000000" points="4088.2415,-958.1552 4088.5894,-952.8692 4085.021,-956.7845 4088.2415,-958.1552"/>
+</g>
+<!-- golang.org/x/net/proxy&#45;&gt;errors -->
+<g id="edge656" class="edge">
+<title>golang.org/x/net/proxy&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3425.0537,-7551.2047C3458.1793,-7547.0217 3495.9989,-7536.6649 3522,-7512.5998 3583.6849,-7455.5081 3536.4804,-7404.5063 3580,-7332.5998 3712.2981,-7114.0067 3813.0012,-7113.3514 3983,-6922.5998 4018.8515,-6882.3717 4057.6994,-6832.7845 4079.2241,-6804.7208"/>
+<polygon fill="#000000" stroke="#000000" points="4080.6218,-6805.774 4082.2718,-6800.7401 4077.8427,-6803.6463 4080.6218,-6805.774"/>
+</g>
+<!-- golang.org/x/net/proxy&#45;&gt;strings -->
+<g id="edge661" class="edge">
+<title>golang.org/x/net/proxy&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3425.1497,-7548.5321C3457.3236,-7543.6535 3494.3109,-7533.4828 3522,-7512.5998 3563.9447,-7480.9653 3537.2346,-7440.1157 3580,-7409.5998 3728.2359,-7303.8237 3840.2378,-7448.6548 3983,-7335.5998 4059.5331,-7274.9923 4085.4205,-7152.0054 4093.1093,-7100.7933"/>
+<polygon fill="#000000" stroke="#000000" points="4094.851,-7100.976 4093.8384,-7095.7763 4091.3874,-7100.4726 4094.851,-7100.976"/>
+</g>
+<!-- golang.org/x/net/proxy&#45;&gt;sync -->
+<g id="edge662" class="edge">
+<title>golang.org/x/net/proxy&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3359.3017,-7534.3665C3382.7999,-7444.1382 3485.9937,-7036.0871 3522,-6694.5998 3531.762,-6602.0158 3523.7964,-3417.8174 3580,-3343.5998 3698.7412,-3186.8007 3970.5496,-3183.0121 4063.8593,-3186.6611"/>
+<polygon fill="#000000" stroke="#000000" points="4063.8888,-3188.4138 4068.9578,-3186.8751 4064.0356,-3184.9169 4063.8888,-3188.4138"/>
+</g>
+<!-- golang.org/x/net/proxy&#45;&gt;net/url -->
+<g id="edge659" class="edge">
+<title>golang.org/x/net/proxy&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M3359.3386,-7534.3703C3383.0117,-7444.1603 3486.9049,-7036.182 3522,-6694.5998 3535.756,-6560.7117 3514.2542,-1967.0424 3580,-1849.5998 3689.108,-1654.6989 3968.8344,-1560.3098 4063.8551,-1533.1367"/>
+<polygon fill="#000000" stroke="#000000" points="4064.4426,-1534.7892 4068.7762,-1531.7424 4063.4885,-1531.4217 4064.4426,-1534.7892"/>
+</g>
+<!-- golang.org/x/net/proxy&#45;&gt;os -->
+<g id="edge660" class="edge">
+<title>golang.org/x/net/proxy&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3356.1482,-7570.6329C3371.0607,-7734.7248 3483.0288,-8986.6314 3522,-10004.5998 3529.4048,-10198.0204 3515.192,-16790.2094 3580,-16972.5998 3676.0271,-17242.8508 3882.1437,-17208.1137 3983,-17476.5998 4034.3136,-17613.2 3965.4752,-18663.7451 4041,-18788.5998 4046.4051,-18797.5353 4055.3179,-18804.235 4064.4109,-18809.1318"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6234,-18810.6946 4068.8746,-18811.3927 4065.2049,-18807.5722 4063.6234,-18810.6946"/>
+</g>
+<!-- golang.org/x/net/proxy&#45;&gt;net -->
+<g id="edge658" class="edge">
+<title>golang.org/x/net/proxy&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M3356.1476,-7570.6329C3371.0545,-7734.7251 3482.9827,-8986.6332 3522,-10004.5998 3525.6669,-10100.2691 3521.3122,-16821.9572 3580,-16897.5998 3693.5981,-17044.0163 3842.8987,-16883.2988 3983,-17004.5998 4032.6787,-17047.6121 3997.4989,-17091.3487 4041,-17140.5998 4047.4174,-17147.8654 4055.8975,-17153.9099 4064.2722,-17158.7168"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6754,-17160.3856 4068.9002,-17161.2598 4065.361,-17157.3182 4063.6754,-17160.3856"/>
+</g>
+<!-- golang.org/x/net/internal/socks -->
+<g id="node136" class="node">
+<title>golang.org/x/net/internal/socks</title>
+<g id="a_node136"><a xlink:href="https://godoc.org/golang.org/x/net/internal/socks" xlink:title="golang.org/x/net/internal/socks" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3862,-7871.5998C3862,-7871.5998 3701,-7871.5998 3701,-7871.5998 3695,-7871.5998 3689,-7865.5998 3689,-7859.5998 3689,-7859.5998 3689,-7847.5998 3689,-7847.5998 3689,-7841.5998 3695,-7835.5998 3701,-7835.5998 3701,-7835.5998 3862,-7835.5998 3862,-7835.5998 3868,-7835.5998 3874,-7841.5998 3874,-7847.5998 3874,-7847.5998 3874,-7859.5998 3874,-7859.5998 3874,-7865.5998 3868,-7871.5998 3862,-7871.5998"/>
+<text text-anchor="middle" x="3781.5" y="-7849.8998" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/net/internal/socks</text>
+</a>
+</g>
+</g>
+<!-- golang.org/x/net/proxy&#45;&gt;golang.org/x/net/internal/socks -->
+<g id="edge657" class="edge">
+<title>golang.org/x/net/proxy&#45;&gt;golang.org/x/net/internal/socks</title>
+<path fill="none" stroke="#000000" d="M3403.3038,-7570.6661C3440.5285,-7586.7227 3490.5922,-7613.576 3522,-7651.5998 3570.7029,-7710.5619 3522.0776,-7763.6649 3580,-7813.5998 3608.3856,-7838.0711 3647.4189,-7849.1922 3683.5759,-7853.7998"/>
+<polygon fill="#000000" stroke="#000000" points="3683.716,-7855.5789 3688.8881,-7854.4332 3684.1305,-7852.1035 3683.716,-7855.5789"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;bytes -->
+<g id="edge509" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2578.537,-6956.7954C2593.7805,-7093.8477 2690.4236,-7978.5014 2727,-8700.5998 2732.0904,-8801.095 2739.9799,-12232.6088 2785,-12322.5998 2935.666,-12623.767 3799.0632,-12968.5198 3983,-13250.5998 4031.1333,-13324.4156 4078.8532,-13599.2647 4092.2803,-13681.3238"/>
+<polygon fill="#000000" stroke="#000000" points="4090.5834,-13681.7924 4093.1147,-13686.446 4094.0379,-13681.2296 4090.5834,-13681.7924"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;encoding/json -->
+<g id="edge510" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M2578.5727,-6956.7936C2594.0775,-7093.8331 2692.2783,-7978.4099 2727,-8700.5998 2741.229,-8996.5533 2723.276,-13743.805 2785,-14033.5998 2865.1164,-14409.7473 3044.1922,-14452.4822 3129,-14827.5998 3208.2904,-15178.3128 3091.4674,-16097.9587 3187,-16444.5998 3283.4471,-16794.5595 3282.1448,-16953.1001 3580,-17160.5998 3723.835,-17260.8019 3945.1062,-17253.5063 4044.5215,-17243.9808"/>
+<polygon fill="#000000" stroke="#000000" points="4044.935,-17245.6986 4049.7398,-17243.4679 4044.5926,-17242.2154 4044.935,-17245.6986"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;errors -->
+<g id="edge511" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2587.3794,-6920.3836C2615.5489,-6872.3019 2691.1624,-6737.2366 2727,-6614.5998 2754.1073,-6521.8383 2712.2501,-6247.2155 2785,-6183.5998 2824.4357,-6149.1155 3883.5331,-6107.6343 3983,-6190.5998 4072.5955,-6265.3316 4091.8444,-6659.1124 4095.3176,-6759.3629"/>
+<polygon fill="#000000" stroke="#000000" points="4093.5718,-6759.5205 4095.4892,-6764.4587 4097.0699,-6759.4026 4093.5718,-6759.5205"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;expvar -->
+<g id="edge512" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;expvar</title>
+<path fill="none" stroke="#000000" d="M2617.8159,-6920.5389C2659.4454,-6902.8214 2725.8182,-6875.8758 2785,-6857.5998 2833.0222,-6842.77 2890.0745,-6830.5302 2924.8982,-6823.6447"/>
+<polygon fill="#000000" stroke="#000000" points="2925.3021,-6825.3488 2929.8711,-6822.6679 2924.6275,-6821.9145 2925.3021,-6825.3488"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;fmt -->
+<g id="edge513" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2578.1411,-6956.7212C2588.891,-7071.3525 2652.9528,-7695.0262 2785,-7821.5998 2897.3852,-7929.3265 3010.7652,-7786.328 3129,-7887.5998 3187.082,-7937.3489 3133.8542,-7994.6088 3187,-8049.5998 3298.3249,-8164.79 3384.9886,-8099.5943 3522,-8182.5998 3551.1089,-8200.2348 3549.3029,-8217.9026 3580,-8232.5998 3744.2504,-8311.2399 3849.6464,-8182.5866 3983,-8306.5998 4044.9911,-8364.2489 4083.0771,-8636.6871 4093.2458,-8718.3868"/>
+<polygon fill="#000000" stroke="#000000" points="4091.526,-8718.74 4093.8752,-8723.488 4094.9997,-8718.3113 4091.526,-8718.74"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;io/ioutil -->
+<g id="edge522" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2578.5685,-6956.7938C2594.0424,-7093.8348 2692.0591,-7978.4204 2727,-8700.5998 2740.5617,-8980.9015 2702.7331,-13483.2994 2785,-13751.5998 2866.3425,-14016.8853 3043.8605,-14008.5084 3129,-14272.5998 3178.2357,-14425.3223 3108.7041,-15575.5353 3187,-15715.5998 3289.6707,-15899.2688 3391.5218,-15886.0502 3580,-15979.5998 3749.5815,-16063.7703 3835.0498,-15999.4757 3983,-16117.5998 4037.5959,-16161.1894 4072.7474,-16238.8135 4087.7141,-16277.4397"/>
+<polygon fill="#000000" stroke="#000000" points="4086.1896,-16278.3531 4089.6079,-16282.4 4089.4594,-16277.1047 4086.1896,-16278.3531"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;math -->
+<g id="edge523" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M2579.0511,-6920.3105C2593.9157,-6813.5668 2670.6406,-6260.1949 2727,-5807.5998 2740.8221,-5696.6009 2707.7413,-5388.488 2785,-5307.5998 2891.99,-5195.5837 3026.6884,-5367.9047 3129,-5251.5998 3255.9597,-5107.2757 3117.7293,-3701.9034 3187,-3522.5998 3334.4907,-3140.828 3673.6467,-2791.6376 3983,-3059.5998 4042.4047,-3111.0562 4085.8223,-3695.7789 4094.4354,-3820.3495"/>
+<polygon fill="#000000" stroke="#000000" points="4092.7072,-3820.7288 4094.7963,-3825.5969 4096.199,-3820.4885 4092.7072,-3820.7288"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;sort -->
+<g id="edge528" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M2578.8416,-6920.4194C2594.2618,-6802.4933 2683.7534,-6145.4614 2785,-5973.5998 2888.1177,-5798.562 3035.3165,-5852.8632 3129,-5672.5998 3184.8595,-5565.1163 3102.8896,-5217.7687 3187,-5130.5998 3291.4398,-5022.3624 3420.2313,-5193.3524 3522,-5082.5998 3672.9109,-4918.3669 3417.7944,-4233.6878 3580,-4080.5998 3710.2587,-3957.663 3845.8247,-3965.4315 3983,-4080.5998 4097.0489,-4176.3519 3955.8238,-4288.4498 4041,-4410.5998 4046.758,-4418.8572 4055.3856,-4425.2907 4064.1103,-4430.1524"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7093,-4431.9183 4068.9497,-4432.6932 4065.3363,-4428.8195 4063.7093,-4431.9183"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;strings -->
+<g id="edge529" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2722.0092,-6945.6844C2743.1367,-6946.4794 2764.6245,-6947.1603 2785,-6947.5998 2937.8533,-6950.8973 3000.5037,-7030.4474 3129,-6947.5998 3181.4921,-6913.7557 3134.5079,-6853.4439 3187,-6819.5998 3312.1345,-6738.9198 3374.2782,-6800.9939 3522,-6819.5998 3731.8365,-6846.0292 3805.074,-6823.266 3983,-6937.5998 4029.8354,-6967.6959 4066.2294,-7023.7472 4084.0186,-7055.0236"/>
+<polygon fill="#000000" stroke="#000000" points="4082.5321,-7055.9507 4086.5072,-7059.4524 4085.5834,-7054.2361 4082.5321,-7055.9507"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;sync -->
+<g id="edge530" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2579.1342,-6920.3206C2594.4667,-6813.6337 2673.3791,-6260.5275 2727,-5807.5998 2741.6827,-5683.5768 2705.8593,-5345.2126 2785,-5248.5998 2886.4624,-5124.7373 3032.5395,-5269.3962 3129,-5141.5998 3291.5065,-4926.3017 3028.4096,-2932.7986 3187,-2714.5998 3292.5496,-2569.3779 3845.0105,-2461.7563 3983,-2576.5998 4029.653,-2615.4273 4081.468,-3057.6154 4093.4386,-3165.1414"/>
+<polygon fill="#000000" stroke="#000000" points="4091.7199,-3165.5209 4094.0106,-3170.2975 4095.1985,-3165.135 4091.7199,-3165.5209"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;time -->
+<g id="edge532" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2578.347,-6956.8063C2592.1982,-7093.9387 2680.5439,-7979.0697 2727,-8700.5998 2736.5469,-8848.8771 2714.8213,-9905.6333 2785,-10036.5998 2874.3908,-10203.4199 3010.8101,-10139.7796 3129,-10287.5998 3169.5257,-10338.2854 3136.9441,-10380.299 3187,-10421.5998 3304.6121,-10518.6409 3415.9449,-10386.0466 3522,-10495.5998 3619.4932,-10596.3087 3480.7196,-10706.6523 3580,-10805.5998 3708.9842,-10934.1515 3858.1418,-10747.037 3983,-10879.5998 4128.518,-11034.0973 3924.191,-11176.3977 4041,-11353.5998 4046.5917,-11362.0826 4055.2642,-11368.6004 4064.0822,-11373.4704"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7331,-11375.2605 4068.9774,-11376.0084 4065.3441,-11372.1533 4063.7331,-11375.2605"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;unicode/utf8 -->
+<g id="edge533" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M2578.9462,-6920.297C2593.2202,-6813.4774 2667.1833,-6259.7509 2727,-5807.5998 2739.9635,-5709.6097 2716.1845,-5437.5544 2785,-5366.5998 2892.8429,-5255.4046 3026.7052,-5426.9194 3129,-5310.5998 3257.0389,-5165.0066 3047.6428,-3701.3994 3187,-3566.5998 3237.9887,-3517.2787 3919.9973,-3514.6599 3983,-3572.5998 4081.2735,-3662.9762 4017.3346,-4039.2015 4041,-4170.5998 4053.2372,-4238.5447 4076.6536,-4316.815 4088.5972,-4354.6955"/>
+<polygon fill="#000000" stroke="#000000" points="4086.9564,-4355.3108 4090.1345,-4359.5489 4090.293,-4354.2539 4086.9564,-4355.3108"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/beorn7/perks/quantile -->
+<g id="edge514" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/beorn7/perks/quantile</title>
+<path fill="none" stroke="#000000" d="M2577.9303,-6920.3991C2586.3844,-6818.8166 2634.6907,-6312.8718 2785,-5932.5998 2892.791,-5659.8962 3044.7179,-5650.4605 3129,-5369.5998 3155.7783,-5280.3643 3124.9815,-3763.1251 3187,-3693.5998 3310.8029,-3554.8118 3550.3311,-3566.1232 3684.4716,-3585.8132"/>
+<polygon fill="#000000" stroke="#000000" points="3684.39,-3587.5703 3689.5934,-3586.5766 3684.9061,-3584.1085 3684.39,-3587.5703"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/cespare/xxhash/v2 -->
+<g id="edge515" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/cespare/xxhash/v2</title>
+<path fill="none" stroke="#000000" d="M2578.1324,-6956.8218C2590.412,-7094.0676 2669.391,-7979.8744 2727,-8700.5998 2740.8313,-8873.6382 2732.395,-9314.1722 2785,-9479.5998 2874.8986,-9762.305 3009.6941,-9779.9934 3129,-10051.5998 3165.7067,-10135.1647 3130.1172,-10177.2219 3187,-10248.5998 3293.4049,-10382.1192 3419.104,-10300.3579 3522,-10436.5998 3588.9173,-10525.2033 3500.3647,-10602.226 3580,-10679.5998 3607.7781,-10706.5891 3648.1531,-10718.2535 3685.4796,-10722.7318"/>
+<polygon fill="#000000" stroke="#000000" points="3685.3237,-10724.4752 3690.4874,-10723.2922 3685.7131,-10720.9969 3685.3237,-10724.4752"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;os -->
+<g id="edge524" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2578.5842,-6956.7931C2594.1729,-7093.8285 2692.8736,-7978.3815 2727,-8700.5998 2743.4838,-9049.4477 2716.2587,-14644.1948 2785,-14986.5998 2864.7314,-15383.7475 3047.3111,-15433.8502 3129,-15830.5998 3146.0214,-15913.27 3131.9496,-18804.6191 3187,-18868.5998 3302.7789,-19003.1606 3819.2976,-19004.2503 3983,-18935.5998 4027.5937,-18916.899 4063.4611,-18871.0668 4082.0717,-18843.2381"/>
+<polygon fill="#000000" stroke="#000000" points="4083.6186,-18844.0711 4084.9091,-18838.9332 4080.6963,-18842.1449 4083.6186,-18844.0711"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;path/filepath -->
+<g id="edge525" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M2578.5884,-6956.7929C2594.2081,-7093.8269 2693.0934,-7978.3711 2727,-8700.5998 2735.7672,-8887.3455 2728.4141,-15252.4177 2785,-15430.5998 2866.2003,-15686.2896 3046.4443,-15671.3444 3129,-15926.5998 3180.5713,-16086.054 3094.0039,-18803.1833 3187,-18942.5998 3277.2249,-19077.8619 3385.3071,-19001.5578 3522,-19089.5998 3551.1062,-19108.3467 3550.7603,-19123.0618 3580,-19141.5998 3744.1789,-19245.6898 3810.0096,-19222.9221 3983,-19311.5998 4011.3817,-19326.1487 4042.1925,-19345.196 4064.4619,-19359.5916"/>
+<polygon fill="#000000" stroke="#000000" points="4063.6518,-19361.1521 4068.7987,-19362.406 4065.5571,-19358.2161 4063.6518,-19361.1521"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;runtime -->
+<g id="edge526" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M2578.5782,-6956.7933C2594.1233,-7093.8309 2692.5643,-7978.3962 2727,-8700.5998 2757.4407,-9339.0181 2678.7031,-13822.3574 2785,-14452.5998 2864.847,-14926.0189 3044.2515,-15004.0336 3129,-15476.5998 3202.8599,-15888.4501 3073.9405,-16955.7431 3187,-17358.5998 3268.4786,-17648.9266 3417.1104,-17665.8869 3522,-17948.5998 3568.4791,-18073.8767 3476.8034,-18158.7171 3580,-18243.5998 3649.1644,-18300.4899 3912.5109,-18298.8401 3983,-18243.5998 4053.5461,-18188.3149 4085.9722,-17893.2191 4093.971,-17807.7874"/>
+<polygon fill="#000000" stroke="#000000" points="4095.7234,-17807.8411 4094.4414,-17802.7011 4092.2383,-17807.5187 4095.7234,-17807.8411"/>
+</g>
+<!-- github.com/golang/protobuf/proto -->
+<g id="node110" class="node">
+<title>github.com/golang/protobuf/proto</title>
+<g id="a_node110"><a xlink:href="https://godoc.org/github.com/golang/protobuf/proto" xlink:title="github.com/golang/protobuf/proto" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3870.5,-6620.5998C3870.5,-6620.5998 3692.5,-6620.5998 3692.5,-6620.5998 3686.5,-6620.5998 3680.5,-6614.5998 3680.5,-6608.5998 3680.5,-6608.5998 3680.5,-6596.5998 3680.5,-6596.5998 3680.5,-6590.5998 3686.5,-6584.5998 3692.5,-6584.5998 3692.5,-6584.5998 3870.5,-6584.5998 3870.5,-6584.5998 3876.5,-6584.5998 3882.5,-6590.5998 3882.5,-6596.5998 3882.5,-6596.5998 3882.5,-6608.5998 3882.5,-6608.5998 3882.5,-6614.5998 3876.5,-6620.5998 3870.5,-6620.5998"/>
+<text text-anchor="middle" x="3781.5" y="-6598.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/proto</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge516" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M2722.1252,-6947.9459C2865.1591,-6953.8518 3070.2304,-6952.5023 3129,-6901.5998 3222.4327,-6820.6743 3092.7034,-6709.5171 3187,-6629.5998 3258.0795,-6569.3593 3524.0957,-6580.6 3675.0868,-6592.5014"/>
+<polygon fill="#000000" stroke="#000000" points="3675.2859,-6594.2727 3680.4089,-6592.9249 3675.5636,-6590.7837 3675.2859,-6594.2727"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;sync/atomic -->
+<g id="edge531" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;sync/atomic</title>
+<path fill="none" stroke="#000000" d="M2588.3402,-6956.8919C2662.0426,-7068.9162 3071.4355,-7664.7998 3580,-7886.5998 3742.0767,-7957.2862 3955.8591,-7971.3578 4049.1233,-7974.0408"/>
+<polygon fill="#000000" stroke="#000000" points="4049.2368,-7975.7944 4054.2826,-7974.1811 4049.332,-7972.2957 4049.2368,-7975.7944"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;runtime/debug -->
+<g id="edge527" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;runtime/debug</title>
+<path fill="none" stroke="#000000" d="M2578.4392,-6956.8007C2592.9657,-7093.8919 2685.3362,-7978.7771 2727,-8700.5998 2733.1686,-8807.4704 2734.4333,-10532.2473 2785,-10626.5998 2873.5432,-10791.8127 3028.8924,-10712.1269 3129,-10870.5998 3208.9981,-10997.2388 3099.5965,-11080.9538 3187,-11202.5998 3214.5366,-11240.9247 3263.3671,-11264.7888 3301.2252,-11278.411"/>
+<polygon fill="#000000" stroke="#000000" points="3300.9578,-11280.1722 3306.2552,-11280.1842 3302.1215,-11276.8713 3300.9578,-11280.1722"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/internal -->
+<g id="node123" class="node">
+<title>github.com/prometheus/client_golang/prometheus/internal</title>
+<g id="a_node123"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus/internal" xlink:title="github.com/prometheus/client_golang/prometheus/internal" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3113,-6233.5998C3113,-6233.5998 2801,-6233.5998 2801,-6233.5998 2795,-6233.5998 2789,-6227.5998 2789,-6221.5998 2789,-6221.5998 2789,-6209.5998 2789,-6209.5998 2789,-6203.5998 2795,-6197.5998 2801,-6197.5998 2801,-6197.5998 3113,-6197.5998 3113,-6197.5998 3119,-6197.5998 3125,-6203.5998 3125,-6209.5998 3125,-6209.5998 3125,-6221.5998 3125,-6221.5998 3125,-6227.5998 3119,-6233.5998 3113,-6233.5998"/>
+<text text-anchor="middle" x="2957" y="-6211.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus/internal</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/client_golang/prometheus/internal -->
+<g id="edge517" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/client_golang/prometheus/internal</title>
+<path fill="none" stroke="#000000" d="M2587.1945,-6920.3282C2614.9176,-6872.1129 2689.5462,-6736.7527 2727,-6614.5998 2774.3793,-6460.0758 2674.6842,-6373.7222 2785,-6255.5998 2792.283,-6247.8014 2800.765,-6241.3841 2809.9852,-6236.1165"/>
+<polygon fill="#000000" stroke="#000000" points="2811.0136,-6237.5493 2814.5688,-6233.622 2809.3405,-6234.4751 2811.0136,-6237.5493"/>
+</g>
+<!-- github.com/prometheus/client_model/go -->
+<g id="node124" class="node">
+<title>github.com/prometheus/client_model/go</title>
+<g id="a_node124"><a xlink:href="https://godoc.org/github.com/prometheus/client_model/go" xlink:title="github.com/prometheus/client_model/go" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3462,-6555.5998C3462,-6555.5998 3247,-6555.5998 3247,-6555.5998 3241,-6555.5998 3235,-6549.5998 3235,-6543.5998 3235,-6543.5998 3235,-6531.5998 3235,-6531.5998 3235,-6525.5998 3241,-6519.5998 3247,-6519.5998 3247,-6519.5998 3462,-6519.5998 3462,-6519.5998 3468,-6519.5998 3474,-6525.5998 3474,-6531.5998 3474,-6531.5998 3474,-6543.5998 3474,-6543.5998 3474,-6549.5998 3468,-6555.5998 3462,-6555.5998"/>
+<text text-anchor="middle" x="3354.5" y="-6533.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_model/go</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/client_model/go -->
+<g id="edge518" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/client_model/go</title>
+<path fill="none" stroke="#000000" d="M2714.5005,-6920.5731C2873.1938,-6899.2582 3113.9692,-6864.8774 3129,-6850.5998 3218.9351,-6765.1718 3100.396,-6666.4031 3187,-6577.5998 3198.9178,-6565.3794 3213.851,-6556.6203 3229.8618,-6550.3864"/>
+<polygon fill="#000000" stroke="#000000" points="3230.7836,-6551.9111 3234.8632,-6548.5318 3229.5666,-6548.6294 3230.7836,-6551.9111"/>
+</g>
+<!-- github.com/prometheus/common/expfmt -->
+<g id="node125" class="node">
+<title>github.com/prometheus/common/expfmt</title>
+<g id="a_node125"><a xlink:href="https://godoc.org/github.com/prometheus/common/expfmt" xlink:title="github.com/prometheus/common/expfmt" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3066,-6357.5998C3066,-6357.5998 2848,-6357.5998 2848,-6357.5998 2842,-6357.5998 2836,-6351.5998 2836,-6345.5998 2836,-6345.5998 2836,-6333.5998 2836,-6333.5998 2836,-6327.5998 2842,-6321.5998 2848,-6321.5998 2848,-6321.5998 3066,-6321.5998 3066,-6321.5998 3072,-6321.5998 3078,-6327.5998 3078,-6333.5998 3078,-6333.5998 3078,-6345.5998 3078,-6345.5998 3078,-6351.5998 3072,-6357.5998 3066,-6357.5998"/>
+<text text-anchor="middle" x="2957" y="-6335.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/expfmt</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/common/expfmt -->
+<g id="edge519" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/common/expfmt</title>
+<path fill="none" stroke="#000000" d="M2588.0954,-6920.3459C2643.5728,-6833.0108 2881.7327,-6458.0889 2942.726,-6362.0705"/>
+<polygon fill="#000000" stroke="#000000" points="2944.2913,-6362.8701 2945.4951,-6357.7113 2941.337,-6360.9934 2944.2913,-6362.8701"/>
+</g>
+<!-- github.com/prometheus/common/model -->
+<g id="node126" class="node">
+<title>github.com/prometheus/common/model</title>
+<g id="a_node126"><a xlink:href="https://godoc.org/github.com/prometheus/common/model" xlink:title="github.com/prometheus/common/model" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3888,-4944.5998C3888,-4944.5998 3675,-4944.5998 3675,-4944.5998 3669,-4944.5998 3663,-4938.5998 3663,-4932.5998 3663,-4932.5998 3663,-4920.5998 3663,-4920.5998 3663,-4914.5998 3669,-4908.5998 3675,-4908.5998 3675,-4908.5998 3888,-4908.5998 3888,-4908.5998 3894,-4908.5998 3900,-4914.5998 3900,-4920.5998 3900,-4920.5998 3900,-4932.5998 3900,-4932.5998 3900,-4938.5998 3894,-4944.5998 3888,-4944.5998"/>
+<text text-anchor="middle" x="3781.5" y="-4922.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/model</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/common/model -->
+<g id="edge520" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/common/model</title>
+<path fill="none" stroke="#000000" d="M2579.0455,-6920.3223C2594.3811,-6812.4436 2677.4271,-6259.2768 2785,-6131.5998 2893.0492,-6003.3576 3032.4567,-6113.7132 3129,-5976.5998 3222.4333,-5843.9032 3087.5214,-5376.8269 3187,-5248.5998 3283.4916,-5124.2229 3409.5189,-5243.7281 3522,-5133.5998 3575.4506,-5081.2673 3524.1962,-5025.4154 3580,-4975.5998 3601.7074,-4956.2218 3629.7932,-4944.1956 3658.066,-4936.8105"/>
+<polygon fill="#000000" stroke="#000000" points="3658.5522,-4938.4928 3662.9736,-4935.5748 3657.6976,-4935.0987 3658.5522,-4938.4928"/>
+</g>
+<!-- github.com/prometheus/procfs -->
+<g id="node127" class="node">
+<title>github.com/prometheus/procfs</title>
+<g id="a_node127"><a xlink:href="https://godoc.org/github.com/prometheus/procfs" xlink:title="github.com/prometheus/procfs" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3434.5,-13812.5998C3434.5,-13812.5998 3274.5,-13812.5998 3274.5,-13812.5998 3268.5,-13812.5998 3262.5,-13806.5998 3262.5,-13800.5998 3262.5,-13800.5998 3262.5,-13788.5998 3262.5,-13788.5998 3262.5,-13782.5998 3268.5,-13776.5998 3274.5,-13776.5998 3274.5,-13776.5998 3434.5,-13776.5998 3434.5,-13776.5998 3440.5,-13776.5998 3446.5,-13782.5998 3446.5,-13788.5998 3446.5,-13788.5998 3446.5,-13800.5998 3446.5,-13800.5998 3446.5,-13806.5998 3440.5,-13812.5998 3434.5,-13812.5998"/>
+<text text-anchor="middle" x="3354.5" y="-13790.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/procfs -->
+<g id="edge521" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/procfs</title>
+<path fill="none" stroke="#000000" d="M2578.5598,-6956.7942C2593.9698,-7093.8383 2691.6056,-7978.4425 2727,-8700.5998 2739.3835,-8953.2615 2696.2219,-13016.7249 2785,-13253.5998 2881.8235,-13511.9414 2963.4256,-13564.9531 3187,-13726.5998 3216.3325,-13747.8075 3253.2313,-13763.7079 3284.9259,-13774.7786"/>
+<polygon fill="#000000" stroke="#000000" points="3284.6686,-13776.5408 3289.966,-13776.514 3285.8081,-13773.2315 3284.6686,-13776.5408"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;bufio -->
+<g id="edge536" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M2196.4555,-6499.7338C2207.9614,-6718.8792 2320.3459,-8881.1795 2368,-10630.5998 2371.3972,-10755.314 2372.3384,-15008.9695 2426,-15121.5998 2608.2176,-15504.0559 2788.2302,-15552.5663 3187,-15695.5998 3520.3259,-15815.1595 3728.4885,-15976.8205 3983,-15730.5998 4122.9069,-15595.2504 3942.2989,-14147.3839 4041,-13979.5998 4046.3373,-13970.5268 4055.2324,-13963.6547 4064.33,-13958.5952"/>
+<polygon fill="#000000" stroke="#000000" points="4065.1803,-13960.1255 4068.7977,-13956.2554 4063.5564,-13957.0249 4065.1803,-13960.1255"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;compress/gzip -->
+<g id="edge537" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;compress/gzip</title>
+<path fill="none" stroke="#000000" d="M2197.4501,-6463.4768C2210.4483,-6346.0325 2288.2629,-5693.3825 2426,-5559.5998 2451.5901,-5534.7444 2491.3627,-5526.8301 2523.5958,-5524.9514"/>
+<polygon fill="#000000" stroke="#000000" points="2523.7931,-5526.6939 2528.701,-5524.7001 2523.6209,-5523.1982 2523.7931,-5526.6939"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;crypto/tls -->
+<g id="edge538" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;crypto/tls</title>
+<path fill="none" stroke="#000000" d="M2196.4801,-6499.7331C2208.2778,-6718.8708 2323.4023,-8881.099 2368,-10630.5998 2385.4342,-11314.5191 2368.3684,-16105.8901 2426,-16787.5998 2497.0613,-17628.1655 2630.7522,-17823.5445 2727,-18661.5998 2737.5985,-18753.8837 2734.6682,-19417.5271 2785,-19495.5998 2934.8485,-19728.0391 3776.1392,-20016.1515 3983,-19832.5998 4064.0263,-19760.7036 4004.4541,-19450.5739 4041,-19348.5998 4049.4335,-19325.0679 4065.2697,-19301.3602 4077.7914,-19284.8138"/>
+<polygon fill="#000000" stroke="#000000" points="4079.2419,-19285.7981 4080.8969,-19280.7658 4076.465,-19283.6677 4079.2419,-19285.7981"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;errors -->
+<g id="edge539" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2199.5965,-6463.5112C2220.9833,-6370.768 2322.7818,-5949.9851 2426,-5873.5998 2495.6139,-5822.083 3918.3669,-5739.9575 3983,-5797.5998 4057.3549,-5863.9123 4089.643,-6615.9268 4095.1377,-6759.126"/>
+<polygon fill="#000000" stroke="#000000" points="4093.3973,-6759.4155 4095.3364,-6764.3453 4096.8948,-6759.2823 4093.3973,-6759.4155"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;fmt -->
+<g id="edge540" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2197.8064,-6500.0055C2215.3774,-6632.6267 2339.6987,-7449.2125 2785,-7876.5998 2902.8055,-7989.6661 3010.3959,-7893.3716 3129,-8005.5998 3175.5453,-8049.643 3139.2023,-8094.919 3187,-8137.5998 3303.2854,-8241.4367 3385.6548,-8166.0062 3522,-8241.5998 3551.7654,-8258.1026 3549.3029,-8276.9026 3580,-8291.5998 3744.2504,-8370.2399 3847.3269,-8244.1285 3983,-8365.5998 4037.3033,-8414.2189 4079.7401,-8644.023 4092.2378,-8718.3489"/>
+<polygon fill="#000000" stroke="#000000" points="4090.536,-8718.7836 4093.0856,-8723.4271 4093.9882,-8718.2071 4090.536,-8718.7836"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;io -->
+<g id="edge544" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2196.3135,-6499.7384C2206.1328,-6718.9382 2302.6814,-8881.75 2368,-10630.5998 2371.6327,-10727.8631 2383.3628,-12294.1046 2426,-12381.5998 2502.4298,-12538.4404 2650.7091,-12476.6916 2727,-12633.5998 2816.9269,-12818.5532 2646.0851,-14331.9513 2785,-14483.5998 2965.3897,-14680.5249 3786.1167,-14759.0352 3983,-14578.5998 4092.1967,-14478.5256 3962.4787,-14040.1911 4041,-13914.5998 4046.4291,-13905.9161 4055.0559,-13899.2127 4063.882,-13894.1874"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2018,-13895.4663 4068.7869,-13891.5662 4063.5522,-13892.3794 4065.2018,-13895.4663"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;strconv -->
+<g id="edge548" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M2195.8046,-6463.151C2199.3021,-6319.2865 2242.8811,-5373.4422 2785,-5024.5998 3011.4458,-4878.8866 3772.9703,-4674.0841 3983,-4842.5998 4002.6381,-4858.3563 4073.5119,-5231.2868 4091.7147,-5328.5661"/>
+<polygon fill="#000000" stroke="#000000" points="4090.0016,-5328.9262 4092.6406,-5333.5195 4093.442,-5328.2831 4090.0016,-5328.9262"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;strings -->
+<g id="edge549" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2220.5213,-6463.4097C2298.8458,-6407.74 2548.2325,-6240.0434 2785,-6183.5998 2933.7213,-6148.1458 2977.4539,-6163.3815 3129,-6183.5998 3226.4644,-6196.6029 3917.8125,-6341.9861 3983,-6415.5998 4042.5461,-6482.8428 4025.3214,-6727.1604 4041,-6815.5998 4056.7412,-6904.3922 4080.2146,-7009.0892 4090.6578,-7054.5791"/>
+<polygon fill="#000000" stroke="#000000" points="4088.9795,-7055.0901 4091.8059,-7059.5705 4092.3904,-7054.3054 4088.9795,-7055.0901"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;sync -->
+<g id="edge550" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M2199.0673,-6463.4163C2220.1712,-6354.889 2329.0729,-5781.3665 2368,-5307.5998 2375.3007,-5218.7458 2362.3919,-2161.0686 2426,-2098.5998 2487.7149,-2037.9903 3919.2782,-2040.1038 3983,-2098.5998 4038.6168,-2149.6555 4032.7924,-2700.5495 4041,-2775.5998 4057.2916,-2924.571 4083.2357,-3102.9113 4092.5392,-3165.5186"/>
+<polygon fill="#000000" stroke="#000000" points="4090.8166,-3165.8333 4093.2838,-3170.5212 4094.2785,-3165.318 4090.8166,-3165.8333"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;time -->
+<g id="edge551" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2196.8675,-6499.7749C2212.6947,-6709.3878 2361.7571,-8663.4365 2426,-8765.5998 2509.4651,-8898.3314 2647.1225,-8814.6788 2727,-8949.5998 2866.8554,-9185.8294 2632.8056,-9955.1248 2785,-10183.5998 2878.7942,-10324.4041 3011.4204,-10224.9517 3129,-10346.5998 3174.1008,-10393.2612 3136.9441,-10439.299 3187,-10480.5998 3304.6121,-10577.6409 3415.9449,-10445.0466 3522,-10554.5998 3619.4932,-10655.3087 3480.7196,-10765.6523 3580,-10864.5998 3708.9842,-10993.1515 3857.0128,-10807.1095 3983,-10938.5998 4111.8454,-11073.0733 3937.1751,-11198.9885 4041,-11353.5998 4046.6121,-11361.9571 4055.1976,-11368.4193 4063.9287,-11373.2766"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5347,-11375.0453 4068.7764,-11375.8118 4065.1567,-11371.9439 4063.5347,-11375.0453"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net/http -->
+<g id="edge546" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M2196.2582,-6463.4958C2209.4062,-6149.9091 2389.5871,-1866.1787 2426,-1744.5998 2516.0354,-1443.9811 2614.1036,-1399.7964 2785,-1136.5998 2805.9791,-1104.2901 3155.9809,-594.444 3187,-571.5998 3475.5797,-359.0732 3659.1005,-289.1888 3983,-442.5998 4033.4129,-466.4773 4068.8301,-525.0826 4085.3934,-557.7184"/>
+<polygon fill="#000000" stroke="#000000" points="4083.9006,-558.6463 4087.6999,-562.3379 4087.032,-557.0827 4083.9006,-558.6463"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net -->
+<g id="edge545" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M2196.4792,-6499.7331C2208.2671,-6718.8711 2323.2994,-8881.1016 2368,-10630.5998 2370.1574,-10715.0344 2377.0677,-16642.7559 2426,-16711.5998 2508.3407,-16827.4467 2618.782,-16727.462 2727,-16819.5998 2767.593,-16854.1611 2744.8322,-16889.5453 2785,-16924.5998 2908.5015,-17032.3797 2988.0035,-16973.9988 3129,-17057.5998 3158.0268,-17074.8107 3156.7936,-17091.5552 3187,-17106.5998 3347.3068,-17186.4423 3402.0196,-17175.6988 3580,-17195.5998 3758.0018,-17215.5032 3804.7908,-17213.5521 3983,-17195.5998 4010.5932,-17192.8202 4041.4107,-17186.3144 4063.8839,-17180.9288"/>
+<polygon fill="#000000" stroke="#000000" points="4064.388,-17182.6073 4068.8347,-17179.7282 4063.563,-17179.2059 4064.388,-17182.6073"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/client_golang/prometheus -->
+<g id="edge541" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/client_golang/prometheus</title>
+<path fill="none" stroke="#000000" d="M2210.6708,-6499.7968C2270.7688,-6571.8828 2492.0631,-6837.3198 2557.9029,-6916.293"/>
+<polygon fill="#000000" stroke="#000000" points="2556.74,-6917.6311 2561.2859,-6920.3509 2559.4283,-6915.3898 2556.74,-6917.6311"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/client_model/go -->
+<g id="edge542" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/client_model/go</title>
+<path fill="none" stroke="#000000" d="M2368.0751,-6489.9382C2604.0746,-6501.3411 3021.8591,-6521.5274 3229.7624,-6531.5728"/>
+<polygon fill="#000000" stroke="#000000" points="3229.86,-6533.3295 3234.9387,-6531.8229 3230.029,-6529.8336 3229.86,-6533.3295"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/common/expfmt -->
+<g id="edge543" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/common/expfmt</title>
+<path fill="none" stroke="#000000" d="M2292.3309,-6463.5434C2436.8317,-6436.5977 2707.6432,-6386.0984 2855.0583,-6358.6093"/>
+<polygon fill="#000000" stroke="#000000" points="2855.6554,-6360.2782 2860.2499,-6357.6412 2855.0138,-6356.8375 2855.6554,-6360.2782"/>
+</g>
+<!-- net/http/httptrace -->
+<g id="node128" class="node">
+<title>net/http/httptrace</title>
+<g id="a_node128"><a xlink:href="https://godoc.org/net/http/httptrace" xlink:title="net/http/httptrace" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2619,-5609.5998C2619,-5609.5998 2534,-5609.5998 2534,-5609.5998 2528,-5609.5998 2522,-5603.5998 2522,-5597.5998 2522,-5597.5998 2522,-5585.5998 2522,-5585.5998 2522,-5579.5998 2528,-5573.5998 2534,-5573.5998 2534,-5573.5998 2619,-5573.5998 2619,-5573.5998 2625,-5573.5998 2631,-5579.5998 2631,-5585.5998 2631,-5585.5998 2631,-5597.5998 2631,-5597.5998 2631,-5603.5998 2625,-5609.5998 2619,-5609.5998"/>
+<text text-anchor="middle" x="2576.5" y="-5587.8998" font-family="Times,serif" font-size="14.00" fill="#000000">net/http/httptrace</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net/http/httptrace -->
+<g id="edge547" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net/http/httptrace</title>
+<path fill="none" stroke="#000000" d="M2197.9321,-6463.5273C2213.3064,-6351.8666 2300.4057,-5756.5636 2426,-5631.5998 2449.7333,-5607.9857 2485.7222,-5597.8575 2516.5562,-5593.6938"/>
+<polygon fill="#000000" stroke="#000000" points="2517.1091,-5595.3882 2521.8524,-5593.0296 2516.6735,-5591.9154 2517.1091,-5595.3882"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;bytes -->
+<g id="edge673" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3838.0368,-4386.4208C3885.7053,-4397.1578 3951.1052,-4420.3852 3983,-4469.5998 4040.9974,-4559.0917 4038.7146,-12040.9824 4041,-12147.5998 4054.4058,-12773.0144 4088.4789,-13539.1817 4094.9352,-13681.3376"/>
+<polygon fill="#000000" stroke="#000000" points="4093.195,-13681.5949 4095.1705,-13686.5102 4096.6914,-13681.4358 4093.195,-13681.5949"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;encoding -->
+<g id="edge674" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;encoding</title>
+<path fill="none" stroke="#000000" d="M3838.0541,-4374.4155C3882.5228,-4374.986 3943.3796,-4382.9928 3983,-4417.5998 4030.9875,-4459.5152 4076.6367,-4655.8426 4091.2016,-4723.5646"/>
+<polygon fill="#000000" stroke="#000000" points="4089.5194,-4724.067 4092.2761,-4728.5906 4092.942,-4723.3352 4089.5194,-4724.067"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;encoding/base64 -->
+<g id="edge675" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;encoding/base64</title>
+<path fill="none" stroke="#000000" d="M3838.0716,-4386.3983C3885.7627,-4397.1207 3951.1761,-4420.3393 3983,-4469.5998 4076.5391,-4614.3901 3954.7124,-16731.3744 4041,-16880.5998 4043.2655,-16884.5179 4046.2146,-16888.0014 4049.5703,-16891.0892"/>
+<polygon fill="#000000" stroke="#000000" points="4048.7239,-16892.6632 4053.6798,-16894.5345 4050.9725,-16889.9811 4048.7239,-16892.6632"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;errors -->
+<g id="edge676" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3838.0086,-4386.8567C3885.3171,-4397.8517 3950.2046,-4421.2128 3983,-4469.5998 3999.9771,-4494.6482 4085.0291,-6519.8657 4095.0377,-6759.5137"/>
+<polygon fill="#000000" stroke="#000000" points="4093.2898,-6759.6025 4095.2469,-6764.5251 4096.7867,-6759.4564 4093.2898,-6759.6025"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;fmt -->
+<g id="edge677" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3838.3991,-4386.5966C3885.958,-4397.425 3950.9933,-4420.6875 3983,-4469.5998 4036.8197,-4551.8467 4036.5203,-7909.411 4041,-8007.5998 4053.7912,-8287.9621 4085.1155,-8627.5043 4093.759,-8718.3349"/>
+<polygon fill="#000000" stroke="#000000" points="4092.0314,-8718.6533 4094.2482,-8723.4645 4095.5156,-8718.3209 4092.0314,-8718.6533"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;io -->
+<g id="edge678" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3838.0524,-4386.4107C3885.7311,-4397.1411 3951.1371,-4420.3645 3983,-4469.5998 4052.9372,-4577.6685 4017.2518,-13611.0847 4041,-13737.5998 4049.4009,-13782.3545 4070.9326,-13831.2325 4084.4282,-13858.9803"/>
+<polygon fill="#000000" stroke="#000000" points="4082.9114,-13859.8618 4086.6857,-13863.579 4086.0533,-13858.3195 4082.9114,-13859.8618"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;math -->
+<g id="edge679" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3792.1442,-4359.5267C3839.4219,-4279.2523 4030.3615,-3955.0497 4082.6846,-3866.2085"/>
+<polygon fill="#000000" stroke="#000000" points="4084.3289,-3866.8649 4085.3585,-3861.6684 4081.3131,-3865.0887 4084.3289,-3866.8649"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;reflect -->
+<g id="edge680" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M3838.3535,-4372.6666C3883.9161,-4372.0606 3946.1268,-4379.3329 3983,-4417.5998 4039.53,-4476.2666 4025.4477,-4699.6276 4041,-4779.5998 4056.5652,-4859.6382 4079.4698,-4953.6455 4090.1352,-4996.3698"/>
+<polygon fill="#000000" stroke="#000000" points="4088.4627,-4996.8956 4091.3738,-5001.3214 4091.8581,-4996.0462 4088.4627,-4996.8956"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;sort -->
+<g id="edge682" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3838.0231,-4388.479C3878.4512,-4396.3436 3934.133,-4407.3451 3983,-4417.5998 4010.2394,-4423.316 4041.086,-4430.166 4063.6604,-4435.249"/>
+<polygon fill="#000000" stroke="#000000" points="4063.3728,-4436.978 4068.6353,-4436.3708 4064.1427,-4433.5638 4063.3728,-4436.978"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;strconv -->
+<g id="edge683" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3838.1208,-4372.0443C3884.1786,-4370.9411 3947.2061,-4377.8403 3983,-4417.5998 4077.8065,-4522.9098 4021.7182,-4912.2193 4041,-5052.5998 4055.2346,-5156.2345 4080.3941,-5278.6157 4091.0308,-5328.6006"/>
+<polygon fill="#000000" stroke="#000000" points="4089.3192,-5328.966 4092.0742,-5333.4907 4092.7422,-5328.2356 4089.3192,-5328.966"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;strings -->
+<g id="edge684" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3838.2984,-4386.6628C3885.7927,-4397.5336 3950.79,-4420.8211 3983,-4469.5998 4054.84,-4578.394 4025.9341,-6686.1001 4041,-6815.5998 4051.395,-6904.9503 4077.5084,-7008.9495 4089.632,-7054.3784"/>
+<polygon fill="#000000" stroke="#000000" points="4087.9841,-7054.9897 4090.9697,-7059.3656 4091.3646,-7054.0829 4087.9841,-7054.9897"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;sync -->
+<g id="edge685" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3786.6154,-4359.3273C3809.8315,-4276.2925 3907.4198,-3926.1204 3983,-3637.5998 4025.583,-3475.0429 4073.9735,-3278.5582 4090.2895,-3211.9607"/>
+<polygon fill="#000000" stroke="#000000" points="4092.0091,-3212.2958 4091.4987,-3207.023 4088.6095,-3211.4632 4092.0091,-3212.2958"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;time -->
+<g id="edge686" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3838.0263,-4386.4276C3885.688,-4397.1691 3951.0838,-4420.3991 3983,-4469.5998 4087.0702,-4630.0303 3944.9651,-11188.2341 4041,-11353.5998 4046.2922,-11362.7126 4055.2769,-11369.4772 4064.4675,-11374.3783"/>
+<polygon fill="#000000" stroke="#000000" points="4063.726,-11375.9641 4068.9806,-11376.6368 4065.2924,-11372.8341 4063.726,-11375.9641"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;unicode -->
+<g id="edge687" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;unicode</title>
+<path fill="none" stroke="#000000" d="M3805.1386,-4359.5608C3862.0565,-4316.1259 4006.5311,-4205.875 4067.8204,-4159.1042"/>
+<polygon fill="#000000" stroke="#000000" points="4069.1739,-4160.2727 4072.0871,-4155.8482 4067.0506,-4157.4903 4069.1739,-4160.2727"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;unicode/utf8 -->
+<g id="edge688" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M3838.0895,-4377.5998C3898.1732,-4377.5998 3992.0811,-4377.5998 4048.4679,-4377.5998"/>
+<polygon fill="#000000" stroke="#000000" points="4048.4689,-4379.3499 4053.4689,-4377.5998 4048.4689,-4375.8499 4048.4689,-4379.3499"/>
+</g>
+<!-- gopkg.in/yaml.v2&#45;&gt;regexp -->
+<g id="edge681" class="edge">
+<title>gopkg.in/yaml.v2&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M3787.8733,-4359.5957C3816.5404,-4277.7237 3934.2553,-3931.8454 3983,-3637.5998 4050.4658,-3230.3445 4000.8401,-3121.4473 4041,-2710.5998 4055.5791,-2561.4513 4082.5825,-2383.2207 4092.3523,-2320.6616"/>
+<polygon fill="#000000" stroke="#000000" points="4094.0904,-2320.8736 4093.1347,-2315.663 4090.6325,-2320.3322 4094.0904,-2320.8736"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;bufio -->
+<g id="edge394" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3786.6116,-6620.6949C3813.7264,-6717.7138 3941.069,-7186.5285 3983,-7579.5998 4001.6668,-7754.5866 3952.5678,-13762.4533 4041,-13914.5998 4046.2955,-13923.7106 4055.2811,-13930.4748 4064.4715,-13935.376"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7298,-13936.9617 4068.9843,-13937.6346 4065.2963,-13933.8318 4063.7298,-13936.9617"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;bytes -->
+<g id="edge395" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3786.5871,-6620.6975C3813.5755,-6717.73 3940.3731,-7186.6033 3983,-7579.5998 4037.7361,-8084.2364 4028.8173,-11640.1496 4041,-12147.5998 4056.0138,-12772.9778 4088.7895,-13539.1746 4094.9802,-13681.3365"/>
+<polygon fill="#000000" stroke="#000000" points="4093.2395,-13681.5904 4095.2057,-13686.5094 4096.7362,-13681.4378 4093.2395,-13681.5904"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;encoding -->
+<g id="edge396" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;encoding</title>
+<path fill="none" stroke="#000000" d="M3787.8478,-6584.5915C3816.4042,-6502.701 3933.7106,-6156.7547 3983,-5862.5998 4047.4083,-5478.2164 3984.5768,-5373.2363 4041,-4987.5998 4052.851,-4906.6016 4077.5763,-4812.5096 4089.4112,-4769.8046"/>
+<polygon fill="#000000" stroke="#000000" points="4091.1338,-4770.1419 4090.7888,-4764.8557 4087.762,-4769.2032 4091.1338,-4770.1419"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;encoding/json -->
+<g id="edge397" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3786.6333,-6620.6926C3813.8598,-6717.6996 3941.6842,-7186.4633 3983,-7579.5998 3996.9736,-7712.5644 3973.9964,-17089.9046 4041,-17205.5998 4043.2682,-17209.5163 4046.219,-17212.9988 4049.5759,-17216.0859"/>
+<polygon fill="#000000" stroke="#000000" points="4048.7302,-17217.6604 4053.6865,-17219.5306 4050.9782,-17214.9778 4048.7302,-17217.6604"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;errors -->
+<g id="edge398" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3882.5842,-6608.7929C3916.3652,-6614.3628 3953.0143,-6624.4687 3983,-6642.5998 4030.6398,-6671.4057 4066.7181,-6727.9633 4084.2413,-6759.6665"/>
+<polygon fill="#000000" stroke="#000000" points="4082.7603,-6760.6061 4086.6912,-6764.1574 4085.8329,-6758.9299 4082.7603,-6760.6061"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;fmt -->
+<g id="edge399" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3786.005,-6620.7731C3809.9959,-6718.1947 3923.8618,-7188.747 3983,-7579.5998 4051.9446,-8035.2644 4087.5593,-8598.1453 4094.6631,-8718.3474"/>
+<polygon fill="#000000" stroke="#000000" points="4092.9204,-8718.5243 4094.961,-8723.4129 4096.4143,-8718.3187 4092.9204,-8718.5243"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;io -->
+<g id="edge400" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3786.6098,-6620.6951C3813.7152,-6717.715 3941.0172,-7186.534 3983,-7579.5998 4055.6706,-8259.9825 3913.7007,-13065.293 4041,-13737.5998 4049.4716,-13782.3411 4070.98,-13831.2235 4084.4521,-13858.9758"/>
+<polygon fill="#000000" stroke="#000000" points="4082.9341,-13859.8551 4086.7054,-13863.5753 4086.0771,-13858.3152 4082.9341,-13859.8551"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;math -->
+<g id="edge402" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3788.1762,-6584.3533C3817.8962,-6502.1245 3938.6483,-6157.2029 3983,-5862.5998 4099.3142,-5089.9919 3947.499,-4881.2992 4041,-4105.5998 4051.7648,-4016.2931 4077.6889,-3912.2715 4089.6981,-3866.8291"/>
+<polygon fill="#000000" stroke="#000000" points="4091.4309,-3867.122 4091.0229,-3861.8403 4088.0481,-3866.2236 4091.4309,-3867.122"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;reflect -->
+<g id="edge403" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M3787.571,-6584.5422C3814.9279,-6502.4384 3927.8053,-6155.704 3983,-5862.5998 4027.9148,-5624.0858 4006.2034,-5559.7986 4041,-5319.5998 4056.0461,-5215.7379 4080.7606,-5092.8819 4091.1549,-5042.6942"/>
+<polygon fill="#000000" stroke="#000000" points="4092.8713,-5043.0356 4092.1742,-5037.7842 4089.4443,-5042.3241 4092.8713,-5043.0356"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;sort -->
+<g id="edge404" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3788.0537,-6584.3343C3817.2478,-6502.024 3936.075,-6156.804 3983,-5862.5998 4063.4661,-5358.1036 3977.3082,-5221.487 4041,-4714.5998 4052.7065,-4621.4344 4078.4668,-4512.4725 4090.0967,-4465.7662"/>
+<polygon fill="#000000" stroke="#000000" points="4091.7952,-4466.1876 4091.3104,-4460.9125 4088.3997,-4465.3386 4091.7952,-4466.1876"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;strconv -->
+<g id="edge405" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3882.6715,-6606.3204C3919.329,-6602.1884 3957.8573,-6590.4732 3983,-6562.5998 4065.6418,-6470.9825 4091.5731,-5537.2023 4095.4595,-5375.4217"/>
+<polygon fill="#000000" stroke="#000000" points="4097.2191,-5375.0352 4095.5885,-5369.995 4093.7201,-5374.9519 4097.2191,-5375.0352"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;strings -->
+<g id="edge406" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3882.7583,-6601.3712C3918.4419,-6605.9919 3956.2796,-6617.3907 3983,-6642.5998 4014.027,-6671.8719 4074.4969,-6968.5271 4091.472,-7054.4176"/>
+<polygon fill="#000000" stroke="#000000" points="4089.796,-7054.9641 4092.4804,-7059.531 4093.2298,-7054.2869 4089.796,-7054.9641"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;sync -->
+<g id="edge407" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3788.2093,-6584.3582C3818.0717,-6502.1508 3939.3447,-6157.3069 3983,-5862.5998 4049.8128,-5411.5615 4014.495,-4266.7888 4041,-3811.5998 4054.6648,-3576.9245 4084.5527,-3293.6101 4093.4514,-3211.7615"/>
+<polygon fill="#000000" stroke="#000000" points="4095.2048,-3211.8244 4094.0068,-3206.6643 4091.7254,-3211.4453 4095.2048,-3211.8244"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;unicode/utf8 -->
+<g id="edge409" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M3788.1276,-6584.3459C3817.6392,-6502.0854 3937.6285,-6157.0476 3983,-5862.5998 4007.5895,-5703.0212 3958.6592,-4549.4881 4041,-4410.5998 4043.598,-4406.2176 4047.024,-4402.3477 4050.8991,-4398.947"/>
+<polygon fill="#000000" stroke="#000000" points="4052.1241,-4400.207 4054.9098,-4395.7011 4049.9223,-4397.4863 4052.1241,-4400.207"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;unsafe -->
+<g id="edge410" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;unsafe</title>
+<path fill="none" stroke="#000000" d="M3786.6123,-6620.6948C3813.7303,-6717.7133 3941.0872,-7186.5265 3983,-7579.5998 4001.8501,-7756.3828 3951.6677,-13825.8882 4041,-13979.5998 4046.2951,-13988.7109 4055.2805,-13995.4751 4064.471,-14000.3763"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7293,-14001.962 4068.9838,-14002.6349 4065.2958,-13998.8321 4063.7293,-14001.962"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;log -->
+<g id="edge401" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;log</title>
+<path fill="none" stroke="#000000" d="M3786.5461,-6620.702C3813.3233,-6717.7577 3939.2098,-7186.7313 3983,-7579.5998 4059.6954,-8267.6809 4004.236,-10003.2344 4041,-10694.5998 4053.5422,-10930.4627 4084.2063,-11215.1016 4093.3712,-11297.3307"/>
+<polygon fill="#000000" stroke="#000000" points="4091.6489,-11297.6769 4093.9434,-11302.4516 4095.1273,-11297.2882 4091.6489,-11297.6769"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;sync/atomic -->
+<g id="edge408" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;sync/atomic</title>
+<path fill="none" stroke="#000000" d="M3785.6496,-6620.7022C3819.6562,-6769.0553 4052.1659,-7783.3745 4090.619,-7951.1254"/>
+<polygon fill="#000000" stroke="#000000" points="4088.9796,-7951.8059 4091.8025,-7956.2885 4092.3911,-7951.0239 4088.9796,-7951.8059"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;bufio -->
+<g id="edge421" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3890.0097,-10465.6524C3924.7177,-10469.0978 3960.0516,-10480.0485 3983,-10506.5998 4106.8249,-10649.8649 3945.2007,-13751.2597 4041,-13914.5998 4046.2832,-13923.6078 4055.1642,-13930.3266 4064.2654,-13935.2184"/>
+<polygon fill="#000000" stroke="#000000" points="4063.484,-13936.7843 4068.7362,-13937.4751 4065.0612,-13933.6597 4063.484,-13936.7843"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;bytes -->
+<g id="edge422" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3890.2622,-10465.9437C3924.778,-10469.4673 3959.9267,-10480.3927 3983,-10506.5998 3983,-10506.5998 4084.8753,-13389.7624 4095.1646,-13680.9572"/>
+<polygon fill="#000000" stroke="#000000" points="4093.425,-13681.284 4095.3505,-13686.2191 4096.9228,-13681.1604 4093.425,-13681.284"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;encoding/binary -->
+<g id="edge423" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M3890.1514,-10466.0416C3924.6579,-10469.5734 3959.8291,-10480.4789 3983,-10506.5998 4033.0001,-10562.9657 4031.3506,-11786.8735 4041,-11861.5998 4052.0858,-11947.45 4077.6169,-12047.308 4089.5868,-12091.5168"/>
+<polygon fill="#000000" stroke="#000000" points="4087.9075,-12092.0109 4090.9087,-12096.3762 4091.2848,-12091.0922 4087.9075,-12092.0109"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;fmt -->
+<g id="edge424" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3890.1361,-10476.7791C3924.6413,-10471.998 3959.8156,-10459.9934 3983,-10433.5998 4040.7474,-10367.8592 4088.7051,-8964.5942 4095.2413,-8765.1095"/>
+<polygon fill="#000000" stroke="#000000" points="4096.9997,-8764.8787 4095.414,-8759.8242 4093.5016,-8764.7643 4096.9997,-8764.8787"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;io -->
+<g id="edge425" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3890.2587,-10465.6907C3924.8809,-10469.1663 3960.0972,-10480.1263 3983,-10506.5998 4041.7295,-10574.4857 4023.924,-13649.4745 4041,-13737.5998 4049.6624,-13782.3046 4071.1079,-13831.199 4084.5164,-13858.9635"/>
+<polygon fill="#000000" stroke="#000000" points="4082.9953,-13859.837 4086.7588,-13863.5651 4086.1416,-13858.3037 4082.9953,-13859.837"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;math -->
+<g id="edge426" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3786.9664,-10455.4263C3814.797,-10361.8625 3941.0589,-9924.6343 3983,-9556.5998 4120.1633,-8352.9883 3909.4841,-5309.8415 4041,-4105.5998 4050.7658,-4016.1784 4077.2014,-3912.2155 4089.5197,-3866.8086"/>
+<polygon fill="#000000" stroke="#000000" points="4091.2518,-3867.1081 4090.8792,-3861.8238 4087.8752,-3866.1871 4091.2518,-3867.1081"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;sort -->
+<g id="edge428" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3786.9571,-10455.4253C3814.7417,-10361.8562 3940.8127,-9924.6062 3983,-9556.5998 4105.5562,-8487.5245 3927.4202,-5784.6661 4041,-4714.5998 4050.9109,-4621.2263 4077.6097,-4512.3731 4089.7899,-4465.7307"/>
+<polygon fill="#000000" stroke="#000000" points="4091.4853,-4466.1641 4091.062,-4460.8837 4088.0999,-4465.2756 4091.4853,-4466.1641"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;strconv -->
+<g id="edge429" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3786.8966,-10455.4182C3814.3827,-10361.8143 3939.2142,-9924.4194 3983,-9556.5998 4056.7243,-8937.2838 4022.3044,-7374.0083 4041,-6750.5998 4057.7448,-6192.2418 4088.7494,-5508.9365 4094.917,-5374.9915"/>
+<polygon fill="#000000" stroke="#000000" points="4096.6769,-5374.8153 4095.159,-5369.74 4093.1806,-5374.6541 4096.6769,-5374.8153"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;sync -->
+<g id="edge430" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3786.9702,-10455.4267C3814.8195,-10361.8651 3941.1591,-9924.6457 3983,-9556.5998 4055.1075,-8922.3195 4009.6849,-4449.1971 4041,-3811.5998 4052.5315,-3576.81 4083.8944,-3293.5748 4093.299,-3211.7533"/>
+<polygon fill="#000000" stroke="#000000" points="4095.0522,-3211.8253 4093.8863,-3206.6578 4091.5752,-3211.4245 4095.0522,-3211.8253"/>
+</g>
+<!-- github.com/klauspost/compress/flate&#45;&gt;math/bits -->
+<g id="edge427" class="edge">
+<title>github.com/klauspost/compress/flate&#45;&gt;math/bits</title>
+<path fill="none" stroke="#000000" d="M3890.2171,-10475.9489C3921.9394,-10480.5891 3955.4504,-10489.6487 3983,-10506.5998 4034.7687,-10538.4529 4070.1599,-10603.6779 4086.2111,-10638.4953"/>
+<polygon fill="#000000" stroke="#000000" points="4084.7788,-10639.5741 4088.4386,-10643.4041 4087.966,-10638.1278 4084.7788,-10639.5741"/>
+</g>
+<!-- github.com/klauspost/compress/fse -->
+<g id="node114" class="node">
+<title>github.com/klauspost/compress/fse</title>
+<g id="a_node114"><a xlink:href="https://godoc.org/github.com/klauspost/compress/fse" xlink:title="github.com/klauspost/compress/fse" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3874,-10556.5998C3874,-10556.5998 3689,-10556.5998 3689,-10556.5998 3683,-10556.5998 3677,-10550.5998 3677,-10544.5998 3677,-10544.5998 3677,-10532.5998 3677,-10532.5998 3677,-10526.5998 3683,-10520.5998 3689,-10520.5998 3689,-10520.5998 3874,-10520.5998 3874,-10520.5998 3880,-10520.5998 3886,-10526.5998 3886,-10532.5998 3886,-10532.5998 3886,-10544.5998 3886,-10544.5998 3886,-10550.5998 3880,-10556.5998 3874,-10556.5998"/>
+<text text-anchor="middle" x="3781.5" y="-10534.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/fse</text>
+</a>
+</g>
+</g>
+<!-- github.com/klauspost/compress/fse&#45;&gt;errors -->
+<g id="edge431" class="edge">
+<title>github.com/klauspost/compress/fse&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3886.0213,-10547.4712C3921.97,-10544.6569 3959.1379,-10534.0808 3983,-10506.5998 4046.0413,-10433.9977 4030.3055,-7141.1556 4041,-7045.5998 4051.0416,-6955.878 4077.336,-6851.4973 4089.5689,-6805.9045"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3064,-6806.1825 4090.9189,-6800.8992 4087.9272,-6805.271 4091.3064,-6806.1825"/>
+</g>
+<!-- github.com/klauspost/compress/fse&#45;&gt;fmt -->
+<g id="edge432" class="edge">
+<title>github.com/klauspost/compress/fse&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3886.1173,-10546.7935C3921.7456,-10543.8203 3958.6752,-10533.3139 3983,-10506.5998 4044.5692,-10438.983 4089.4154,-8968.3422 4095.3344,-8764.872"/>
+<polygon fill="#000000" stroke="#000000" points="4097.0869,-8764.8089 4095.4827,-8759.7602 4093.5884,-8764.7074 4097.0869,-8764.8089"/>
+</g>
+<!-- github.com/klauspost/compress/fse&#45;&gt;io -->
+<g id="edge433" class="edge">
+<title>github.com/klauspost/compress/fse&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3824.6922,-10556.6894C3873.2033,-10579.6303 3949.5195,-10624.2337 3983,-10689.5998 4060.2095,-10840.3408 4008.6922,-13571.3459 4041,-13737.5998 4049.6865,-13782.2999 4071.1241,-13831.1959 4084.5245,-13858.9619"/>
+<polygon fill="#000000" stroke="#000000" points="4083.003,-13859.8346 4086.7655,-13863.5638 4086.1497,-13858.3022 4083.003,-13859.8346"/>
+</g>
+<!-- github.com/klauspost/compress/fse&#45;&gt;math/bits -->
+<g id="edge434" class="edge">
+<title>github.com/klauspost/compress/fse&#45;&gt;math/bits</title>
+<path fill="none" stroke="#000000" d="M3886.141,-10550.4394C3918.1251,-10556.3971 3952.8065,-10565.3063 3983,-10578.5998 4017.3949,-10593.7431 4051.4033,-10620.8093 4072.9815,-10639.9094"/>
+<polygon fill="#000000" stroke="#000000" points="4071.9689,-10641.3515 4076.8632,-10643.3785 4074.3012,-10638.7418 4071.9689,-10641.3515"/>
+</g>
+<!-- github.com/klauspost/compress/huff0&#45;&gt;errors -->
+<g id="edge435" class="edge">
+<title>github.com/klauspost/compress/huff0&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3373.9304,-10018.4444C3409.8041,-9983.6646 3486.0456,-9903.6043 3522,-9819.5998 3589.2239,-9662.5365 3468.5408,-9569.0787 3580,-9439.5998 3702.3441,-9297.4762 3869.8394,-9464.1388 3983,-9314.5998 4059.0904,-9214.0482 4026.4237,-7170.8512 4041,-7045.5998 4051.4363,-6955.9231 4077.5286,-6851.5193 4089.6394,-6805.9125"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3771,-6806.1879 4090.9756,-6800.9057 4087.9954,-6805.2854 4091.3771,-6806.1879"/>
+</g>
+<!-- github.com/klauspost/compress/huff0&#45;&gt;fmt -->
+<g id="edge436" class="edge">
+<title>github.com/klauspost/compress/huff0&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3465.572,-10025.5116C3486.0061,-10019.3975 3505.9288,-10010.2375 3522,-9996.5998 3572.7275,-9953.5535 3536.8448,-9909.2348 3580,-9858.5998 3716.8732,-9698.0036 3868.8202,-9785.0496 3983,-9607.5998 4076.5748,-9462.1728 4093.1086,-8888.3327 4095.6005,-8765.0196"/>
+<polygon fill="#000000" stroke="#000000" points="4097.354,-8764.855 4095.7026,-8759.8216 4093.8547,-8764.7863 4097.354,-8764.855"/>
+</g>
+<!-- github.com/klauspost/compress/huff0&#45;&gt;io -->
+<g id="edge438" class="edge">
+<title>github.com/klauspost/compress/huff0&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3418.8456,-10054.6117C3455.6649,-10068.5035 3498.9864,-10091.7302 3522,-10128.5998 3665.7165,-10358.8451 3471.2764,-11099.9102 3580,-11348.5998 3682.3518,-11582.7148 3882.7836,-11523.5628 3983,-11758.5998 4069.2819,-11960.9562 3997.9275,-13521.8745 4041,-13737.5998 4049.916,-13782.2547 4071.278,-13831.1656 4084.602,-13858.9467"/>
+<polygon fill="#000000" stroke="#000000" points="4083.0767,-13859.8124 4086.8297,-13863.5511 4086.2273,-13858.288 4083.0767,-13859.8124"/>
+</g>
+<!-- github.com/klauspost/compress/huff0&#45;&gt;math -->
+<g id="edge439" class="edge">
+<title>github.com/klauspost/compress/huff0&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3356.0957,-10018.572C3370.5411,-9854.5284 3479.1728,-8602.9994 3522,-7585.5998 3526.26,-7484.3988 3514.64,-4016.981 3580,-3939.5998 3703.3478,-3793.5655 3971.0451,-3822.4055 4063.6806,-3837.578"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5249,-3839.3262 4068.7447,-3838.4226 4064.1007,-3835.8738 4063.5249,-3839.3262"/>
+</g>
+<!-- github.com/klauspost/compress/huff0&#45;&gt;sync -->
+<g id="edge442" class="edge">
+<title>github.com/klauspost/compress/huff0&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3356.1099,-10018.5726C3370.6811,-9854.5342 3480.2115,-8603.0426 3522,-7585.5998 3526.7688,-7469.4921 3509.4708,-3494.9544 3580,-3402.5998 3693.1226,-3254.4713 3819.1966,-3375.5224 3983,-3286.5998 4019.8915,-3266.5728 4055.2484,-3232.7634 4076.2995,-3210.5621"/>
+<polygon fill="#000000" stroke="#000000" points="4077.6576,-3211.6723 4079.8068,-3206.8305 4075.1073,-3209.2753 4077.6576,-3211.6723"/>
+</g>
+<!-- github.com/klauspost/compress/huff0&#45;&gt;math/bits -->
+<g id="edge440" class="edge">
+<title>github.com/klauspost/compress/huff0&#45;&gt;math/bits</title>
+<path fill="none" stroke="#000000" d="M3465.8199,-10043.4291C3487.1078,-10049.7429 3507.3037,-10060.0621 3522,-10076.5998 3669.1381,-10242.1742 3426.991,-10411.435 3580,-10571.5998 3642.6864,-10637.2179 3893.8134,-10620.8351 3983,-10637.5998 4007.6342,-10642.2304 4035.1482,-10648.0599 4056.8587,-10652.814"/>
+<polygon fill="#000000" stroke="#000000" points="4056.7048,-10654.5718 4061.9639,-10653.9355 4057.4558,-10651.1534 4056.7048,-10654.5718"/>
+</g>
+<!-- github.com/klauspost/compress/huff0&#45;&gt;runtime -->
+<g id="edge441" class="edge">
+<title>github.com/klauspost/compress/huff0&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M3420.2125,-10054.6123C3457.033,-10068.4266 3499.8812,-10091.5724 3522,-10128.5998 3619.9969,-10292.6487 3474.3493,-16848.3724 3580,-17007.5998 3687.8638,-17170.1625 3861.2043,-17029.1956 3983,-17181.5998 4057.062,-17274.2744 4087.7942,-17662.2549 4094.5256,-17761.434"/>
+<polygon fill="#000000" stroke="#000000" points="4092.7831,-17761.6054 4094.864,-17766.477 4096.2752,-17761.371 4092.7831,-17761.6054"/>
+</g>
+<!-- github.com/klauspost/compress/huff0&#45;&gt;github.com/klauspost/compress/fse -->
+<g id="edge437" class="edge">
+<title>github.com/klauspost/compress/huff0&#45;&gt;github.com/klauspost/compress/fse</title>
+<path fill="none" stroke="#000000" d="M3465.6021,-10043.6243C3486.9037,-10049.9259 3507.1601,-10060.1908 3522,-10076.5998 3651.3498,-10219.6263 3445.2463,-10368.6528 3580,-10506.5998 3603.7295,-10530.8916 3637.9457,-10541.477 3671.466,-10545.215"/>
+<polygon fill="#000000" stroke="#000000" points="3671.6877,-10546.9962 3676.8382,-10545.7572 3672.0392,-10543.5139 3671.6877,-10546.9962"/>
+</g>
+<!-- github.com/klauspost/compress/snappy&#45;&gt;encoding/binary -->
+<g id="edge443" class="edge">
+<title>github.com/klauspost/compress/snappy&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M3421.8431,-12173.6645C3538.8148,-12201.8764 3785.8915,-12247.3969 3983,-12188.5998 4017.2714,-12178.3767 4050.813,-12154.0716 4072.3316,-12136.0909"/>
+<polygon fill="#000000" stroke="#000000" points="4073.5204,-12137.3775 4076.2071,-12132.812 4071.2597,-12134.7055 4073.5204,-12137.3775"/>
+</g>
+<!-- github.com/klauspost/compress/snappy&#45;&gt;errors -->
+<g id="edge444" class="edge">
+<title>github.com/klauspost/compress/snappy&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3369.1922,-12137.5988C3403.807,-12093.9251 3489.9705,-11977.5299 3522,-11863.5998 3553.9325,-11750.0146 3506.0447,-9832.5339 3580,-9740.5998 3695.6718,-9596.8078 3867.6856,-9784.6786 3983,-9640.5998 4073.1072,-9528.016 4024.5606,-7188.8624 4041,-7045.5998 4051.2924,-6955.9064 4077.4584,-6851.5112 4089.6137,-6805.9095"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3513,-6806.1859 4090.9549,-6800.9033 4087.9705,-6805.2801 4091.3513,-6806.1859"/>
+</g>
+<!-- github.com/klauspost/compress/snappy&#45;&gt;io -->
+<g id="edge446" class="edge">
+<title>github.com/klauspost/compress/snappy&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3397.8502,-12173.6633C3517.4691,-12226.1706 3851.9669,-12391.5996 3983,-12649.5998 4092.6385,-12865.4748 3990.752,-13500.7502 4041,-13737.5998 4050.4503,-13782.1447 4071.6363,-13831.0918 4084.7822,-13858.9096"/>
+<polygon fill="#000000" stroke="#000000" points="4083.2485,-13859.7593 4086.9792,-13863.5203 4086.4082,-13858.2538 4083.2485,-13859.7593"/>
+</g>
+<!-- github.com/klauspost/compress/snappy&#45;&gt;hash/crc32 -->
+<g id="edge445" class="edge">
+<title>github.com/klauspost/compress/snappy&#45;&gt;hash/crc32</title>
+<path fill="none" stroke="#000000" d="M3470.0523,-12155.5998C3558.6989,-12155.5998 3676.1077,-12155.5998 3738.3938,-12155.5998"/>
+<polygon fill="#000000" stroke="#000000" points="3738.5143,-12157.3499 3743.5143,-12155.5998 3738.5142,-12153.8499 3738.5143,-12157.3499"/>
+</g>
+<!-- github.com/klauspost/compress/zstd/internal/xxhash&#45;&gt;encoding/binary -->
+<g id="edge468" class="edge">
+<title>github.com/klauspost/compress/zstd/internal/xxhash&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M3858.5737,-11223.6514C3902.3101,-11238.0618 3953.9612,-11262.9576 3983,-11304.5998 4054.1833,-11406.6782 4021.3075,-11738.7207 4041,-11861.5998 4054.6977,-11947.0721 4078.9111,-12047.1207 4090.0677,-12091.4472"/>
+<polygon fill="#000000" stroke="#000000" points="4088.3771,-12091.9005 4091.2981,-12096.3199 4091.7706,-12091.0436 4088.3771,-12091.9005"/>
+</g>
+<!-- github.com/klauspost/compress/zstd/internal/xxhash&#45;&gt;errors -->
+<g id="edge469" class="edge">
+<title>github.com/klauspost/compress/zstd/internal/xxhash&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3825.1706,-11187.5334C3873.7431,-11164.7843 3949.7433,-11120.6395 3983,-11055.5998 4084.4337,-10857.2276 4016.4729,-7267.0468 4041,-7045.5998 4050.9387,-6955.8666 4077.2858,-6851.4917 4089.5505,-6805.9024"/>
+<polygon fill="#000000" stroke="#000000" points="4091.288,-6806.1811 4090.9041,-6800.8976 4087.9094,-6805.2673 4091.288,-6806.1811"/>
+</g>
+<!-- github.com/klauspost/compress/zstd/internal/xxhash&#45;&gt;math/bits -->
+<g id="edge470" class="edge">
+<title>github.com/klauspost/compress/zstd/internal/xxhash&#45;&gt;math/bits</title>
+<path fill="none" stroke="#000000" d="M3851.7804,-11187.5469C3895.6336,-11172.7645 3949.8565,-11147.4684 3983,-11106.5998 4037.4711,-11039.4326 4080.785,-10766.4272 4092.7218,-10684.7757"/>
+<polygon fill="#000000" stroke="#000000" points="4094.4753,-10684.8781 4093.4625,-10679.6784 4091.0116,-10684.3747 4094.4753,-10684.8781"/>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil -->
+<g id="node121" class="node">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil</title>
+<g id="a_node121"><a xlink:href="https://godoc.org/github.com/matttproud/golang_protobuf_extensions/pbutil" xlink:title="github.com/matttproud/golang_protobuf_extensions/pbutil" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3510,-10178.5998C3510,-10178.5998 3199,-10178.5998 3199,-10178.5998 3193,-10178.5998 3187,-10172.5998 3187,-10166.5998 3187,-10166.5998 3187,-10154.5998 3187,-10154.5998 3187,-10148.5998 3193,-10142.5998 3199,-10142.5998 3199,-10142.5998 3510,-10142.5998 3510,-10142.5998 3516,-10142.5998 3522,-10148.5998 3522,-10154.5998 3522,-10154.5998 3522,-10166.5998 3522,-10166.5998 3522,-10172.5998 3516,-10178.5998 3510,-10178.5998"/>
+<text text-anchor="middle" x="3354.5" y="-10156.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/matttproud/golang_protobuf_extensions/pbutil</text>
+</a>
+</g>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;encoding/binary -->
+<g id="edge481" class="edge">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M3355.9471,-10178.7092C3364.8514,-10284.2615 3417.2967,-10829.5222 3580,-11238.5998 3706.4596,-11556.5517 3815.1531,-11596.4172 3983,-11894.5998 4022.7707,-11965.253 4065.3492,-12051.3694 4084.9565,-12091.6993"/>
+<polygon fill="#000000" stroke="#000000" points="4083.474,-12092.6529 4087.232,-12096.3865 4086.6226,-12091.1243 4083.474,-12092.6529"/>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;errors -->
+<g id="edge482" class="edge">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3418.1426,-10142.5247C3454.7051,-10128.6893 3498.0036,-10105.7066 3522,-10069.5998 3593.4518,-9962.0882 3494.686,-9588.4792 3580,-9491.5998 3701.4838,-9353.6472 3866.7997,-9541.0313 3983,-9398.5998 4065.6608,-9297.2791 4025.9436,-7175.492 4041,-7045.5998 4051.3954,-6955.9183 4077.5086,-6851.517 4089.6321,-6805.9117"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3698,-6806.1873 4090.9698,-6800.905 4087.9884,-6805.2839 4091.3698,-6806.1873"/>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;io -->
+<g id="edge484" class="edge">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3358.1658,-10178.8234C3377.7014,-10276.5045 3470.5975,-10748.1759 3522,-11137.5998 3540.2243,-11275.6666 3500.4,-11647.3264 3580,-11761.5998 3693.5099,-11924.5542 3874.4428,-11788.3051 3983,-11954.5998 4091.3519,-12120.58 4001.8768,-13543.2833 4041,-13737.5998 4049.9878,-13782.2403 4071.3262,-13831.1559 4084.6262,-13858.9418"/>
+<polygon fill="#000000" stroke="#000000" points="4083.0998,-13859.8054 4086.8498,-13863.5471 4086.2516,-13858.2835 4083.0998,-13859.8054"/>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge483" class="edge">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3420.8415,-10142.5344C3457.3929,-10128.8577 3499.7509,-10106.0355 3522,-10069.5998 3594.113,-9951.5058 3564.3506,-7717.0829 3580,-7579.5998 3623.9655,-7193.3551 3745.7418,-6733.3332 3775.0803,-6625.8459"/>
+<polygon fill="#000000" stroke="#000000" points="3776.855,-6625.9902 3776.4863,-6620.7057 3773.479,-6625.0668 3776.855,-6625.9902"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/internal&#45;&gt;sort -->
+<g id="edge535" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/internal&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M2975.4948,-6197.5788C3012.3749,-6160.3143 3094.481,-6070.3909 3129,-5976.5998 3183.6766,-5828.0387 3092.8194,-5393.8396 3187,-5266.5998 3282.3052,-5137.8406 3428.0546,-5263.3545 3522,-5133.5998 3647.077,-4960.847 3424.6991,-4321.7827 3580,-4175.5998 3710.4214,-4052.8357 3841.9895,-4065.1606 3983,-4175.5998 4067.6944,-4241.9323 3976.5035,-4324.4991 4041,-4410.5998 4046.9794,-4418.5822 4055.5818,-4424.9142 4064.2134,-4429.7672"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7592,-4431.5079 4068.9952,-4432.3122 4065.4037,-4428.4182 4063.7592,-4431.5079"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/internal&#45;&gt;github.com/prometheus/client_model/go -->
+<g id="edge534" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/internal&#45;&gt;github.com/prometheus/client_model/go</title>
+<path fill="none" stroke="#000000" d="M2999.0609,-6233.692C3035.2462,-6250.2574 3087.9477,-6276.8286 3129,-6307.5998 3215.912,-6372.7457 3300.752,-6471.4392 3336.6318,-6515.271"/>
+<polygon fill="#000000" stroke="#000000" points="3335.4331,-6516.5701 3339.9496,-6519.3385 3338.1452,-6514.3578 3335.4331,-6516.5701"/>
+</g>
+<!-- github.com/prometheus/client_model/go&#45;&gt;fmt -->
+<g id="edge552" class="edge">
+<title>github.com/prometheus/client_model/go&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3418.7554,-6555.6682C3455.5417,-6569.5807 3498.8602,-6592.8092 3522,-6629.5998 3585.1539,-6730.01 3499.7067,-7608.2866 3580,-7695.5998 3702.4047,-7828.7063 3856.7034,-7622.1803 3983,-7751.5998 4018.0404,-7787.5066 4082.3845,-8571.5202 4094.1331,-8718.1059"/>
+<polygon fill="#000000" stroke="#000000" points="4092.4167,-8718.597 4094.56,-8723.4415 4095.9056,-8718.3178 4092.4167,-8718.597"/>
+</g>
+<!-- github.com/prometheus/client_model/go&#45;&gt;math -->
+<g id="edge554" class="edge">
+<title>github.com/prometheus/client_model/go&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3358.1502,-6519.3004C3378.8191,-6414.7869 3481.6721,-5882.6003 3522,-5442.5998 3531.4867,-5339.0948 3505.3736,-3644.9475 3580,-3572.5998 3708.5989,-3447.9277 3833.9575,-3473.2664 3983,-3572.5998 4068.8819,-3629.8382 4089.5999,-3766.0516 4094.5091,-3820.515"/>
+<polygon fill="#000000" stroke="#000000" points="4092.7678,-3820.6919 4094.9365,-3825.525 4096.2551,-3820.3943 4092.7678,-3820.6919"/>
+</g>
+<!-- github.com/prometheus/client_model/go&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge553" class="edge">
+<title>github.com/prometheus/client_model/go&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3472.8763,-6555.6196C3536.2533,-6565.2672 3613.6875,-6577.0546 3675.1376,-6586.4088"/>
+<polygon fill="#000000" stroke="#000000" points="3675.0751,-6588.1694 3680.2816,-6587.1919 3675.6019,-6584.7093 3675.0751,-6588.1694"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;bufio -->
+<g id="edge555" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M2966.9185,-6358.0017C2998.4099,-6417.5934 3095.9942,-6611.5196 3129,-6785.5998 3151.4682,-6904.1024 3108.6746,-15377.8784 3187,-15469.5998 3244.4839,-15536.9152 3491.7773,-15527.3533 3580,-15534.5998 3669.255,-15541.9312 3918.5053,-15596.7338 3983,-15534.5998 4107.5149,-15414.6426 3953.0237,-14128.4416 4041,-13979.5998 4046.3562,-13970.5379 4055.2563,-13963.6687 4064.3526,-13958.6085"/>
+<polygon fill="#000000" stroke="#000000" points="4065.2026,-13960.1389 4068.8192,-13956.2681 4063.5781,-13957.0387 4065.2026,-13960.1389"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;bytes -->
+<g id="edge556" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2966.8844,-6358.0082C2998.2729,-6417.6195 3095.5815,-6611.5984 3129,-6785.5998 3155.6064,-6924.1327 3102.5347,-11750.6181 3187,-11863.5998 3280.2689,-11988.3573 3373.8134,-11918.5973 3522,-11966.5998 3727.911,-12033.3012 3850.8157,-11952.2064 3983,-12123.5998 4082.4836,-12252.5927 4094.6078,-13495.5856 4095.8672,-13681.4072"/>
+<polygon fill="#000000" stroke="#000000" points="4094.1183,-13681.6061 4095.9013,-13686.5944 4097.6183,-13681.5829 4094.1183,-13681.6061"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;fmt -->
+<g id="edge557" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3018.0213,-6357.759C3056.9037,-6372.7739 3104.436,-6398.2926 3129,-6438.5998 3274.5192,-6677.383 3024.6415,-7467.9313 3187,-7695.5998 3280.0113,-7826.0257 3402.6129,-7721.7881 3522,-7828.5998 3562.8972,-7865.1892 3538.0386,-7902.236 3580,-7937.5998 3724.2251,-8059.1482 3860.1367,-7927.4931 3983,-8070.5998 4069.4587,-8171.3037 4091.1997,-8612.2003 4095.223,-8718.4859"/>
+<polygon fill="#000000" stroke="#000000" points="4093.4777,-8718.6478 4095.4119,-8723.5795 4096.9753,-8718.518 4093.4777,-8718.6478"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;io -->
+<g id="edge563" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2966.9073,-6358.0038C2998.3649,-6417.6019 3095.8587,-6611.5454 3129,-6785.5998 3165.5898,-6977.7651 3095.6369,-13654.6285 3187,-13827.5998 3286.3292,-14015.6528 3377.0502,-14037.0264 3580,-14100.5998 3750.9216,-14154.1405 3838.7243,-14206.738 3983,-14100.5998 4052.7511,-14049.2865 3987.4771,-13982.6701 4041,-13914.5998 4047.1559,-13906.7708 4055.7235,-13900.3843 4064.2664,-13895.388"/>
+<polygon fill="#000000" stroke="#000000" points="4065.4776,-13896.7166 4068.9944,-13892.7549 4063.7747,-13893.6588 4065.4776,-13896.7166"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;io/ioutil -->
+<g id="edge564" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2966.9188,-6358.0016C2998.4111,-6417.5932 3095.9977,-6611.5189 3129,-6785.5998 3151.6029,-6904.8261 3108.0098,-15430.4784 3187,-15522.5998 3236.0836,-15579.843 3450.8074,-15551.7485 3522,-15576.5998 3743.881,-15654.052 3841.0263,-15648.3206 3983,-15835.5998 4037.6358,-15907.6706 4081.072,-16193.4859 4092.8406,-16277.3517"/>
+<polygon fill="#000000" stroke="#000000" points="4091.1457,-16277.8691 4093.5697,-16282.5793 4094.6122,-16277.3855 4091.1457,-16277.8691"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;math -->
+<g id="edge565" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3026.4328,-6321.5566C3063.6624,-6307.9931 3106.4153,-6285.2772 3129,-6248.5998 3214.7742,-6109.303 3099.701,-3442.9461 3187,-3304.5998 3284.2454,-3150.4911 3844.7152,-2988.9259 3983,-3107.5998 4038.8242,-3155.5073 4084.7105,-3700.0642 4094.1994,-3820.2298"/>
+<polygon fill="#000000" stroke="#000000" points="4092.4618,-3820.4572 4094.5984,-3825.3046 4095.9511,-3820.1828 4092.4618,-3820.4572"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;strconv -->
+<g id="edge568" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3078.0649,-6330.9102C3097.3276,-6324.4499 3115.3646,-6314.5498 3129,-6299.5998 3256.5773,-6159.7227 3055.0898,-6013.3985 3187,-5877.5998 3292.1797,-5769.3197 3417.9935,-5931.0074 3522,-5821.5998 3599.3721,-5740.2098 3496.1604,-5394.3105 3580,-5319.5998 3719.3993,-5195.3791 3972.9762,-5294.1747 4063.1174,-5335.5637"/>
+<polygon fill="#000000" stroke="#000000" points="4062.5246,-5337.2176 4067.7974,-5337.7286 4063.9941,-5334.041 4062.5246,-5337.2176"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;strings -->
+<g id="edge569" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3078.1036,-6350.0605C3322.1915,-6373.7187 3860.4579,-6439.7457 3983,-6570.5998 3991.5729,-6579.7542 4070.6175,-6956.0488 4091.1468,-7054.321"/>
+<polygon fill="#000000" stroke="#000000" points="4089.4567,-7054.7887 4092.1919,-7059.3253 4092.8828,-7054.0732 4089.4567,-7054.7887"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;sync -->
+<g id="edge570" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3026.4495,-6321.5669C3063.6846,-6308.0067 3106.4374,-6285.2908 3129,-6248.5998 3220.2766,-6100.1668 3115.2259,-3271.3834 3187,-3112.5998 3284.4437,-2897.0282 3359.4465,-2839.1718 3580,-2753.5998 3746.9832,-2688.8124 3841.3195,-2644.0214 3983,-2753.5998 4050.0386,-2805.4488 4084.6706,-3082.5251 4093.6226,-3165.2588"/>
+<polygon fill="#000000" stroke="#000000" points="4091.903,-3165.6374 4094.1752,-3170.4228 4095.3832,-3165.265 4091.903,-3165.6374"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;mime -->
+<g id="edge566" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;mime</title>
+<path fill="none" stroke="#000000" d="M3025.0207,-6321.5359C3062.2715,-6307.896 3105.5163,-6285.1105 3129,-6248.5998 3247.2555,-6064.7447 3110.3646,-5471.3291 3187,-5266.5998 3217.8268,-5184.2469 3291.4094,-5107.7304 3329.9717,-5071.5247"/>
+<polygon fill="#000000" stroke="#000000" points="3331.5793,-5072.4186 3334.0437,-5067.7293 3329.1929,-5069.8583 3331.5793,-5072.4186"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;net/http -->
+<g id="edge567" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M3026.9451,-6321.442C3064.0953,-6307.8748 3106.6011,-6285.193 3129,-6248.5998 3187.4777,-6153.0647 3165.1061,-2326.4509 3187,-2216.5998 3265.6417,-1822.02 3418.2433,-1764.3315 3522,-1375.5998 3568.779,-1200.3388 3487.9115,-1127.8829 3580,-971.5998 3699.2395,-769.2387 3967.6415,-636.7118 4062.2298,-594.8497"/>
+<polygon fill="#000000" stroke="#000000" points="4062.9988,-596.4233 4066.8701,-592.8072 4061.5887,-593.2199 4062.9988,-596.4233"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge558" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3075.06,-6357.6762C3093.7857,-6363.1591 3112.4223,-6370.3003 3129,-6379.5998 3161.0697,-6397.5897 3154.6545,-6421.1108 3187,-6438.5998 3320.5638,-6510.817 3375.9736,-6463.9956 3522,-6505.5998 3597.9536,-6527.2397 3683.234,-6561.0266 3734.6278,-6582.4749"/>
+<polygon fill="#000000" stroke="#000000" points="3734.2061,-6584.1955 3739.4941,-6584.5112 3735.5572,-6580.9667 3734.2061,-6584.1955"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/matttproud/golang_protobuf_extensions/pbutil -->
+<g id="edge559" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/matttproud/golang_protobuf_extensions/pbutil</title>
+<path fill="none" stroke="#000000" d="M2966.8415,-6358.0165C2998.1005,-6417.6529 3095.0624,-6611.6989 3129,-6785.5998 3146.7469,-6876.5372 3128.9652,-10048.3743 3187,-10120.5998 3193.0833,-10128.1706 3200.3198,-10134.4641 3208.3188,-10139.6826"/>
+<polygon fill="#000000" stroke="#000000" points="3207.6647,-10141.3354 3212.8387,-10142.4725 3209.5031,-10138.357 3207.6647,-10141.3354"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/client_model/go -->
+<g id="edge560" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/client_model/go</title>
+<path fill="none" stroke="#000000" d="M3078.4218,-6353.4762C3096.6386,-6359.4724 3114.2357,-6367.8733 3129,-6379.5998 3174.7599,-6415.9446 3141.3478,-6461.1198 3187,-6497.5998 3199.7001,-6507.7483 3214.5618,-6515.3902 3230.094,-6521.1326"/>
+<polygon fill="#000000" stroke="#000000" points="3229.6368,-6522.8273 3234.9342,-6522.8544 3230.8099,-6519.5297 3229.6368,-6522.8273"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/common/model -->
+<g id="edge562" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/common/model</title>
+<path fill="none" stroke="#000000" d="M3024.919,-6321.4702C3062.1353,-6307.808 3105.3796,-6285.0222 3129,-6248.5998 3234.5488,-6085.8444 3071.3452,-5533.3363 3187,-5377.5998 3281.8398,-5249.8919 3418.8063,-5372.6575 3522,-5251.5998 3605.856,-5153.2275 3488.378,-5057.7828 3580,-4966.5998 3600.7905,-4945.9089 3629.019,-4934.384 3657.7607,-4928.2712"/>
+<polygon fill="#000000" stroke="#000000" points="3658.1977,-4929.9684 3662.7527,-4927.2639 3657.5054,-4926.5375 3658.1977,-4929.9684"/>
+</g>
+<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg -->
+<g id="node129" class="node">
+<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</title>
+<g id="a_node129"><a xlink:href="https://godoc.org/github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" xlink:title="github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3971,-5369.5998C3971,-5369.5998 3592,-5369.5998 3592,-5369.5998 3586,-5369.5998 3580,-5363.5998 3580,-5357.5998 3580,-5357.5998 3580,-5345.5998 3580,-5345.5998 3580,-5339.5998 3586,-5333.5998 3592,-5333.5998 3592,-5333.5998 3971,-5333.5998 3971,-5333.5998 3977,-5333.5998 3983,-5339.5998 3983,-5345.5998 3983,-5345.5998 3983,-5357.5998 3983,-5357.5998 3983,-5363.5998 3977,-5369.5998 3971,-5369.5998"/>
+<text text-anchor="middle" x="3781.5" y="-5347.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg -->
+<g id="edge561" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</title>
+<path fill="none" stroke="#000000" d="M3076.1548,-6321.5888C3094.5516,-6316.1001 3112.7981,-6308.94 3129,-6299.5998 3350.1942,-6172.084 3406.6902,-6108.3953 3522,-5880.5998 3585.6993,-5754.7613 3516.6162,-5694.5976 3580,-5568.5998 3622.6374,-5483.8428 3708.5163,-5408.5162 3753.1346,-5373.0698"/>
+<polygon fill="#000000" stroke="#000000" points="3754.4807,-5374.2368 3757.3209,-5369.765 3752.312,-5371.4896 3754.4807,-5374.2368"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;encoding/json -->
+<g id="edge574" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3872.3035,-4944.6681C3913.4491,-4957.8648 3958.3218,-4980.4038 3983,-5018.5998 4074.857,-5160.773 3956.2634,-17059.0712 4041,-17205.5998 4043.2657,-17209.5178 4046.2149,-17213.0012 4049.5706,-17216.089"/>
+<polygon fill="#000000" stroke="#000000" points="4048.7243,-17217.663 4053.6803,-17219.5342 4050.9729,-17214.9809 4048.7243,-17217.663"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;fmt -->
+<g id="edge575" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3871.5402,-4944.6585C3912.7688,-4957.8701 3957.9373,-4980.4247 3983,-5018.5998 4028.5754,-5088.0195 4037.0903,-7924.6485 4041,-8007.5998 4054.2131,-8287.9426 4085.2354,-8627.4988 4093.7846,-8718.3337"/>
+<polygon fill="#000000" stroke="#000000" points="4092.0566,-8718.65 4094.2684,-8723.4636 4095.5411,-8718.3213 4092.0566,-8718.65"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;math -->
+<g id="edge576" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3808.2227,-4908.4191C3853.3078,-4876.0544 3943.4294,-4803.6976 3983,-4717.5998 4097.0979,-4469.3456 3999.9805,-4375.7218 4041,-4105.5998 4054.505,-4016.6663 4079.026,-3912.4535 4090.1874,-3866.8957"/>
+<polygon fill="#000000" stroke="#000000" points="4091.9227,-3867.1672 4091.417,-3861.894 4088.5239,-3866.3316 4091.9227,-3867.1672"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;sort -->
+<g id="edge578" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3815.3005,-4908.3953C3859.8014,-4882.797 3937.6853,-4832.1088 3983,-4768.5998 4053.6111,-4669.6377 4083.2848,-4522.6931 4092.5505,-4466.0443"/>
+<polygon fill="#000000" stroke="#000000" points="4094.3199,-4466.0628 4093.3842,-4460.8487 4090.8641,-4465.5082 4094.3199,-4466.0628"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;strconv -->
+<g id="edge579" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3856.0207,-4944.7244C3898.4395,-4958.5405 3949.6108,-4981.6497 3983,-5018.5998 4067.0335,-5111.5954 4088.9589,-5269.6017 4094.3472,-5328.5355"/>
+<polygon fill="#000000" stroke="#000000" points="4092.6109,-5328.7684 4094.7912,-5333.5963 4096.0975,-5328.4625 4092.6109,-5328.7684"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;strings -->
+<g id="edge580" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3870.809,-4944.6314C3912.1253,-4957.8542 3957.5857,-4980.4267 3983,-5018.5998 4038.3543,-5101.7441 4029.083,-6716.4279 4041,-6815.5998 4051.732,-6904.9104 4077.6729,-7008.9301 4089.6922,-7054.3713"/>
+<polygon fill="#000000" stroke="#000000" points="4088.0424,-7054.9772 4091.0182,-7059.3599 4091.425,-7054.0781 4088.0424,-7054.9772"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;time -->
+<g id="edge581" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3871.9698,-4944.6316C3913.1689,-4957.828 3958.1856,-4980.3775 3983,-5018.5998 4078.8251,-5166.2019 3952.5678,-11201.4533 4041,-11353.5998 4046.2955,-11362.7106 4055.2811,-11369.4748 4064.4715,-11374.376"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7298,-11375.9617 4068.9843,-11376.6346 4065.2963,-11372.8318 4063.7298,-11375.9617"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;unicode/utf8 -->
+<g id="edge582" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M3806.6958,-4908.5364C3850.2811,-4875.8156 3939.2708,-4802.2296 3983,-4717.5998 4046.743,-4594.2369 3961.6177,-4524.5298 4041,-4410.5998 4044.0706,-4406.1929 4047.9366,-4402.2752 4052.1893,-4398.8187"/>
+<polygon fill="#000000" stroke="#000000" points="4053.2723,-4400.1936 4056.1918,-4395.7732 4051.1529,-4397.4082 4053.2723,-4400.1936"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;regexp -->
+<g id="edge577" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M3809.1928,-4908.4281C3855.3051,-4876.3373 3946.3717,-4804.7167 3983,-4717.5998 4069.4675,-4511.945 4023.0288,-2932.9679 4041,-2710.5998 4053.0718,-2561.2275 4081.6261,-2383.1353 4092.0787,-2320.6372"/>
+<polygon fill="#000000" stroke="#000000" points="4093.815,-2320.8642 4092.9166,-2315.6436 4090.3632,-2320.285 4093.815,-2320.8642"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;bufio -->
+<g id="edge583" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3392.2487,-13812.6907C3436.2792,-13832.9926 3511.7576,-13865.3393 3580,-13882.5998 3759.1885,-13927.9219 3981.8322,-13941.6866 4063.7887,-13945.3812"/>
+<polygon fill="#000000" stroke="#000000" points="4063.9093,-13947.1381 4068.9816,-13945.6102 4064.0636,-13943.6415 4063.9093,-13947.1381"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;bytes -->
+<g id="edge584" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3446.5592,-13810.0752C3571.5953,-13827.3735 3800.4458,-13845.7158 3983,-13785.5998 4018.828,-13773.8015 4053.0225,-13746.2237 4074.2268,-13726.5279"/>
+<polygon fill="#000000" stroke="#000000" points="4075.5899,-13727.6483 4078.0324,-13722.9475 4073.1916,-13725.0991 4075.5899,-13727.6483"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;encoding/hex -->
+<g id="edge585" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;encoding/hex</title>
+<path fill="none" stroke="#000000" d="M3364.7539,-13776.3918C3395.922,-13719.8981 3489.3948,-13541.7075 3522,-13380.5998 3564.8616,-13168.8139 3497.8833,-9691.4678 3580,-9491.5998 3614.9487,-9406.5365 3702.0551,-9336.2784 3749.4891,-9302.7705"/>
+<polygon fill="#000000" stroke="#000000" points="3750.5833,-9304.1407 3753.673,-9299.8377 3748.5742,-9301.2747 3750.5833,-9304.1407"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;errors -->
+<g id="edge586" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3364.7304,-13776.387C3395.8309,-13719.8795 3489.1298,-13541.6536 3522,-13380.5998 3557.4009,-13207.1462 3491.1274,-10347.7045 3580,-10194.5998 3684.6551,-10014.306 3878.1887,-10134.8029 3983,-9954.5998 4064.2695,-9814.8722 4022.7659,-7206.2113 4041,-7045.5998 4051.1842,-6955.8941 4077.4056,-6851.5052 4089.5944,-6805.9073"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3319,-6806.1844 4090.9394,-6800.9016 4087.9518,-6805.2762 4091.3319,-6806.1844"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;fmt -->
+<g id="edge587" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3364.0009,-13776.2185C3393.0015,-13719.2259 3480.8989,-13539.7522 3522,-13380.5998 3575.4002,-13173.8224 3486.2547,-13095.486 3580,-12903.5998 3690.914,-12676.5711 3882.5228,-12734.4366 3983,-12502.5998 3985.4778,-12496.8827 4086.0396,-9080.1968 4095.3161,-8764.854"/>
+<polygon fill="#000000" stroke="#000000" points="4097.0727,-8764.652 4095.4705,-8759.6027 4093.5742,-8764.5491 4097.0727,-8764.652"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;io -->
+<g id="edge590" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3423.5987,-13812.6799C3467.9796,-13823.6052 3527.0273,-13836.8704 3580,-13844.5998 3761.7524,-13871.12 3982.4177,-13878.8809 4063.8315,-13880.9301"/>
+<polygon fill="#000000" stroke="#000000" points="4063.9492,-13882.6834 4068.9908,-13881.0569 4064.0353,-13879.1845 4063.9492,-13882.6834"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;io/ioutil -->
+<g id="edge591" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M3366.6377,-13812.8476C3399.1587,-13861.3283 3491.7824,-13996.7342 3580,-14100.5998 3746.0236,-14296.0726 3878.8585,-14279.2326 3983,-14513.5998 4058.0362,-14682.4663 4091.1669,-16079.2441 4095.5042,-16277.2823"/>
+<polygon fill="#000000" stroke="#000000" points="4093.7599,-16277.5677 4095.6185,-16282.5284 4097.2591,-16277.4914 4093.7599,-16277.5677"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;sort -->
+<g id="edge596" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3364.8116,-13776.4033C3396.1457,-13719.9429 3490.0457,-13541.8379 3522,-13380.5998 3544.9127,-13264.9845 3515.2211,-4993.066 3580,-4894.5998 3687.5172,-4731.17 3846.3396,-4857.5754 3983,-4717.5998 4054.9883,-4643.8652 4083.4195,-4517.8867 4092.4485,-4465.9936"/>
+<polygon fill="#000000" stroke="#000000" points="4094.1992,-4466.1368 4093.3111,-4460.9143 4090.7486,-4465.5507 4094.1992,-4466.1368"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;strconv -->
+<g id="edge597" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3364.8,-13776.401C3396.1009,-13719.934 3489.9154,-13541.812 3522,-13380.5998 3559.2073,-13193.6485 3486.7607,-6684.8575 3580,-6518.5998 3683.2141,-6334.5556 3869.4428,-6445.4486 3983,-6267.5998 4081.6689,-6113.0685 4094.2312,-5502.8226 4095.786,-5375.112"/>
+<polygon fill="#000000" stroke="#000000" points="4097.5401,-5374.7593 4095.8483,-5369.7393 4094.0404,-5374.7187 4097.5401,-5374.7593"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;strings -->
+<g id="edge598" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3364.7246,-13776.3858C3395.8084,-13719.875 3489.0644,-13541.6403 3522,-13380.5998 3555.9577,-13214.5616 3479.4002,-10466.9871 3580,-10330.5998 3691.9601,-10178.811 3870.532,-10349.0127 3983,-10197.5998 4057.7265,-10096.9974 4035.4118,-8067.7944 4041,-7942.5998 4055.9311,-7608.0939 4086.4598,-7201.5672 4094.1899,-7100.9522"/>
+<polygon fill="#000000" stroke="#000000" points="4095.9439,-7100.9669 4094.5827,-7095.8474 4092.4542,-7100.6983 4095.9439,-7100.9669"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;time -->
+<g id="edge599" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3413.1726,-13776.53C3543.3366,-13732.9263 3854.8688,-13608.3506 3983,-13380.5998 4069.3547,-13227.1059 4026.0805,-11972.0847 4041,-11796.5998 4053.605,-11648.3385 4081.7084,-11471.5679 4092.0673,-11408.9699"/>
+<polygon fill="#000000" stroke="#000000" points="4093.8057,-11409.1835 4092.8981,-11403.9644 4090.3529,-11408.6103 4093.8057,-11409.1835"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;os -->
+<g id="edge593" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3361.461,-13812.9236C3388.6045,-13885.4296 3487.5377,-14159.808 3522,-14395.5998 3572.5129,-14741.2096 3474.8709,-17205.5149 3580,-17538.5998 3676.4654,-17844.2353 3875.8575,-17834.5417 3983,-18136.5998 4031.6277,-18273.692 3962.9596,-18665.8455 4041,-18788.5998 4046.4508,-18797.1738 4055.0837,-18803.7173 4063.9087,-18808.5827"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5667,-18810.3757 4068.8123,-18811.1153 4065.1729,-18807.2659 4063.5667,-18810.3757"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;path/filepath -->
+<g id="edge594" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M3361.4835,-13812.9203C3388.711,-13885.4141 3487.9166,-14159.7529 3522,-14395.5998 3582.4925,-14814.1894 3493.0661,-17787.6928 3580,-18201.5998 3688.4527,-18717.9616 3794.289,-18829.2039 4041,-19295.5998 4052.7437,-19317.8007 4068.5127,-19341.6997 4080.1548,-19358.4845"/>
+<polygon fill="#000000" stroke="#000000" points="4078.7248,-19359.4935 4083.0207,-19362.5932 4081.5955,-19357.4911 4078.7248,-19359.4935"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;regexp -->
+<g id="edge595" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M3364.8229,-13776.4056C3396.1897,-13719.9516 3490.1737,-13541.8632 3522,-13380.5998 3551.724,-13229.9889 3474.3526,-2438.9811 3580,-2327.5998 3645.9667,-2258.0529 3961.2046,-2283.692 4063.7146,-2294.0802"/>
+<polygon fill="#000000" stroke="#000000" points="4063.5699,-2295.8244 4068.7221,-2294.5927 4063.9263,-2292.3426 4063.5699,-2295.8244"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;net -->
+<g id="edge592" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M3361.4348,-13812.9274C3388.4801,-13885.4479 3487.0956,-14159.873 3522,-14395.5998 3543.2603,-14539.1812 3491.2934,-16892.7139 3580,-17007.5998 3693.8334,-17155.0281 3807.8583,-17059.2081 3983,-17122.5998 4010.8721,-17132.688 4041.5755,-17146.452 4063.9344,-17156.985"/>
+<polygon fill="#000000" stroke="#000000" points="4063.3068,-17158.624 4068.5749,-17159.1811 4064.8041,-17155.4604 4063.3068,-17158.624"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/fs -->
+<g id="node130" class="node">
+<title>github.com/prometheus/procfs/internal/fs</title>
+<g id="a_node130"><a xlink:href="https://godoc.org/github.com/prometheus/procfs/internal/fs" xlink:title="github.com/prometheus/procfs/internal/fs" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3890.5,-18186.5998C3890.5,-18186.5998 3672.5,-18186.5998 3672.5,-18186.5998 3666.5,-18186.5998 3660.5,-18180.5998 3660.5,-18174.5998 3660.5,-18174.5998 3660.5,-18162.5998 3660.5,-18162.5998 3660.5,-18156.5998 3666.5,-18150.5998 3672.5,-18150.5998 3672.5,-18150.5998 3890.5,-18150.5998 3890.5,-18150.5998 3896.5,-18150.5998 3902.5,-18156.5998 3902.5,-18162.5998 3902.5,-18162.5998 3902.5,-18174.5998 3902.5,-18174.5998 3902.5,-18180.5998 3896.5,-18186.5998 3890.5,-18186.5998"/>
+<text text-anchor="middle" x="3781.5" y="-18164.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs/internal/fs</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;github.com/prometheus/procfs/internal/fs -->
+<g id="edge588" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;github.com/prometheus/procfs/internal/fs</title>
+<path fill="none" stroke="#000000" d="M3361.4797,-13812.9208C3388.6929,-13885.4167 3487.852,-14159.7622 3522,-14395.5998 3536.6264,-14496.6147 3525.3551,-17983.3916 3580,-18069.5998 3605.6185,-18110.0157 3653.0069,-18134.4358 3695.3934,-18148.9071"/>
+<polygon fill="#000000" stroke="#000000" points="3694.9733,-18150.6118 3700.2702,-18150.5363 3696.0823,-18147.2922 3694.9733,-18150.6118"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util -->
+<g id="node131" class="node">
+<title>github.com/prometheus/procfs/internal/util</title>
+<g id="a_node131"><a xlink:href="https://godoc.org/github.com/prometheus/procfs/internal/util" xlink:title="github.com/prometheus/procfs/internal/util" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3895,-14085.5998C3895,-14085.5998 3668,-14085.5998 3668,-14085.5998 3662,-14085.5998 3656,-14079.5998 3656,-14073.5998 3656,-14073.5998 3656,-14061.5998 3656,-14061.5998 3656,-14055.5998 3662,-14049.5998 3668,-14049.5998 3668,-14049.5998 3895,-14049.5998 3895,-14049.5998 3901,-14049.5998 3907,-14055.5998 3907,-14061.5998 3907,-14061.5998 3907,-14073.5998 3907,-14073.5998 3907,-14079.5998 3901,-14085.5998 3895,-14085.5998"/>
+<text text-anchor="middle" x="3781.5" y="-14063.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs/internal/util</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;github.com/prometheus/procfs/internal/util -->
+<g id="edge589" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;github.com/prometheus/procfs/internal/util</title>
+<path fill="none" stroke="#000000" d="M3365.4301,-13812.9253C3394.0599,-13859.1125 3476.2495,-13980.5198 3580,-14035.5998 3601.6139,-14047.0744 3626.2369,-14054.6863 3650.6708,-14059.6846"/>
+<polygon fill="#000000" stroke="#000000" points="3650.6366,-14061.4615 3655.8808,-14060.712 3651.3138,-14058.0276 3650.6366,-14061.4615"/>
+</g>
+<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;sort -->
+<g id="edge571" class="edge">
+<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3800.3061,-5333.5242C3841.1383,-5293.0499 3937.8572,-5190.2834 3983,-5083.5998 4047.6946,-4930.7106 4011.6526,-4877.9988 4041,-4714.5998 4057.6064,-4622.1394 4080.8229,-4512.7312 4090.9464,-4465.831"/>
+<polygon fill="#000000" stroke="#000000" points="4092.7114,-4465.9485 4092.0574,-4460.6916 4089.2904,-4465.2089 4092.7114,-4465.9485"/>
+</g>
+<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;strconv -->
+<g id="edge572" class="edge">
+<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3983.3466,-5351.5998C4014.0035,-5351.5998 4042.1951,-5351.5998 4062.7083,-5351.5998"/>
+<polygon fill="#000000" stroke="#000000" points="4062.755,-5353.3499 4067.7549,-5351.5998 4062.7549,-5349.8499 4062.755,-5353.3499"/>
+</g>
+<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;strings -->
+<g id="edge573" class="edge">
+<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3956.8499,-5369.6907C3966.594,-5375.577 3975.4641,-5382.7927 3983,-5391.5998 4034.476,-5451.7593 4031.2219,-6737.0292 4041,-6815.5998 4052.109,-6904.8643 4077.8569,-7008.9076 4089.7595,-7054.363"/>
+<polygon fill="#000000" stroke="#000000" points="4088.1078,-7054.9631 4091.0724,-7059.3533 4091.4926,-7054.0725 4088.1078,-7054.9631"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/fs&#45;&gt;fmt -->
+<g id="edge600" class="edge">
+<title>github.com/prometheus/procfs/internal/fs&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3785.4228,-18150.4356C3809.7109,-18036.9624 3939.9507,-17413.5725 3983,-16897.5998 4040.908,-16203.5371 4027.6368,-11325.9459 4041,-10629.5998 4055.69,-9864.1158 4089.334,-8924.4714 4095.1392,-8765.0862"/>
+<polygon fill="#000000" stroke="#000000" points="4096.9004,-8764.8076 4095.3339,-8759.7471 4093.4027,-8764.68 4096.9004,-8764.8076"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/fs&#45;&gt;os -->
+<g id="edge601" class="edge">
+<title>github.com/prometheus/procfs/internal/fs&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3819.765,-18186.7662C3866.8559,-18211.3334 3945.2069,-18259.8752 3983,-18326.5998 4084.9909,-18506.6668 3926.8337,-18615.9953 4041,-18788.5998 4046.605,-18797.0738 4055.2812,-18803.5892 4064.0986,-18808.4596"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7487,-18810.2495 4068.993,-18810.9981 4065.3602,-18807.1425 4063.7487,-18810.2495"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/fs&#45;&gt;path/filepath -->
+<g id="edge602" class="edge">
+<title>github.com/prometheus/procfs/internal/fs&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M3792.28,-18186.7748C3827.1122,-18246.5181 3937.0378,-18443.2252 3983,-18621.5998 4058.022,-18912.7522 3943.5515,-19011.1674 4041,-19295.5998 4048.9827,-19318.8998 4064.7049,-19342.1592 4077.3023,-19358.4416"/>
+<polygon fill="#000000" stroke="#000000" points="4075.9669,-19359.5747 4080.4311,-19362.4267 4078.7198,-19357.4133 4075.9669,-19359.5747"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;bytes -->
+<g id="edge603" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3907.184,-14069.062C3934.7152,-14063.8942 3961.901,-14053.8457 3983,-14035.5998 4048.4984,-13978.9585 4012.7431,-13931.4522 4041,-13849.5998 4056.0326,-13806.0547 4075.5398,-13755.9444 4086.8251,-13727.4969"/>
+<polygon fill="#000000" stroke="#000000" points="4088.4782,-13728.0756 4088.6987,-13722.7827 4085.2257,-13726.7828 4088.4782,-13728.0756"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;io/ioutil -->
+<g id="edge604" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M3807.5068,-14085.7725C3852.7766,-14119.1193 3944.8654,-14194.9497 3983,-14284.5998 4064.9437,-14477.24 4092.3508,-16065.6828 4095.6508,-16277.3303"/>
+<polygon fill="#000000" stroke="#000000" points="4093.9012,-16277.3817 4095.7286,-16282.3539 4097.4008,-16277.3274 4093.9012,-16277.3817"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;strconv -->
+<g id="edge606" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3907.1787,-14073.994C3936.0671,-14069.0133 3963.8801,-14057.816 3983,-14035.5998 4049.0035,-13958.9076 4038.6095,-6851.7553 4041,-6750.5998 4054.1973,-6192.1467 4088.0237,-5508.9171 4094.8056,-5374.9885"/>
+<polygon fill="#000000" stroke="#000000" points="4096.5663,-5374.8199 4095.0719,-5369.7377 4093.0708,-5374.6426 4096.5663,-5374.8199"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;strings -->
+<g id="edge607" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3907.1549,-14073.9735C3936.0437,-14068.9932 3963.8628,-14057.8011 3983,-14035.5998 4038.2544,-13971.4984 4037.911,-8027.1723 4041,-7942.5998 4053.2218,-7607.984 4085.7511,-7201.5384 4094.0509,-7100.9466"/>
+<polygon fill="#000000" stroke="#000000" points="4095.8048,-7100.9702 4094.4729,-7095.8429 4092.3167,-7100.6817 4095.8048,-7100.9702"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;os -->
+<g id="edge605" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3793.0588,-14085.7543C3829.7678,-14144.6 3943.5974,-14336.5277 3983,-14513.5998 4034.5922,-14745.4508 3921.1893,-18583.5095 4041,-18788.5998 4046.2677,-18797.6169 4055.1446,-18804.3381 4064.2469,-18809.2293"/>
+<polygon fill="#000000" stroke="#000000" points="4063.4663,-18810.7955 4068.7186,-18811.4855 4065.0429,-18807.6707 4063.4663,-18810.7955"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;syscall -->
+<g id="edge608" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;syscall</title>
+<path fill="none" stroke="#000000" d="M3793.0523,-14085.7558C3829.7416,-14144.6059 3943.5184,-14336.5454 3983,-14513.5998 4080.0077,-14948.6283 3985.8283,-18082.3144 4041,-18524.5998 4050.665,-18602.0797 4076.134,-18691.3553 4088.7317,-18732.5629"/>
+<polygon fill="#000000" stroke="#000000" points="4087.1327,-18733.3171 4090.2753,-18737.5818 4090.4781,-18732.2882 4087.1327,-18733.3171"/>
+</g>
+<!-- github.com/ulikunitz/xz/internal/xlog&#45;&gt;fmt -->
+<g id="edge635" class="edge">
+<title>github.com/ulikunitz/xz/internal/xlog&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3891.5989,-13286.2479C3925.9037,-13281.4391 3960.5805,-13269.2977 3983,-13242.5998 4029.6884,-13187.0019 4039.1374,-10702.1771 4041,-10629.5998 4060.6423,-9864.2269 4090.2052,-8924.4909 4095.2541,-8765.0888"/>
+<polygon fill="#000000" stroke="#000000" points="4097.014,-8764.8021 4095.4233,-8759.7491 4093.5158,-8764.6912 4097.014,-8764.8021"/>
+</g>
+<!-- github.com/ulikunitz/xz/internal/xlog&#45;&gt;io -->
+<g id="edge636" class="edge">
+<title>github.com/ulikunitz/xz/internal/xlog&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3891.6725,-13276.6291C3925.153,-13280.5513 3959.3102,-13291.2922 3983,-13315.5998 4049.0671,-13383.3902 4017.5153,-13645.9 4041,-13737.5998 4052.2974,-13781.7124 4072.8751,-13830.8019 4085.4053,-13858.7637"/>
+<polygon fill="#000000" stroke="#000000" points="4083.845,-13859.5609 4087.4958,-13863.3994 4087.0356,-13858.122 4083.845,-13859.5609"/>
+</g>
+<!-- github.com/ulikunitz/xz/internal/xlog&#45;&gt;sync -->
+<g id="edge639" class="edge">
+<title>github.com/ulikunitz/xz/internal/xlog&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3786.1003,-13264.3842C3812.1153,-13160.3349 3941.117,-12630.3241 3983,-12188.5998 4070.8616,-11261.9558 3996.8139,-4741.3505 4041,-3811.5998 4052.1592,-3576.792 4083.7795,-3293.5692 4093.2724,-3211.752"/>
+<polygon fill="#000000" stroke="#000000" points="4095.0256,-3211.8256 4093.8652,-3206.6568 4091.549,-3211.421 4095.0256,-3211.8256"/>
+</g>
+<!-- github.com/ulikunitz/xz/internal/xlog&#45;&gt;time -->
+<g id="edge640" class="edge">
+<title>github.com/ulikunitz/xz/internal/xlog&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3891.5891,-13285.9795C3925.787,-13281.1278 3960.4004,-13269.0283 3983,-13242.5998 4035.2511,-13181.4962 4034.0495,-11876.6967 4041,-11796.5998 4053.8636,-11648.3607 4081.8081,-11471.5765 4092.0961,-11408.9723"/>
+<polygon fill="#000000" stroke="#000000" points="4093.8346,-11409.1844 4092.9211,-11403.9664 4090.3812,-11408.6152 4093.8346,-11409.1844"/>
+</g>
+<!-- github.com/ulikunitz/xz/internal/xlog&#45;&gt;os -->
+<g id="edge637" class="edge">
+<title>github.com/ulikunitz/xz/internal/xlog&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M3891.6844,-13274.7309C3925.8897,-13278.3436 3960.4833,-13289.3784 3983,-13315.5998 4082.0484,-13430.9448 3964.5049,-18657.209 4041,-18788.5998 4046.302,-18797.7068 4055.2893,-18804.47 4064.4792,-18809.3715"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7372,-18810.9571 4068.9917,-18811.6303 4065.3039,-18807.8273 4063.7372,-18810.9571"/>
+</g>
+<!-- github.com/ulikunitz/xz/internal/xlog&#45;&gt;runtime -->
+<g id="edge638" class="edge">
+<title>github.com/ulikunitz/xz/internal/xlog&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M3891.6231,-13274.7836C3925.8237,-13278.4003 3960.43,-13289.4243 3983,-13315.5998 4054.7492,-13398.8111 4034.3424,-17160.9288 4041,-17270.5998 4052.5274,-17460.4906 4082.6751,-17688.33 4092.7028,-17761.0745"/>
+<polygon fill="#000000" stroke="#000000" points="4091.0263,-17761.7276 4093.4448,-17766.4407 4094.4933,-17761.2481 4091.0263,-17761.7276"/>
+</g>
+<!-- github.com/ulikunitz/xz/lzma&#45;&gt;bufio -->
+<g id="edge641" class="edge">
+<title>github.com/ulikunitz/xz/lzma&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3373.3402,-13365.8484C3411.4792,-13402.5737 3500.929,-13487.617 3580,-13554.5998 3778.3555,-13722.6312 3825.7094,-13768.8966 4041,-13914.5998 4048.3491,-13919.5735 4056.5015,-13924.5766 4064.2294,-13929.1009"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7717,-13930.8583 4068.9752,-13931.8515 4065.5268,-13927.8302 4063.7717,-13930.8583"/>
+</g>
+<!-- github.com/ulikunitz/xz/lzma&#45;&gt;bytes -->
+<g id="edge642" class="edge">
+<title>github.com/ulikunitz/xz/lzma&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3443.6409,-13361.0838C3615.7948,-13387.1991 3977.6705,-13442.4882 3983,-13446.5998 4060.2956,-13506.2318 4085.7562,-13629.7648 4093.2201,-13681.2645"/>
+<polygon fill="#000000" stroke="#000000" points="4091.5001,-13681.6013 4093.9268,-13686.3102 4094.9663,-13681.1157 4091.5001,-13681.6013"/>
+</g>
+<!-- github.com/ulikunitz/xz/lzma&#45;&gt;errors -->
+<g id="edge643" class="edge">
+<title>github.com/ulikunitz/xz/lzma&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3443.6032,-13351.7083C3472.8942,-13348.1735 3502.8279,-13338.3533 3522,-13315.5998 3635.2113,-13181.2407 3514.933,-10316.8035 3580,-10153.5998 3678.3293,-9906.9662 3884.2512,-9959.0658 3983,-9712.5998 4038.1189,-9575.0293 4024.1491,-7192.8404 4041,-7045.5998 4051.2653,-6955.9033 4077.4452,-6851.5097 4089.6089,-6805.909"/>
+<polygon fill="#000000" stroke="#000000" points="4091.3465,-6806.1855 4090.9511,-6800.9029 4087.9659,-6805.2791 4091.3465,-6806.1855"/>
+</g>
+<!-- github.com/ulikunitz/xz/lzma&#45;&gt;fmt -->
+<g id="edge644" class="edge">
+<title>github.com/ulikunitz/xz/lzma&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3443.9231,-13350.6755C3472.7069,-13346.9314 3502.2348,-13337.2423 3522,-13315.5998 3610.6925,-13218.483 3529.7271,-12848.1344 3580,-12726.5998 3688.0434,-12465.4054 3883.609,-12499.2077 3983,-12234.5998 3991.1235,-12212.9727 4086.1741,-9067.4046 4095.2975,-8764.908"/>
+<polygon fill="#000000" stroke="#000000" points="4097.0482,-8764.9073 4095.4498,-8759.8568 4093.5498,-8764.8017 4097.0482,-8764.9073"/>
+</g>
+<!-- github.com/ulikunitz/xz/lzma&#45;&gt;io -->
+<g id="edge647" class="edge">
+<title>github.com/ulikunitz/xz/lzma&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3400.8334,-13365.6786C3538.5283,-13419.7154 3938.4652,-13579.0218 3983,-13623.5998 4000.8582,-13641.4753 4063.5402,-13798.667 4087.0252,-13858.56"/>
+<polygon fill="#000000" stroke="#000000" points="4085.4582,-13859.3578 4088.9111,-13863.3753 4088.7172,-13858.0814 4085.4582,-13859.3578"/>
+</g>
+<!-- github.com/ulikunitz/xz/lzma&#45;&gt;unicode -->
+<g id="edge648" class="edge">
+<title>github.com/ulikunitz/xz/lzma&#45;&gt;unicode</title>
+<path fill="none" stroke="#000000" d="M3443.7531,-13351.8339C3473.0587,-13348.3113 3502.9633,-13338.4667 3522,-13315.5998 3681.4217,-13124.1022 3439.9157,-4551.6654 3580,-4345.5998 3693.3624,-4178.8424 3964.9835,-4145.738 4061.3593,-4139.1989"/>
+<polygon fill="#000000" stroke="#000000" points="4061.4868,-4140.9444 4066.3623,-4138.8728 4061.2591,-4137.4518 4061.4868,-4140.9444"/>
+</g>
+<!-- github.com/ulikunitz/xz/lzma&#45;&gt;github.com/ulikunitz/xz/internal/xlog -->
+<g id="edge646" class="edge">
+<title>github.com/ulikunitz/xz/lzma&#45;&gt;github.com/ulikunitz/xz/internal/xlog</title>
+<path fill="none" stroke="#000000" d="M3443.7532,-13334.0133C3508.4556,-13324.164 3596.5079,-13310.7602 3666.4843,-13300.1081"/>
+<polygon fill="#000000" stroke="#000000" points="3666.8112,-13301.8286 3671.4908,-13299.3459 3666.2844,-13298.3684 3666.8112,-13301.8286"/>
+</g>
+<!-- github.com/ulikunitz/xz/internal/hash -->
+<g id="node135" class="node">
+<title>github.com/ulikunitz/xz/internal/hash</title>
+<g id="a_node135"><a xlink:href="https://godoc.org/github.com/ulikunitz/xz/internal/hash" xlink:title="github.com/ulikunitz/xz/internal/hash" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3880,-13365.5998C3880,-13365.5998 3683,-13365.5998 3683,-13365.5998 3677,-13365.5998 3671,-13359.5998 3671,-13353.5998 3671,-13353.5998 3671,-13341.5998 3671,-13341.5998 3671,-13335.5998 3677,-13329.5998 3683,-13329.5998 3683,-13329.5998 3880,-13329.5998 3880,-13329.5998 3886,-13329.5998 3892,-13335.5998 3892,-13341.5998 3892,-13341.5998 3892,-13353.5998 3892,-13353.5998 3892,-13359.5998 3886,-13365.5998 3880,-13365.5998"/>
+<text text-anchor="middle" x="3781.5" y="-13343.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ulikunitz/xz/internal/hash</text>
+</a>
+</g>
+</g>
+<!-- github.com/ulikunitz/xz/lzma&#45;&gt;github.com/ulikunitz/xz/internal/hash -->
+<g id="edge645" class="edge">
+<title>github.com/ulikunitz/xz/lzma&#45;&gt;github.com/ulikunitz/xz/internal/hash</title>
+<path fill="none" stroke="#000000" d="M3443.7532,-13347.5998C3508.0879,-13347.5998 3595.5079,-13347.5998 3665.2896,-13347.5998"/>
+<polygon fill="#000000" stroke="#000000" points="3665.7069,-13349.3499 3670.7069,-13347.5998 3665.7068,-13345.8499 3665.7069,-13349.3499"/>
+</g>
+<!-- golang.org/x/net/internal/socks&#45;&gt;context -->
+<g id="edge649" class="edge">
+<title>golang.org/x/net/internal/socks&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M3809.3753,-7835.5038C3855.7598,-7803.5258 3947.2212,-7732.069 3983,-7644.5998 4047.7007,-7486.4245 4031.8037,-1663.2487 4041,-1492.5998 4052.2031,-1284.7136 4083.0735,-1034.7296 4092.9314,-958.0753"/>
+<polygon fill="#000000" stroke="#000000" points="4094.7002,-958.0415 4093.6043,-952.8587 4091.2289,-957.5937 4094.7002,-958.0415"/>
+</g>
+<!-- golang.org/x/net/internal/socks&#45;&gt;errors -->
+<g id="edge650" class="edge">
+<title>golang.org/x/net/internal/socks&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3808.2041,-7835.4106C3853.2612,-7803.0329 3943.3416,-7730.6573 3983,-7644.5998 4094.9437,-7401.6855 4000.6756,-7310.0099 4041,-7045.5998 4054.6451,-6956.1278 4079.1997,-6851.2069 4090.2892,-6805.6485"/>
+<polygon fill="#000000" stroke="#000000" points="4092.0237,-6805.9223 4091.5099,-6800.6499 4088.6236,-6805.0919 4092.0237,-6805.9223"/>
+</g>
+<!-- golang.org/x/net/internal/socks&#45;&gt;io -->
+<g id="edge651" class="edge">
+<title>golang.org/x/net/internal/socks&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3786.9591,-7871.7945C3814.7537,-7965.4669 3940.8659,-8403.1995 3983,-8771.5998 4045.7026,-9319.8412 3937.7267,-13195.5344 4041,-13737.5998 4049.5222,-13782.3315 4071.0139,-13831.2171 4084.4691,-13858.9726"/>
+<polygon fill="#000000" stroke="#000000" points="4082.9503,-13859.8503 4086.7196,-13863.5726 4086.0942,-13858.3122 4082.9503,-13859.8503"/>
+</g>
+<!-- golang.org/x/net/internal/socks&#45;&gt;strconv -->
+<g id="edge653" class="edge">
+<title>golang.org/x/net/internal/socks&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3809.0028,-7835.3471C3854.8318,-7803.1354 3945.4872,-7731.3395 3983,-7644.5998 4006.7793,-7589.6156 4085.5982,-5614.5079 4095.0668,-5375.2504"/>
+<polygon fill="#000000" stroke="#000000" points="4096.8284,-5374.9886 4095.2775,-5369.9233 4093.3311,-5374.8502 4096.8284,-5374.9886"/>
+</g>
+<!-- golang.org/x/net/internal/socks&#45;&gt;time -->
+<g id="edge654" class="edge">
+<title>golang.org/x/net/internal/socks&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3786.884,-7871.8033C3814.3078,-7965.5191 3938.8808,-8403.432 3983,-8771.5998 4000.0718,-8914.0612 3968.0754,-11230.0333 4041,-11353.5998 4046.3077,-11362.5934 4055.1951,-11369.3084 4064.2947,-11374.2012"/>
+<polygon fill="#000000" stroke="#000000" points="4063.512,-11375.7664 4068.7641,-11376.4588 4065.0901,-11372.6424 4063.512,-11375.7664"/>
+</g>
+<!-- golang.org/x/net/internal/socks&#45;&gt;net -->
+<g id="edge652" class="edge">
+<title>golang.org/x/net/internal/socks&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M3786.9922,-7871.7907C3814.9502,-7965.4447 3941.7407,-8403.1005 3983,-8771.5998 3995.9339,-8887.1169 3982.7002,-17040.0383 4041,-17140.5998 4046.2853,-17149.7165 4055.2683,-17156.4821 4064.4595,-17161.383"/>
+<polygon fill="#000000" stroke="#000000" points="4063.7183,-17162.969 4068.973,-17163.6412 4065.2845,-17159.8389 4063.7183,-17162.969"/>
+</g>
+</g>
+</svg>
diff --git a/images/crane.png b/images/crane.png
new file mode 100644
index 0000000..ffd95af
--- /dev/null
+++ b/images/crane.png
Binary files differ
diff --git a/images/credhelper-basic.svg b/images/credhelper-basic.svg
new file mode 100644
index 0000000..44d4d0e
--- /dev/null
+++ b/images/credhelper-basic.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg xmlns="http://www.w3.org/2000/svg" width="1948" height="812" xmlns:xlink="http://www.w3.org/1999/xlink"><desc style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Created with Raphaël 2.2.0</desc><defs style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><marker id="raphael-marker-endblock55-objbv9so" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objlilim" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objy0dfc" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objcudj2" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objv9tph" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objl72wc" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-obj7gt9b" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objhtahn" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objay9je" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objaok9x" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objhhdsz" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objv9d0m" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker></defs><rect x="10" y="10" width="346.0625" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="15" y="15" width="336.0625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="183.03125" y="24" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Credential helper flow - Basic auth</tspan></text><rect x="10" y="48" width="58.8125" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="20.203125" y="58" width="38.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="39.40625" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">ggcr</tspan></text><rect x="10" y="754.09375" width="58.8125" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="20.203125" y="764.09375" width="38.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="39.40625" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">ggcr</tspan></text><path fill="none" stroke="#000000" d="M39.40625,86L39.40625,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="578.484375" y="48" width="97.21875" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="588.6875" y="58" width="77.21875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="627.09375" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">registry</tspan></text><rect x="578.484375" y="754.09375" width="97.21875" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="588.6875" y="764.09375" width="77.21875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="627.09375" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">registry</tspan></text><path fill="none" stroke="#000000" d="M627.09375,86L627.09375,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="954.9609375" y="48" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="965.15625" y="58" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="993.96875" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">config</tspan></text><rect x="954.9609375" y="754.09375" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="965.15625" y="764.09375" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="993.96875" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">config</tspan></text><path fill="none" stroke="#000000" d="M993.96875,86L993.96875,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1369.0390625" y="48" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1379.234375" y="58" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1408.046875" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">helper</tspan></text><rect x="1369.0390625" y="754.09375" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1379.234375" y="764.09375" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1408.046875" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">helper</tspan></text><path fill="none" stroke="#000000" d="M1408.046875,86L1408.046875,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1840.515625" y="48" width="77.625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1850.515625" y="58" width="57.625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1879.328125" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">gcloud</tspan></text><rect x="1840.515625" y="754.09375" width="77.625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1850.515625" y="764.09375" width="57.625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1879.328125" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">gcloud</tspan></text><path fill="none" stroke="#000000" d="M1879.328125,86L1879.328125,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="294.84375" y="102" width="76.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="111" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET /v2/</tspan></text><path fill="none" stroke="#000000" d="M39.40625,124C39.40625,124,565.1157077997923,124,622.101791604262,124" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objbv9so)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="73" y="130.390625" width="519.5" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="149" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">401 Unauthorized</tspan><tspan dy="19.2" x="333.25" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Www-Authenticate: Bearer realm="&lt;rlm&gt;",service="&lt;svc&gt;"</tspan></text><path fill="none" stroke="#000000" d="M627.09375,181.21875C627.09375,181.21875,101.38429220020771,181.21875,44.39820839573804,181.21875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objlilim)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="406.265625" y="197.21875" width="220.84375" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="516.6875" y="206.21875" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GetAuthConfig("gcr.io")</tspan></text><path fill="none" stroke="#000000" d="M39.40625,219.21875C39.40625,219.21875,914.81398099754,219.21875,988.9757199272633,219.21875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objy0dfc)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="647.09375" y="239.21875" width="326.875" height="47.21875" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="652.09375" y="244.21875" width="316.875" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="810.53125" y="262.828125" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">~/.docker/config.json:</tspan><tspan dy="19.2" x="810.53125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"credHelpers":{"gcr.io": "gcr"}}</tspan></text><rect x="1004.171875" y="302.4375" width="394.078125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1201.0078125" y="311.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">$ echo gcr.io | docker-credential-gcr get</tspan></text><path fill="none" stroke="#000000" d="M993.96875,324.4375C993.96875,324.4375,1356.106105241226,324.4375,1403.0531110110237,324.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objcudj2)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1422.84375" y="340.4375" width="441.6875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1643.6875" y="349.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">$ gcloud auth print-access-token --format=json</tspan></text><path fill="none" stroke="#000000" d="M1408.046875,362.4375C1408.046875,362.4375,1823.878506992478,362.4375,1874.3346400735963,362.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objv9tph)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1418.046875" y="378.4375" width="451.28125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1643.6875" y="387.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"access_token":"hunter2","token_expiry":"..."}</tspan></text><path fill="none" stroke="#000000" d="M1879.328125,400.4375C1879.328125,400.4375,1463.496493007522,400.4375,1413.0403599264037,400.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objl72wc)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1008.96875" y="416.4375" width="384.078125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1201.0078125" y="425.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"Username":"_token","Secret":"hunter2"}</tspan></text><path fill="none" stroke="#000000" d="M1408.046875,438.4375C1408.046875,438.4375,1045.909519758774,438.4375,998.9625139889764,438.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-obj7gt9b)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="315.046875" y="454.4375" width="403.28125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="516.6875" y="463.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"username":"_token","password":"hunter2"}</tspan></text><path fill="none" stroke="#000000" d="M993.96875,476.4375C993.96875,476.4375,118.56101900245994,476.4375,44.3992800727367,476.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objhtahn)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="59.40625" y="496.4375" width="547.6875" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="64.40625" y="501.4375" width="537.6875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="510.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">note: base64("_token:hunter2") == "X3Rva2VuOmh1bnRlcjI="</tspan></text><rect x="135.40625" y="530.828125" width="395.078125" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="549.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET &lt;rlm&gt;?service=&lt;svc&gt;&amp;scope=...</tspan><tspan dy="19.2" x="333.25" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Authorization: Basic X3Rva2VuOmh1bnRlcjI=</tspan></text><path fill="none" stroke="#000000" d="M39.40625,581.65625C39.40625,581.65625,565.1157077997923,581.65625,622.101791604262,581.65625" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objay9je)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="208.421875" y="588.046875" width="249.65625" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="606.65625" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">200 OK</tspan><tspan dy="19.2" x="333.25" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"token":"&lt;bearer token&gt;"}</tspan></text><path fill="none" stroke="#000000" d="M627.09375,638.875C627.09375,638.875,101.38429220020771,638.875,44.39820839573804,638.875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objaok9x)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="159.421875" y="645.265625" width="346.65625" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="663.875" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET /v2/_catalog</tspan><tspan dy="19.2" x="333.25" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Authorization: Bearer &lt;bearer token&gt;</tspan></text><path fill="none" stroke="#000000" d="M39.40625,696.09375C39.40625,696.09375,565.1157077997923,696.09375,622.101791604262,696.09375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objhhdsz)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="184.421875" y="712.09375" width="297.65625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="721.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"repositories":["foo", "bar"]}</tspan></text><path fill="none" stroke="#000000" d="M627.09375,734.09375C627.09375,734.09375,101.38429220020771,734.09375,44.39820839573804,734.09375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objv9d0m)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path></svg> \ No newline at end of file
diff --git a/images/credhelper-oauth.svg b/images/credhelper-oauth.svg
new file mode 100644
index 0000000..a88e1b8
--- /dev/null
+++ b/images/credhelper-oauth.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg xmlns="http://www.w3.org/2000/svg" width="1763" height="852" xmlns:xlink="http://www.w3.org/1999/xlink"><desc style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Created with Raphaël 2.2.0</desc><defs style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><marker id="raphael-marker-endblock55-objujk3m" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-obj2bf7s" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-obj6hbu7" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objtv5cq" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objcypdu" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objviw31" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objn0c01" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objyeoik" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objdivb3" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objrg426" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker></defs><rect x="10" y="10" width="298.0625" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="15" y="15" width="288.0625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="159.03125" y="24" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Credential helper flow - Oauth</tspan></text><rect x="10" y="48" width="58.8125" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="20.203125" y="58" width="38.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="39.40625" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">ggcr</tspan></text><rect x="10" y="794.484375" width="58.8125" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="20.203125" y="804.484375" width="38.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="39.40625" y="813.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">ggcr</tspan></text><path fill="none" stroke="#000000" d="M39.40625,86L39.40625,794.484375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="586.890625" y="48" width="97.21875" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="597.09375" y="58" width="77.21875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="635.5" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">registry</tspan></text><rect x="586.890625" y="794.484375" width="97.21875" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="597.09375" y="804.484375" width="77.21875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="635.5" y="813.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">registry</tspan></text><path fill="none" stroke="#000000" d="M635.5,86L635.5,794.484375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1193.8046875" y="48" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1204" y="58" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1232.8125" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">config</tspan></text><rect x="1193.8046875" y="794.484375" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1204" y="804.484375" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1232.8125" y="813.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">config</tspan></text><path fill="none" stroke="#000000" d="M1232.8125,86L1232.8125,794.484375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1655.8828125" y="48" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1666.078125" y="58" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1694.890625" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">helper</tspan></text><rect x="1655.8828125" y="794.484375" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1666.078125" y="804.484375" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1694.890625" y="813.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">helper</tspan></text><path fill="none" stroke="#000000" d="M1694.890625,86L1694.890625,794.484375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="299.046875" y="102" width="76.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="111" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET /v2/</tspan></text><path fill="none" stroke="#000000" d="M39.40625,124C39.40625,124,573.0484363525175,124,630.5035643265068,124" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objujk3m)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="77.203125" y="130.390625" width="519.5" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="149" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">401 Unauthorized</tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Www-Authenticate: Bearer realm="&lt;rlm&gt;",service="&lt;svc&gt;"</tspan></text><path fill="none" stroke="#000000" d="M635.5,181.21875C635.5,181.21875,101.85781364748254,181.21875,44.40268567349324,181.21875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-obj2bf7s)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="501.6875" y="197.21875" width="268.84375" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="636.109375" y="206.21875" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GetAuthConfig("example.com")</tspan></text><path fill="none" stroke="#000000" d="M39.40625,219.21875C39.40625,219.21875,1144.2001858446747,219.21875,1227.8149612626066,219.21875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-obj6hbu7)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="837.9375" y="239.21875" width="374.875" height="47.21875" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="842.9375" y="244.21875" width="364.875" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1025.375" y="262.828125" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">~/.docker/config.json:</tspan><tspan dy="19.2" x="1025.375" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"credHelpers":{"example.com": "foo"}}</tspan></text><rect x="1243.015625" y="302.4375" width="442.078125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1463.8515625" y="311.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">$ echo example.com | docker-credential-foo get</tspan></text><path fill="none" stroke="#000000" d="M1232.8125,324.4375C1232.8125,324.4375,1639.9941534968093,324.4375,1689.897764350178,324.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objtv5cq)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1267.015625" y="340.4375" width="393.671875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1463.8515625" y="349.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"Username":"&lt;token&gt;","Secret":"hunter2"}</tspan></text><path fill="none" stroke="#000000" d="M1694.890625,362.4375C1694.890625,362.4375,1287.7089715031907,362.4375,1237.805360649822,362.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objcypdu)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="655.5" y="382.4375" width="557.3125" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="660.5" y="387.4375" width="547.3125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="934.15625" y="396.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">the "&lt;token&gt;" username indicates this is an IdentityToken</tspan></text><rect x="506.484375" y="426.4375" width="259.25" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="636.109375" y="435.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"identitytoken":"hunter2"}</tspan></text><path fill="none" stroke="#000000" d="M1232.8125,448.4375C1232.8125,448.4375,128.0185641553253,448.4375,44.40378873739337,448.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objviw31)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="59.40625" y="468.4375" width="470.875" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="64.40625" y="473.4375" width="460.875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="294.84375" y="482.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">the IdentityToken indicates we should use oauth2</tspan></text><rect x="59.40625" y="516.4375" width="10" height="10" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="0" y="0" width="0" height="0" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="64.40625" y="521.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="521.4375" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></tspan></text><rect x="49.40625" y="513.625" width="576.09375" height="75.609375" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="551.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-22.8046875" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">POST &lt;rlm&gt;</tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"> </tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">service=&lt;svc&gt;&amp;grant_type=refresh_token&amp;refresh_token=hunter2</tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">&amp;client_id=go-containerregistry&amp;scope=...</tspan></text><path fill="none" stroke="#000000" d="M39.40625,622.046875C39.40625,622.046875,573.0484363525175,622.046875,630.5035643265068,622.046875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objn0c01)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="212.625" y="628.4375" width="249.65625" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="647.046875" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">200 OK</tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"token":"&lt;bearer token&gt;"}</tspan></text><path fill="none" stroke="#000000" d="M635.5,679.265625C635.5,679.265625,101.85781364748254,679.265625,44.40268567349324,679.265625" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objyeoik)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="163.625" y="685.65625" width="346.65625" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="704.265625" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET /v2/_catalog</tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Authorization: Bearer &lt;bearer token&gt;</tspan></text><path fill="none" stroke="#000000" d="M39.40625,736.484375C39.40625,736.484375,573.0484363525175,736.484375,630.5035643265068,736.484375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objdivb3)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="188.625" y="752.484375" width="297.65625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="761.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: &quot;Andale Mono&quot;, monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"repositories":["foo", "bar"]}</tspan></text><path fill="none" stroke="#000000" d="M635.5,774.484375C635.5,774.484375,101.85781364748254,774.484375,44.40268567349324,774.484375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objrg426)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path></svg> \ No newline at end of file
diff --git a/images/docker.dot.svg b/images/docker.dot.svg
new file mode 100644
index 0000000..f031ddf
--- /dev/null
+++ b/images/docker.dot.svg
@@ -0,0 +1,2155 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: godep Pages: 1 -->
+<svg width="9165pt" height="1078pt"
+ viewBox="0.00 0.00 9165.00 1078.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1074)">
+<title>godep</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1074 9161,-1074 9161,4 -4,4"/>
+<!-- bufio -->
+<g id="node1" class="node">
+<title>bufio</title>
+<g id="a_node1"><a xlink:href="https://godoc.org/bufio" xlink:title="bufio" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4088,-36C4088,-36 4058,-36 4058,-36 4052,-36 4046,-30 4046,-24 4046,-24 4046,-12 4046,-12 4046,-6 4052,0 4058,0 4058,0 4088,0 4088,0 4094,0 4100,-6 4100,-12 4100,-12 4100,-24 4100,-24 4100,-30 4094,-36 4088,-36"/>
+<text text-anchor="middle" x="4073" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bufio</text>
+</a>
+</g>
+</g>
+<!-- bytes -->
+<g id="node2" class="node">
+<title>bytes</title>
+<g id="a_node2"><a xlink:href="https://godoc.org/bytes" xlink:title="bytes" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M805,-36C805,-36 775,-36 775,-36 769,-36 763,-30 763,-24 763,-24 763,-12 763,-12 763,-6 769,0 775,0 775,0 805,0 805,0 811,0 817,-6 817,-12 817,-12 817,-24 817,-24 817,-30 811,-36 805,-36"/>
+<text text-anchor="middle" x="790" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bytes</text>
+</a>
+</g>
+</g>
+<!-- compress/gzip -->
+<g id="node3" class="node">
+<title>compress/gzip</title>
+<g id="a_node3"><a xlink:href="https://godoc.org/compress/gzip" xlink:title="compress/gzip" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5451.5,-412C5451.5,-412 5380.5,-412 5380.5,-412 5374.5,-412 5368.5,-406 5368.5,-400 5368.5,-400 5368.5,-388 5368.5,-388 5368.5,-382 5374.5,-376 5380.5,-376 5380.5,-376 5451.5,-376 5451.5,-376 5457.5,-376 5463.5,-382 5463.5,-388 5463.5,-388 5463.5,-400 5463.5,-400 5463.5,-406 5457.5,-412 5451.5,-412"/>
+<text text-anchor="middle" x="5416" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">compress/gzip</text>
+</a>
+</g>
+</g>
+<!-- context -->
+<g id="node4" class="node">
+<title>context</title>
+<g id="a_node4"><a xlink:href="https://godoc.org/context" xlink:title="context" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M8369,-224C8369,-224 8337,-224 8337,-224 8331,-224 8325,-218 8325,-212 8325,-212 8325,-200 8325,-200 8325,-194 8331,-188 8337,-188 8337,-188 8369,-188 8369,-188 8375,-188 8381,-194 8381,-200 8381,-200 8381,-212 8381,-212 8381,-218 8375,-224 8369,-224"/>
+<text text-anchor="middle" x="8353" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">context</text>
+</a>
+</g>
+</g>
+<!-- crypto -->
+<g id="node5" class="node">
+<title>crypto</title>
+<g id="a_node5"><a xlink:href="https://godoc.org/crypto" xlink:title="crypto" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M7623,-36C7623,-36 7593,-36 7593,-36 7587,-36 7581,-30 7581,-24 7581,-24 7581,-12 7581,-12 7581,-6 7587,0 7593,0 7593,0 7623,0 7623,0 7629,0 7635,-6 7635,-12 7635,-12 7635,-24 7635,-24 7635,-30 7629,-36 7623,-36"/>
+<text text-anchor="middle" x="7608" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">crypto</text>
+</a>
+</g>
+</g>
+<!-- crypto/tls -->
+<g id="node6" class="node">
+<title>crypto/tls</title>
+<g id="a_node6"><a xlink:href="https://godoc.org/crypto/tls" xlink:title="crypto/tls" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5549,-412C5549,-412 5505,-412 5505,-412 5499,-412 5493,-406 5493,-400 5493,-400 5493,-388 5493,-388 5493,-382 5499,-376 5505,-376 5505,-376 5549,-376 5549,-376 5555,-376 5561,-382 5561,-388 5561,-388 5561,-400 5561,-400 5561,-406 5555,-412 5549,-412"/>
+<text text-anchor="middle" x="5527" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/tls</text>
+</a>
+</g>
+</g>
+<!-- encoding -->
+<g id="node7" class="node">
+<title>encoding</title>
+<g id="a_node7"><a xlink:href="https://godoc.org/encoding" xlink:title="encoding" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2929,-36C2929,-36 2887,-36 2887,-36 2881,-36 2875,-30 2875,-24 2875,-24 2875,-12 2875,-12 2875,-6 2881,0 2887,0 2887,0 2929,0 2929,0 2935,0 2941,-6 2941,-12 2941,-12 2941,-24 2941,-24 2941,-30 2935,-36 2929,-36"/>
+<text text-anchor="middle" x="2908" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding</text>
+</a>
+</g>
+</g>
+<!-- encoding/binary -->
+<g id="node8" class="node">
+<title>encoding/binary</title>
+<g id="a_node8"><a xlink:href="https://godoc.org/encoding/binary" xlink:title="encoding/binary" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1605,-36C1605,-36 1525,-36 1525,-36 1519,-36 1513,-30 1513,-24 1513,-24 1513,-12 1513,-12 1513,-6 1519,0 1525,0 1525,0 1605,0 1605,0 1611,0 1617,-6 1617,-12 1617,-12 1617,-24 1617,-24 1617,-30 1611,-36 1605,-36"/>
+<text text-anchor="middle" x="1565" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/binary</text>
+</a>
+</g>
+</g>
+<!-- encoding/hex -->
+<g id="node9" class="node">
+<title>encoding/hex</title>
+<g id="a_node9"><a xlink:href="https://godoc.org/encoding/hex" xlink:title="encoding/hex" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2647.5,-130C2647.5,-130 2582.5,-130 2582.5,-130 2576.5,-130 2570.5,-124 2570.5,-118 2570.5,-118 2570.5,-106 2570.5,-106 2570.5,-100 2576.5,-94 2582.5,-94 2582.5,-94 2647.5,-94 2647.5,-94 2653.5,-94 2659.5,-100 2659.5,-106 2659.5,-106 2659.5,-118 2659.5,-118 2659.5,-124 2653.5,-130 2647.5,-130"/>
+<text text-anchor="middle" x="2615" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/hex</text>
+</a>
+</g>
+</g>
+<!-- encoding/json -->
+<g id="node10" class="node">
+<title>encoding/json</title>
+<g id="a_node10"><a xlink:href="https://godoc.org/encoding/json" xlink:title="encoding/json" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M352,-36C352,-36 284,-36 284,-36 278,-36 272,-30 272,-24 272,-24 272,-12 272,-12 272,-6 278,0 284,0 284,0 352,0 352,0 358,0 364,-6 364,-12 364,-12 364,-24 364,-24 364,-30 358,-36 352,-36"/>
+<text text-anchor="middle" x="318" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/json</text>
+</a>
+</g>
+</g>
+<!-- errors -->
+<g id="node11" class="node">
+<title>errors</title>
+<g id="a_node11"><a xlink:href="https://godoc.org/errors" xlink:title="errors" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4528,-36C4528,-36 4498,-36 4498,-36 4492,-36 4486,-30 4486,-24 4486,-24 4486,-12 4486,-12 4486,-6 4492,0 4498,0 4498,0 4528,0 4528,0 4534,0 4540,-6 4540,-12 4540,-12 4540,-24 4540,-24 4540,-30 4534,-36 4528,-36"/>
+<text text-anchor="middle" x="4513" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">errors</text>
+</a>
+</g>
+</g>
+<!-- expvar -->
+<g id="node12" class="node">
+<title>expvar</title>
+<g id="a_node12"><a xlink:href="https://godoc.org/expvar" xlink:title="expvar" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3309,-318C3309,-318 3279,-318 3279,-318 3273,-318 3267,-312 3267,-306 3267,-306 3267,-294 3267,-294 3267,-288 3273,-282 3279,-282 3279,-282 3309,-282 3309,-282 3315,-282 3321,-288 3321,-294 3321,-294 3321,-306 3321,-306 3321,-312 3315,-318 3309,-318"/>
+<text text-anchor="middle" x="3294" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">expvar</text>
+</a>
+</g>
+</g>
+<!-- fmt -->
+<g id="node13" class="node">
+<title>fmt</title>
+<g id="a_node13"><a xlink:href="https://godoc.org/fmt" xlink:title="fmt" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M6145,-36C6145,-36 6115,-36 6115,-36 6109,-36 6103,-30 6103,-24 6103,-24 6103,-12 6103,-12 6103,-6 6109,0 6115,0 6115,0 6145,0 6145,0 6151,0 6157,-6 6157,-12 6157,-12 6157,-24 6157,-24 6157,-30 6151,-36 6145,-36"/>
+<text text-anchor="middle" x="6130" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">fmt</text>
+</a>
+</g>
+</g>
+<!-- github.com/beorn7/perks/quantile -->
+<g id="node14" class="node">
+<title>github.com/beorn7/perks/quantile</title>
+<g id="a_node14"><a xlink:href="https://godoc.org/github.com/beorn7/perks/quantile" xlink:title="github.com/beorn7/perks/quantile" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2293,-130C2293,-130 2117,-130 2117,-130 2111,-130 2105,-124 2105,-118 2105,-118 2105,-106 2105,-106 2105,-100 2111,-94 2117,-94 2117,-94 2293,-94 2293,-94 2299,-94 2305,-100 2305,-106 2305,-106 2305,-118 2305,-118 2305,-124 2299,-130 2293,-130"/>
+<text text-anchor="middle" x="2205" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/beorn7/perks/quantile</text>
+</a>
+</g>
+</g>
+<!-- math -->
+<g id="node15" class="node">
+<title>math</title>
+<g id="a_node15"><a xlink:href="https://godoc.org/math" xlink:title="math" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2469,-36C2469,-36 2439,-36 2439,-36 2433,-36 2427,-30 2427,-24 2427,-24 2427,-12 2427,-12 2427,-6 2433,0 2439,0 2439,0 2469,0 2469,0 2475,0 2481,-6 2481,-12 2481,-12 2481,-24 2481,-24 2481,-30 2475,-36 2469,-36"/>
+<text text-anchor="middle" x="2454" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">math</text>
+</a>
+</g>
+</g>
+<!-- github.com/beorn7/perks/quantile&#45;&gt;math -->
+<g id="edge1" class="edge">
+<title>github.com/beorn7/perks/quantile&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M2252.7151,-93.9871C2302.4813,-75.1998 2379.1023,-46.2746 2422.0308,-30.0687"/>
+<polygon fill="#000000" stroke="#000000" points="2422.7808,-31.6562 2426.8405,-28.253 2421.5447,-28.3817 2422.7808,-31.6562"/>
+</g>
+<!-- sort -->
+<g id="node16" class="node">
+<title>sort</title>
+<g id="a_node16"><a xlink:href="https://godoc.org/sort" xlink:title="sort" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2834,-36C2834,-36 2804,-36 2804,-36 2798,-36 2792,-30 2792,-24 2792,-24 2792,-12 2792,-12 2792,-6 2798,0 2804,0 2804,0 2834,0 2834,0 2840,0 2846,-6 2846,-12 2846,-12 2846,-24 2846,-24 2846,-30 2840,-36 2834,-36"/>
+<text text-anchor="middle" x="2819" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sort</text>
+</a>
+</g>
+</g>
+<!-- github.com/beorn7/perks/quantile&#45;&gt;sort -->
+<g id="edge2" class="edge">
+<title>github.com/beorn7/perks/quantile&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M2305.0162,-96.6881C2446.4494,-75.0354 2697.5888,-36.5874 2786.6104,-22.9587"/>
+<polygon fill="#000000" stroke="#000000" points="2787.0578,-24.6607 2791.7353,-22.1741 2786.528,-21.201 2787.0578,-24.6607"/>
+</g>
+<!-- github.com/cespare/xxhash/v2 -->
+<g id="node17" class="node">
+<title>github.com/cespare/xxhash/v2</title>
+<g id="a_node17"><a xlink:href="https://godoc.org/github.com/cespare/xxhash/v2" xlink:title="github.com/cespare/xxhash/v2" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1769,-130C1769,-130 1611,-130 1611,-130 1605,-130 1599,-124 1599,-118 1599,-118 1599,-106 1599,-106 1599,-100 1605,-94 1611,-94 1611,-94 1769,-94 1769,-94 1775,-94 1781,-100 1781,-106 1781,-106 1781,-118 1781,-118 1781,-124 1775,-130 1769,-130"/>
+<text text-anchor="middle" x="1690" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/cespare/xxhash/v2</text>
+</a>
+</g>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;encoding/binary -->
+<g id="edge3" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M1665.8987,-93.8759C1645.1475,-78.2709 1615.1757,-55.7321 1593.2912,-39.275"/>
+<polygon fill="#000000" stroke="#000000" points="1594.0752,-37.675 1589.0273,-36.0685 1591.9716,-40.4723 1594.0752,-37.675"/>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;errors -->
+<g id="edge4" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M1781.3307,-107.4655C1862.3942,-103.542 1984.0619,-97.9012 2090,-94 2989.3152,-60.8822 3214.6866,-69.1684 4114,-36 4250.4202,-30.9686 4412.9886,-23.0305 4480.8084,-19.632"/>
+<polygon fill="#000000" stroke="#000000" points="4481.008,-21.3742 4485.9139,-19.3757 4480.8325,-17.8786 4481.008,-21.3742"/>
+</g>
+<!-- math/bits -->
+<g id="node18" class="node">
+<title>math/bits</title>
+<g id="a_node18"><a xlink:href="https://godoc.org/math/bits" xlink:title="math/bits" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1707,-36C1707,-36 1663,-36 1663,-36 1657,-36 1651,-30 1651,-24 1651,-24 1651,-12 1651,-12 1651,-6 1657,0 1663,0 1663,0 1707,0 1707,0 1713,0 1719,-6 1719,-12 1719,-12 1719,-24 1719,-24 1719,-30 1713,-36 1707,-36"/>
+<text text-anchor="middle" x="1685" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">math/bits</text>
+</a>
+</g>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;math/bits -->
+<g id="edge5" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;math/bits</title>
+<path fill="none" stroke="#000000" d="M1689.0359,-93.8759C1688.2405,-78.9211 1687.1063,-57.5983 1686.2427,-41.3629"/>
+<polygon fill="#000000" stroke="#000000" points="1687.9743,-40.9685 1685.9611,-36.0685 1684.4792,-41.1544 1687.9743,-40.9685"/>
+</g>
+<!-- reflect -->
+<g id="node19" class="node">
+<title>reflect</title>
+<g id="a_node19"><a xlink:href="https://godoc.org/reflect" xlink:title="reflect" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1873,-36C1873,-36 1843,-36 1843,-36 1837,-36 1831,-30 1831,-24 1831,-24 1831,-12 1831,-12 1831,-6 1837,0 1843,0 1843,0 1873,0 1873,0 1879,0 1885,-6 1885,-12 1885,-12 1885,-24 1885,-24 1885,-30 1879,-36 1873,-36"/>
+<text text-anchor="middle" x="1858" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">reflect</text>
+</a>
+</g>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;reflect -->
+<g id="edge6" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M1722.3921,-93.8759C1752.35,-77.1137 1796.6065,-52.3511 1826.3314,-35.7193"/>
+<polygon fill="#000000" stroke="#000000" points="1827.464,-37.091 1830.9729,-33.1223 1825.755,-34.0366 1827.464,-37.091"/>
+</g>
+<!-- unsafe -->
+<g id="node20" class="node">
+<title>unsafe</title>
+<g id="a_node20"><a xlink:href="https://godoc.org/unsafe" xlink:title="unsafe" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1790,-36C1790,-36 1760,-36 1760,-36 1754,-36 1748,-30 1748,-24 1748,-24 1748,-12 1748,-12 1748,-6 1754,0 1760,0 1760,0 1790,0 1790,0 1796,0 1802,-6 1802,-12 1802,-12 1802,-24 1802,-24 1802,-30 1796,-36 1790,-36"/>
+<text text-anchor="middle" x="1775" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">unsafe</text>
+</a>
+</g>
+</g>
+<!-- github.com/cespare/xxhash/v2&#45;&gt;unsafe -->
+<g id="edge7" class="edge">
+<title>github.com/cespare/xxhash/v2&#45;&gt;unsafe</title>
+<path fill="none" stroke="#000000" d="M1706.3889,-93.8759C1720.2645,-78.531 1740.2029,-56.4815 1755.0134,-40.1029"/>
+<polygon fill="#000000" stroke="#000000" points="1756.6059,-40.9509 1758.6615,-36.0685 1754.0099,-38.6034 1756.6059,-40.9509"/>
+</g>
+<!-- github.com/docker/distribution -->
+<g id="node21" class="node">
+<title>github.com/docker/distribution</title>
+<g id="a_node21"><a xlink:href="https://godoc.org/github.com/docker/distribution" xlink:title="github.com/docker/distribution" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M6693.5,-412C6693.5,-412 6532.5,-412 6532.5,-412 6526.5,-412 6520.5,-406 6520.5,-400 6520.5,-400 6520.5,-388 6520.5,-388 6520.5,-382 6526.5,-376 6532.5,-376 6532.5,-376 6693.5,-376 6693.5,-376 6699.5,-376 6705.5,-382 6705.5,-388 6705.5,-388 6705.5,-400 6705.5,-400 6705.5,-406 6699.5,-412 6693.5,-412"/>
+<text text-anchor="middle" x="6613" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;context -->
+<g id="edge8" class="edge">
+<title>github.com/docker/distribution&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M6705.5837,-392.1741C6925.99,-387.0345 7476.5567,-369.3105 7653,-318 7685.4594,-308.5607 7687.6671,-291.8639 7720,-282 7969.0581,-206.0189 8049.0205,-281.4237 8303,-224 8308.5118,-222.7538 8314.2483,-221.0736 8319.7865,-219.2383"/>
+<polygon fill="#000000" stroke="#000000" points="8320.6428,-220.7947 8324.8047,-217.5172 8319.5073,-217.484 8320.6428,-220.7947"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;errors -->
+<g id="edge9" class="edge">
+<title>github.com/docker/distribution&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M6520.4294,-392.4731C6252.9184,-387.4723 5491.5163,-368.8824 5392,-318 5277.2879,-259.348 5321.3097,-155.3174 5208,-94 5091.2424,-30.8168 4666.57,-20.1208 4545.4495,-18.3458"/>
+<polygon fill="#000000" stroke="#000000" points="4545.2752,-16.5933 4540.2509,-18.2724 4545.2257,-20.0929 4545.2752,-16.5933"/>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;fmt -->
+<g id="edge10" class="edge">
+<title>github.com/docker/distribution&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M6616.9833,-375.8966C6621.2183,-353.5354 6626.3718,-314.4714 6619,-282 6598.4911,-191.6622 6602.3936,-150.5235 6529,-94 6471.2677,-49.538 6246.7,-27.3321 6162.3022,-20.4254"/>
+<polygon fill="#000000" stroke="#000000" points="6162.3131,-18.6706 6157.1882,-20.0116 6162.0307,-22.1592 6162.3131,-18.6706"/>
+</g>
+<!-- github.com/docker/distribution/reference -->
+<g id="node22" class="node">
+<title>github.com/docker/distribution/reference</title>
+<g id="a_node22"><a xlink:href="https://godoc.org/github.com/docker/distribution/reference" xlink:title="github.com/docker/distribution/reference" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M7574.5,-318C7574.5,-318 7359.5,-318 7359.5,-318 7353.5,-318 7347.5,-312 7347.5,-306 7347.5,-306 7347.5,-294 7347.5,-294 7347.5,-288 7353.5,-282 7359.5,-282 7359.5,-282 7574.5,-282 7574.5,-282 7580.5,-282 7586.5,-288 7586.5,-294 7586.5,-294 7586.5,-306 7586.5,-306 7586.5,-312 7580.5,-318 7574.5,-318"/>
+<text text-anchor="middle" x="7467" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/reference</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;github.com/docker/distribution/reference -->
+<g id="edge11" class="edge">
+<title>github.com/docker/distribution&#45;&gt;github.com/docker/distribution/reference</title>
+<path fill="none" stroke="#000000" d="M6705.6112,-383.8063C6859.7555,-366.8396 7169.0821,-332.7919 7342.2637,-313.7298"/>
+<polygon fill="#000000" stroke="#000000" points="7342.5689,-315.4568 7347.3474,-313.1702 7342.1859,-311.9778 7342.5689,-315.4568"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest -->
+<g id="node23" class="node">
+<title>github.com/opencontainers/go&#45;digest</title>
+<g id="a_node23"><a xlink:href="https://godoc.org/github.com/opencontainers/go-digest" xlink:title="github.com/opencontainers/go&#45;digest" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M7753,-130C7753,-130 7559,-130 7559,-130 7553,-130 7547,-124 7547,-118 7547,-118 7547,-106 7547,-106 7547,-100 7553,-94 7559,-94 7559,-94 7753,-94 7753,-94 7759,-94 7765,-100 7765,-106 7765,-106 7765,-118 7765,-118 7765,-124 7759,-130 7753,-130"/>
+<text text-anchor="middle" x="7656" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/go&#45;digest</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge12" class="edge">
+<title>github.com/docker/distribution&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M6705.5897,-389.7839C6935.2198,-378.9154 7518.3865,-348.6443 7601,-318 7626.3366,-308.6017 7627.8222,-297.4407 7650,-282 7688.4426,-255.2354 7715.7886,-265.2411 7738,-224 7756.2432,-190.1269 7719.8418,-154.9442 7689.8566,-133.2589"/>
+<polygon fill="#000000" stroke="#000000" points="7690.5771,-131.6244 7685.4864,-130.1591 7688.5522,-134.4792 7690.5771,-131.6244"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="node24" class="node">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<g id="a_node24"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" xlink:title="github.com/opencontainers/image&#45;spec/specs&#45;go/v1" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M6401.5,-224C6401.5,-224 6126.5,-224 6126.5,-224 6120.5,-224 6114.5,-218 6114.5,-212 6114.5,-212 6114.5,-200 6114.5,-200 6114.5,-194 6120.5,-188 6126.5,-188 6126.5,-188 6401.5,-188 6401.5,-188 6407.5,-188 6413.5,-194 6413.5,-200 6413.5,-200 6413.5,-212 6413.5,-212 6413.5,-218 6407.5,-224 6401.5,-224"/>
+<text text-anchor="middle" x="6264" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image&#45;spec/specs&#45;go/v1</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1 -->
+<g id="edge13" class="edge">
+<title>github.com/docker/distribution&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go/v1</title>
+<path fill="none" stroke="#000000" d="M6614.0421,-375.7202C6614.3943,-350.9542 6611.0931,-306.9736 6586,-282 6560.207,-256.3299 6481.0625,-237.4523 6407.6988,-224.932"/>
+<polygon fill="#000000" stroke="#000000" points="6407.649,-223.1488 6402.4274,-224.0415 6407.066,-226.5999 6407.649,-223.1488"/>
+</g>
+<!-- io -->
+<g id="node25" class="node">
+<title>io</title>
+<g id="a_node25"><a xlink:href="https://godoc.org/io" xlink:title="io" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5024,-36C5024,-36 4994,-36 4994,-36 4988,-36 4982,-30 4982,-24 4982,-24 4982,-12 4982,-12 4982,-6 4988,0 4994,0 4994,0 5024,0 5024,0 5030,0 5036,-6 5036,-12 5036,-12 5036,-24 5036,-24 5036,-30 5030,-36 5024,-36"/>
+<text text-anchor="middle" x="5009" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;io -->
+<g id="edge14" class="edge">
+<title>github.com/docker/distribution&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M6605.7455,-375.6873C6594.5383,-350.0333 6570.4296,-304.0688 6534,-282 6367.5556,-181.169 6290.1931,-265.197 6100,-224 6045.9963,-212.3025 6034.7095,-200.9816 5981,-188 5754.8419,-133.3374 5695.2107,-133.9748 5466,-94 5306.797,-66.2347 5115.9782,-35.2359 5041.2458,-23.1835"/>
+<polygon fill="#000000" stroke="#000000" points="5041.2825,-21.4169 5036.0677,-22.3488 5040.7254,-24.8723 5041.2825,-21.4169"/>
+</g>
+<!-- mime -->
+<g id="node26" class="node">
+<title>mime</title>
+<g id="a_node26"><a xlink:href="https://godoc.org/mime" xlink:title="mime" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4599,-224C4599,-224 4569,-224 4569,-224 4563,-224 4557,-218 4557,-212 4557,-212 4557,-200 4557,-200 4557,-194 4563,-188 4569,-188 4569,-188 4599,-188 4599,-188 4605,-188 4611,-194 4611,-200 4611,-200 4611,-212 4611,-212 4611,-218 4605,-224 4599,-224"/>
+<text text-anchor="middle" x="4584" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">mime</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;mime -->
+<g id="edge15" class="edge">
+<title>github.com/docker/distribution&#45;&gt;mime</title>
+<path fill="none" stroke="#000000" d="M6520.3415,-391.2925C6236.3874,-382.6853 5380.8895,-354.3643 5105,-318 4917.6389,-293.3044 4697.572,-236.7125 4616.2775,-214.8478"/>
+<polygon fill="#000000" stroke="#000000" points="4616.4085,-213.0707 4611.1253,-213.4586 4615.4973,-216.45 4616.4085,-213.0707"/>
+</g>
+<!-- net/http -->
+<g id="node27" class="node">
+<title>net/http</title>
+<g id="a_node27"><a xlink:href="https://godoc.org/net/http" xlink:title="net/http" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5955,-224C5955,-224 5921,-224 5921,-224 5915,-224 5909,-218 5909,-212 5909,-212 5909,-200 5909,-200 5909,-194 5915,-188 5921,-188 5921,-188 5955,-188 5955,-188 5961,-188 5967,-194 5967,-200 5967,-200 5967,-212 5967,-212 5967,-218 5961,-224 5955,-224"/>
+<text text-anchor="middle" x="5938" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;net/http -->
+<g id="edge16" class="edge">
+<title>github.com/docker/distribution&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M6520.3804,-386.8659C6427.0148,-377.7546 6279.7073,-358.0894 6158,-318 6148.3047,-314.8064 6031.1713,-254.3237 5971.899,-223.5994"/>
+<polygon fill="#000000" stroke="#000000" points="5972.6316,-222.0081 5967.3872,-221.2603 5971.0206,-225.1153 5972.6316,-222.0081"/>
+</g>
+<!-- strings -->
+<g id="node28" class="node">
+<title>strings</title>
+<g id="a_node28"><a xlink:href="https://godoc.org/strings" xlink:title="strings" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M7075,-36C7075,-36 7045,-36 7045,-36 7039,-36 7033,-30 7033,-24 7033,-24 7033,-12 7033,-12 7033,-6 7039,0 7045,0 7045,0 7075,0 7075,0 7081,0 7087,-6 7087,-12 7087,-12 7087,-24 7087,-24 7087,-30 7081,-36 7075,-36"/>
+<text text-anchor="middle" x="7060" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strings</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;strings -->
+<g id="edge17" class="edge">
+<title>github.com/docker/distribution&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M6635.2487,-375.786C6653.7862,-360.5228 6680.8086,-338.0697 6704,-318 6769.4481,-261.3617 6783.0554,-244.0596 6849,-188 6913.4944,-133.1732 6991.5716,-71.4298 7032.2719,-39.5817"/>
+<polygon fill="#000000" stroke="#000000" points="7033.7087,-40.6797 7036.5695,-36.2212 7031.5527,-37.9225 7033.7087,-40.6797"/>
+</g>
+<!-- time -->
+<g id="node29" class="node">
+<title>time</title>
+<g id="a_node29"><a xlink:href="https://godoc.org/time" xlink:title="time" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M4953,-130C4953,-130 4923,-130 4923,-130 4917,-130 4911,-124 4911,-118 4911,-118 4911,-106 4911,-106 4911,-100 4917,-94 4923,-94 4923,-94 4953,-94 4953,-94 4959,-94 4965,-100 4965,-106 4965,-106 4965,-118 4965,-118 4965,-124 4959,-130 4953,-130"/>
+<text text-anchor="middle" x="4938" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">time</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution&#45;&gt;time -->
+<g id="edge18" class="edge">
+<title>github.com/docker/distribution&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M6520.3333,-391.959C6229.1255,-385.1255 5344.5304,-361.0234 5223,-318 5109.9253,-277.9701 5002.5421,-178.0437 4958.7191,-133.7393"/>
+<polygon fill="#000000" stroke="#000000" points="4959.9293,-132.4741 4955.1751,-130.1373 4957.4344,-134.9288 4959.9293,-132.4741"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;errors -->
+<g id="edge25" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M7406.7341,-281.867C7366.235,-268.5201 7312.3622,-248.4752 7268,-224 7244.6512,-211.1181 7244.7952,-197.8136 7220,-188 7002.1325,-101.7708 6927.2705,-167.6259 6696,-130 6621.0581,-117.8076 6604.3375,-103.4449 6529,-94 5839.6952,-7.5832 5661.1893,-62.6497 4967,-36 4808.8146,-29.9273 4619.6416,-22.3141 4545.2745,-19.3075"/>
+<polygon fill="#000000" stroke="#000000" points="4545.1871,-17.5526 4540.1204,-19.099 4545.0456,-21.0497 4545.1871,-17.5526"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;fmt -->
+<g id="edge26" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M7440.9296,-281.9347C7404.4573,-257.4177 7335.4229,-213.7246 7271,-188 7108.2233,-123.0019 7060.7385,-123.7054 6888,-94 6609.2088,-46.057 6267.1448,-25.174 6162.1526,-19.601"/>
+<polygon fill="#000000" stroke="#000000" points="6162.1245,-17.8472 6157.0394,-19.3318 6161.9404,-21.3423 6162.1245,-17.8472"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge28" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M7565.2656,-281.9741C7625.6485,-268.6737 7694.8229,-248.6404 7713,-224 7734.269,-195.1682 7704.7972,-157.2402 7680.9298,-133.7493"/>
+<polygon fill="#000000" stroke="#000000" points="7681.9689,-132.3201 7677.156,-130.1068 7679.5382,-134.8384 7681.9689,-132.3201"/>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;strings -->
+<g id="edge31" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M7560.865,-281.9369C7625.7529,-264.9496 7694.9391,-234.9962 7662,-188 7644.4288,-162.9301 7432.3958,-102.5517 7403,-94 7290.4969,-61.2712 7153.6983,-34.8667 7092.415,-23.7242"/>
+<polygon fill="#000000" stroke="#000000" points="7092.6596,-21.9901 7087.4278,-22.8208 7092.0358,-25.4341 7092.6596,-21.9901"/>
+</g>
+<!-- github.com/docker/distribution/digestset -->
+<g id="node30" class="node">
+<title>github.com/docker/distribution/digestset</title>
+<g id="a_node30"><a xlink:href="https://godoc.org/github.com/docker/distribution/digestset" xlink:title="github.com/docker/distribution/digestset" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M7635,-224C7635,-224 7423,-224 7423,-224 7417,-224 7411,-218 7411,-212 7411,-212 7411,-200 7411,-200 7411,-194 7417,-188 7423,-188 7423,-188 7635,-188 7635,-188 7641,-188 7647,-194 7647,-200 7647,-200 7647,-212 7647,-212 7647,-218 7641,-224 7635,-224"/>
+<text text-anchor="middle" x="7529" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/digestset</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;github.com/docker/distribution/digestset -->
+<g id="edge27" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;github.com/docker/distribution/digestset</title>
+<path fill="none" stroke="#000000" d="M7478.9542,-281.8759C7488.9895,-266.661 7503.3724,-244.8548 7514.1461,-228.5205"/>
+<polygon fill="#000000" stroke="#000000" points="7515.7903,-229.2059 7517.0825,-224.0685 7512.8686,-227.2788 7515.7903,-229.2059"/>
+</g>
+<!-- path -->
+<g id="node34" class="node">
+<title>path</title>
+<g id="a_node34"><a xlink:href="https://godoc.org/path" xlink:title="path" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M7193,-224C7193,-224 7163,-224 7163,-224 7157,-224 7151,-218 7151,-212 7151,-212 7151,-200 7151,-200 7151,-194 7157,-188 7163,-188 7163,-188 7193,-188 7193,-188 7199,-188 7205,-194 7205,-200 7205,-200 7205,-212 7205,-212 7205,-218 7199,-224 7193,-224"/>
+<text text-anchor="middle" x="7178" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">path</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;path -->
+<g id="edge29" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M7407.0827,-281.8527C7358.3817,-266.9043 7288.0011,-244.8508 7227,-224 7221.4392,-222.0993 7215.5517,-220.0066 7209.8598,-217.9411"/>
+<polygon fill="#000000" stroke="#000000" points="7210.433,-216.2875 7205.136,-216.2173 7209.2331,-219.5754 7210.433,-216.2875"/>
+</g>
+<!-- regexp -->
+<g id="node35" class="node">
+<title>regexp</title>
+<g id="a_node35"><a xlink:href="https://godoc.org/regexp" xlink:title="regexp" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M7824,-36C7824,-36 7794,-36 7794,-36 7788,-36 7782,-30 7782,-24 7782,-24 7782,-12 7782,-12 7782,-6 7788,0 7794,0 7794,0 7824,0 7824,0 7830,0 7836,-6 7836,-12 7836,-12 7836,-24 7836,-24 7836,-30 7830,-36 7824,-36"/>
+<text text-anchor="middle" x="7809" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">regexp</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/reference&#45;&gt;regexp -->
+<g id="edge30" class="edge">
+<title>github.com/docker/distribution/reference&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M7586.7955,-286.8266C7759.5871,-267.5455 8059.1084,-232.9542 8067,-224 8105.2019,-180.6539 8101.6048,-140.2686 8067,-94 8039.73,-57.5384 7904.2853,-32.5244 7841.3035,-22.6762"/>
+<polygon fill="#000000" stroke="#000000" points="7841.3764,-20.9167 7836.1676,-21.8817 7840.8412,-24.3756 7841.3764,-20.9167"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;crypto -->
+<g id="edge140" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;crypto</title>
+<path fill="none" stroke="#000000" d="M7646.7451,-93.8759C7639.0423,-78.7911 7628.0308,-57.227 7619.7143,-40.9405"/>
+<polygon fill="#000000" stroke="#000000" points="7621.059,-39.7257 7617.2265,-36.0685 7617.9418,-41.3174 7621.059,-39.7257"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;fmt -->
+<g id="edge141" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M7546.9353,-103.8141C7503.4128,-100.6263 7452.8431,-97.021 7407,-94 6911.966,-61.3777 6308.5566,-27.8212 6162.8092,-19.7986"/>
+<polygon fill="#000000" stroke="#000000" points="6162.5037,-18.0293 6157.4151,-19.5019 6162.3114,-21.524 6162.5037,-18.0293"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;io -->
+<g id="edge143" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M7546.8837,-108.1251C7085.1022,-91.7263 5303.2835,-28.4506 5041.2535,-19.1454"/>
+<polygon fill="#000000" stroke="#000000" points="5041.1187,-17.3896 5036.0598,-18.9609 5040.9945,-20.8874 5041.1187,-17.3896"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;strings -->
+<g id="edge145" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M7546.6844,-94.759C7408.1067,-72.9027 7177.0514,-36.4611 7092.3736,-23.1059"/>
+<polygon fill="#000000" stroke="#000000" points="7092.4612,-21.3482 7087.2496,-22.2978 7091.9159,-24.8054 7092.4612,-21.3482"/>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;regexp -->
+<g id="edge144" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M7685.4999,-93.8759C7711.8499,-77.687 7750.3462,-54.0356 7777.3359,-37.4537"/>
+<polygon fill="#000000" stroke="#000000" points="7778.5559,-38.7582 7781.9,-34.6497 7776.7237,-35.776 7778.5559,-38.7582"/>
+</g>
+<!-- hash -->
+<g id="node56" class="node">
+<title>hash</title>
+<g id="a_node56"><a xlink:href="https://godoc.org/hash" xlink:title="hash" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M7706,-36C7706,-36 7676,-36 7676,-36 7670,-36 7664,-30 7664,-24 7664,-24 7664,-12 7664,-12 7664,-6 7670,0 7676,0 7676,0 7706,0 7706,0 7712,0 7718,-6 7718,-12 7718,-12 7718,-24 7718,-24 7718,-30 7712,-36 7706,-36"/>
+<text text-anchor="middle" x="7691" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">hash</text>
+</a>
+</g>
+</g>
+<!-- github.com/opencontainers/go&#45;digest&#45;&gt;hash -->
+<g id="edge142" class="edge">
+<title>github.com/opencontainers/go&#45;digest&#45;&gt;hash</title>
+<path fill="none" stroke="#000000" d="M7662.7484,-93.8759C7668.365,-78.7911 7676.3942,-57.227 7682.4583,-40.9405"/>
+<polygon fill="#000000" stroke="#000000" points="7684.1676,-41.3649 7684.2724,-36.0685 7680.8876,-40.1436 7684.1676,-41.3649"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge147" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M6413.6291,-195.8957C6693.6303,-176.9876 7291.0624,-136.6438 7541.6456,-119.7222"/>
+<polygon fill="#000000" stroke="#000000" points="7541.8997,-121.4591 7546.7704,-119.3761 7541.6638,-117.9671 7541.8997,-121.4591"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;time -->
+<g id="edge149" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M6114.4685,-195.3997C5808.5651,-173.7143 5126.8992,-125.391 4970.4381,-114.2995"/>
+<polygon fill="#000000" stroke="#000000" points="4970.2066,-112.5288 4965.0953,-113.9208 4969.959,-116.0201 4970.2066,-112.5288"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go -->
+<g id="node57" class="node">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go</title>
+<g id="a_node57"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go" xlink:title="github.com/opencontainers/image&#45;spec/specs&#45;go" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M6502.5,-130C6502.5,-130 6245.5,-130 6245.5,-130 6239.5,-130 6233.5,-124 6233.5,-118 6233.5,-118 6233.5,-106 6233.5,-106 6233.5,-100 6239.5,-94 6245.5,-94 6245.5,-94 6502.5,-94 6502.5,-94 6508.5,-94 6514.5,-100 6514.5,-106 6514.5,-106 6514.5,-118 6514.5,-118 6514.5,-124 6508.5,-130 6502.5,-130"/>
+<text text-anchor="middle" x="6374" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image&#45;spec/specs&#45;go</text>
+</a>
+</g>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go -->
+<g id="edge148" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go/v1&#45;&gt;github.com/opencontainers/image&#45;spec/specs&#45;go</title>
+<path fill="none" stroke="#000000" d="M6285.2091,-187.8759C6303.318,-172.401 6329.4063,-150.1073 6348.6208,-133.6877"/>
+<polygon fill="#000000" stroke="#000000" points="6350.1917,-134.6472 6352.856,-130.0685 6347.9179,-131.9864 6350.1917,-134.6472"/>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;errors -->
+<g id="edge19" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M7421.6081,-187.9866C7263.771,-162.3142 6959.6262,-115.8923 6699,-94 5931.4937,-29.5303 5736.6948,-64.1455 4967,-36 4808.8038,-30.2152 4619.6369,-22.4385 4545.2729,-19.3478"/>
+<polygon fill="#000000" stroke="#000000" points="4545.1876,-17.5928 4540.1191,-19.1335 4545.0421,-21.0898 4545.1876,-17.5928"/>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;sort -->
+<g id="edge21" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M7410.9367,-197.964C7353.3686,-194.3414 7283.0967,-190.3561 7220,-188 6437.5504,-158.7821 4475.4323,-214.4058 3697,-130 3616.7575,-121.2993 3598.8813,-105.5562 3519,-94 3228.0082,-51.903 3144.8269,-108.9521 2860,-36 2857.176,-35.2767 2854.2995,-34.379 2851.4466,-33.3737"/>
+<polygon fill="#000000" stroke="#000000" points="2851.6494,-31.5806 2846.3532,-31.462 2850.4195,-34.8574 2851.6494,-31.5806"/>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge20" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M7553.4869,-187.8759C7574.658,-172.2059 7605.2754,-149.5442 7627.5336,-133.0696"/>
+<polygon fill="#000000" stroke="#000000" points="7628.6105,-134.4498 7631.5883,-130.0685 7626.5282,-131.6365 7628.6105,-134.4498"/>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;strings -->
+<g id="edge22" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M7505.0949,-187.9664C7471.6114,-163.4861 7408.0883,-119.8354 7348,-94 7259.1775,-55.8101 7146.4764,-32.865 7092.1545,-23.2675"/>
+<polygon fill="#000000" stroke="#000000" points="7092.3094,-21.5181 7087.0826,-22.3803 7091.7062,-24.9658 7092.3094,-21.5181"/>
+</g>
+<!-- sync -->
+<g id="node31" class="node">
+<title>sync</title>
+<g id="a_node31"><a xlink:href="https://godoc.org/sync" xlink:title="sync" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M8558,-36C8558,-36 8528,-36 8528,-36 8522,-36 8516,-30 8516,-24 8516,-24 8516,-12 8516,-12 8516,-6 8522,0 8528,0 8528,0 8558,0 8558,0 8564,0 8570,-6 8570,-12 8570,-12 8570,-24 8570,-24 8570,-30 8564,-36 8558,-36"/>
+<text text-anchor="middle" x="8543" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sync</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/digestset&#45;&gt;sync -->
+<g id="edge23" class="edge">
+<title>github.com/docker/distribution/digestset&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M7588.7356,-187.9155C7690.7637,-157.0685 7889.2006,-97.2691 7905,-94 8134.4217,-46.5297 8416.834,-25.7956 8510.8632,-19.8798"/>
+<polygon fill="#000000" stroke="#000000" points="8511.1111,-21.6178 8515.9925,-19.5601 8510.8933,-18.1246 8511.1111,-21.6178"/>
+</g>
+<!-- github.com/docker/distribution/metrics -->
+<g id="node32" class="node">
+<title>github.com/docker/distribution/metrics</title>
+<g id="a_node32"><a xlink:href="https://godoc.org/github.com/docker/distribution/metrics" xlink:title="github.com/docker/distribution/metrics" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M6019,-694C6019,-694 5813,-694 5813,-694 5807,-694 5801,-688 5801,-682 5801,-682 5801,-670 5801,-670 5801,-664 5807,-658 5813,-658 5813,-658 6019,-658 6019,-658 6025,-658 6031,-664 6031,-670 6031,-670 6031,-682 6031,-682 6031,-688 6025,-694 6019,-694"/>
+<text text-anchor="middle" x="5916" y="-672.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/metrics</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/go&#45;metrics -->
+<g id="node33" class="node">
+<title>github.com/docker/go&#45;metrics</title>
+<g id="a_node33"><a xlink:href="https://godoc.org/github.com/docker/go-metrics" xlink:title="github.com/docker/go&#45;metrics" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M5960,-600C5960,-600 5802,-600 5802,-600 5796,-600 5790,-594 5790,-588 5790,-588 5790,-576 5790,-576 5790,-570 5796,-564 5802,-564 5802,-564 5960,-564 5960,-564 5966,-564 5972,-570 5972,-576 5972,-576 5972,-588 5972,-588 5972,-594 5966,-600 5960,-600"/>
+<text text-anchor="middle" x="5881" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go&#45;metrics</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/metrics&#45;&gt;github.com/docker/go&#45;metrics -->
+<g id="edge24" class="edge">
+<title>github.com/docker/distribution/metrics&#45;&gt;github.com/docker/go&#45;metrics</title>
+<path fill="none" stroke="#000000" d="M5909.2516,-657.8759C5903.635,-642.7911 5895.6058,-621.227 5889.5417,-604.9405"/>
+<polygon fill="#000000" stroke="#000000" points="5891.1124,-604.1436 5887.7276,-600.0685 5887.8324,-605.3649 5891.1124,-604.1436"/>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;fmt -->
+<g id="edge103" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M5874.9505,-563.8632C5855.9294,-503.1349 5803.772,-304.2362 5894,-188 5950.4895,-115.2274 6026.7132,-193.9764 6093,-130 6117.3484,-106.5002 6125.6798,-66.6723 6128.5269,-41.3871"/>
+<polygon fill="#000000" stroke="#000000" points="6130.2872,-41.3755 6129.055,-36.2233 6126.8053,-41.0193 6130.2872,-41.3755"/>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;net/http -->
+<g id="edge106" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M5888.4924,-563.9037C5894.4645,-548.7105 5902.5407,-526.2942 5907,-506 5929.2808,-404.6008 5935.6441,-280.3104 5937.3809,-229.4143"/>
+<polygon fill="#000000" stroke="#000000" points="5939.1386,-229.2022 5937.5528,-224.1478 5935.6405,-229.0879 5939.1386,-229.2022"/>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;time -->
+<g id="edge108" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M5789.9809,-581.3241C5565.3278,-578.734 4998.3975,-566.2356 4940,-506 4839.766,-402.6111 4903.0298,-202.83 4928.6745,-135.1022"/>
+<polygon fill="#000000" stroke="#000000" points="4930.4433,-135.3769 4930.5986,-130.0817 4927.1751,-134.1243 4930.4433,-135.3769"/>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;sync -->
+<g id="edge107" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M5972.1728,-577.0539C6422.8471,-552.1051 8400.7638,-436.5879 8647,-318 8711.0044,-287.1754 8758.5854,-256.566 8740,-188 8727.3367,-141.2822 8719.8703,-127.5705 8685,-94 8652.9727,-63.1665 8605.9543,-41.3172 8574.9198,-29.1858"/>
+<polygon fill="#000000" stroke="#000000" points="8575.3758,-27.4862 8570.0809,-27.3223 8574.1179,-30.7523 8575.3758,-27.4862"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus -->
+<g id="node49" class="node">
+<title>github.com/prometheus/client_golang/prometheus</title>
+<g id="a_node49"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus" xlink:title="github.com/prometheus/client_golang/prometheus" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3024.5,-412C3024.5,-412 2757.5,-412 2757.5,-412 2751.5,-412 2745.5,-406 2745.5,-400 2745.5,-400 2745.5,-388 2745.5,-388 2745.5,-382 2751.5,-376 2757.5,-376 2757.5,-376 3024.5,-376 3024.5,-376 3030.5,-376 3036.5,-382 3036.5,-388 3036.5,-388 3036.5,-400 3036.5,-400 3036.5,-406 3030.5,-412 3024.5,-412"/>
+<text text-anchor="middle" x="2891" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;github.com/prometheus/client_golang/prometheus -->
+<g id="edge104" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;github.com/prometheus/client_golang/prometheus</title>
+<path fill="none" stroke="#000000" d="M5789.8826,-576.2709C5364.8855,-549.5487 3575.558,-437.0424 3041.8689,-403.4861"/>
+<polygon fill="#000000" stroke="#000000" points="3041.9095,-401.7353 3036.8095,-403.168 3041.6898,-405.2284 3041.9095,-401.7353"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp -->
+<g id="node50" class="node">
+<title>github.com/prometheus/client_golang/prometheus/promhttp</title>
+<g id="a_node50"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp" xlink:title="github.com/prometheus/client_golang/prometheus/promhttp" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M5287.5,-506C5287.5,-506 4966.5,-506 4966.5,-506 4960.5,-506 4954.5,-500 4954.5,-494 4954.5,-494 4954.5,-482 4954.5,-482 4954.5,-476 4960.5,-470 4966.5,-470 4966.5,-470 5287.5,-470 5287.5,-470 5293.5,-470 5299.5,-476 5299.5,-482 5299.5,-482 5299.5,-494 5299.5,-494 5299.5,-500 5293.5,-506 5287.5,-506"/>
+<text text-anchor="middle" x="5127" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus/promhttp</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/go&#45;metrics&#45;&gt;github.com/prometheus/client_golang/prometheus/promhttp -->
+<g id="edge105" class="edge">
+<title>github.com/docker/go&#45;metrics&#45;&gt;github.com/prometheus/client_golang/prometheus/promhttp</title>
+<path fill="none" stroke="#000000" d="M5789.6746,-570.6146C5662.3875,-554.7459 5430.0859,-525.7852 5277.1228,-506.7156"/>
+<polygon fill="#000000" stroke="#000000" points="5276.8933,-504.9235 5271.7152,-506.0414 5276.4602,-508.3966 5276.8933,-504.9235"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode -->
+<g id="node36" class="node">
+<title>github.com/docker/distribution/registry/api/errcode</title>
+<g id="a_node36"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/errcode" xlink:title="github.com/docker/distribution/registry/api/errcode" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M6507.5,-318C6507.5,-318 6236.5,-318 6236.5,-318 6230.5,-318 6224.5,-312 6224.5,-306 6224.5,-306 6224.5,-294 6224.5,-294 6224.5,-288 6230.5,-282 6236.5,-282 6236.5,-282 6507.5,-282 6507.5,-282 6513.5,-282 6519.5,-288 6519.5,-294 6519.5,-294 6519.5,-306 6519.5,-306 6519.5,-312 6513.5,-318 6507.5,-318"/>
+<text text-anchor="middle" x="6372" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/errcode</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;encoding/json -->
+<g id="edge32" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M6224.4556,-295.397C6095.7644,-291.4828 5904.0399,-285.8936 5737,-282 4326.481,-249.1217 3973.0793,-272.1797 2563,-224 1728.2195,-195.4772 1508.8369,-273.5672 686,-130 570.035,-109.7666 439.2954,-64.297 369.107,-37.9368"/>
+<polygon fill="#000000" stroke="#000000" points="369.4551,-36.1979 364.1591,-36.0725 368.221,-39.4731 369.4551,-36.1979"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;fmt -->
+<g id="edge33" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M6256.5107,-281.997C6191.5395,-269.2519 6119.5997,-249.8267 6100,-224 6090.3276,-211.2546 6094.282,-202.9434 6100,-188 6112.3726,-155.6653 6139.6274,-162.3347 6152,-130 6163.3056,-100.4539 6151.955,-64.2026 6141.8088,-41.0991"/>
+<polygon fill="#000000" stroke="#000000" points="6143.327,-40.2091 6139.6706,-36.3758 6140.1385,-41.6526 6143.327,-40.2091"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;sort -->
+<g id="edge35" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M6224.4503,-295.6124C6095.7558,-291.8345 5904.0293,-286.3244 5737,-282 5462.0166,-274.8806 3527.015,-297.7127 3262,-224 3230.9304,-215.3581 3227.2259,-203.598 3199,-188 3121.3832,-145.108 3104.1092,-129.8523 3023,-94 2952.6703,-62.9125 2931.4721,-64.3625 2860,-36 2857.0601,-34.8333 2854.0189,-33.5828 2850.9831,-32.3046"/>
+<polygon fill="#000000" stroke="#000000" points="2851.518,-30.6304 2846.2321,-30.2812 2850.1466,-33.8505 2851.518,-30.6304"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;net/http -->
+<g id="edge34" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M6259.1444,-281.9923C6182.3344,-268.6684 6078.7299,-248.6009 5989,-224 5983.523,-222.4984 5977.7871,-220.7015 5972.2204,-218.8328"/>
+<polygon fill="#000000" stroke="#000000" points="5972.4653,-217.0669 5967.168,-217.1028 5971.3315,-220.3782 5972.4653,-217.0669"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;strings -->
+<g id="edge36" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M6416.0841,-281.9306C6544.5215,-229.2862 6916.3574,-76.8768 7027.9606,-31.1324"/>
+<polygon fill="#000000" stroke="#000000" points="7028.801,-32.6793 7032.7638,-29.1637 7027.4736,-29.4408 7028.801,-32.6793"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/errcode&#45;&gt;sync -->
+<g id="edge37" class="edge">
+<title>github.com/docker/distribution/registry/api/errcode&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M6519.5478,-296.2471C6947.2792,-285.0714 8162.7384,-251.1032 8244,-224 8274.3541,-213.876 8444.6474,-90.2251 8513.8046,-39.4997"/>
+<polygon fill="#000000" stroke="#000000" points="8515.2154,-40.635 8518.211,-36.2659 8513.1446,-37.8133 8515.2154,-40.635"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2 -->
+<g id="node37" class="node">
+<title>github.com/docker/distribution/registry/api/v2</title>
+<g id="a_node37"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/v2" xlink:title="github.com/docker/distribution/registry/api/v2" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M7425,-412C7425,-412 7181,-412 7181,-412 7175,-412 7169,-406 7169,-400 7169,-400 7169,-388 7169,-388 7169,-382 7175,-376 7181,-376 7181,-376 7425,-376 7425,-376 7431,-376 7437,-382 7437,-388 7437,-388 7437,-400 7437,-400 7437,-406 7431,-412 7425,-412"/>
+<text text-anchor="middle" x="7303" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/v2</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;fmt -->
+<g id="edge38" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M7225.8536,-375.9462C7178.2851,-363.2081 7116.8636,-343.8086 7066,-318 7028.2642,-298.8526 6797.381,-109.4839 6758,-94 6647.3291,-50.4862 6274.4881,-26.2055 6162.2402,-19.7572"/>
+<polygon fill="#000000" stroke="#000000" points="6162.1857,-18.0013 6157.0941,-19.4636 6161.9863,-21.4957 6162.1857,-18.0013"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/docker/distribution/reference -->
+<g id="edge39" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/docker/distribution/reference</title>
+<path fill="none" stroke="#000000" d="M7334.6208,-375.8759C7362.1868,-360.0759 7402.1544,-337.1676 7430.9536,-320.6607"/>
+<polygon fill="#000000" stroke="#000000" points="7432.0085,-322.0732 7435.4762,-318.0685 7430.268,-319.0367 7432.0085,-322.0732"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge42" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M7437.1439,-384.2844C7564.654,-373.1916 7745.8999,-352.0404 7808,-318 7856.6152,-291.3514 7870.5395,-275.9121 7890,-224 7912.1163,-165.0035 7840.1859,-137.0402 7770.3892,-123.8107"/>
+<polygon fill="#000000" stroke="#000000" points="7770.6036,-122.0708 7765.3689,-122.8835 7769.9678,-125.5126 7770.6036,-122.0708"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;net/http -->
+<g id="edge43" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M7168.9769,-391.9172C6917.8674,-386.8235 6386.0181,-370.0675 6210,-318 6178.3327,-308.6326 6175.0335,-295.7315 6145,-282 6128.7776,-274.583 6026.461,-237.6838 5972.0063,-218.1612"/>
+<polygon fill="#000000" stroke="#000000" points="5972.5075,-216.4819 5967.2102,-216.4423 5971.3267,-219.7767 5972.5075,-216.4819"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;strings -->
+<g id="edge46" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M7305.7882,-375.6878C7312.4671,-324.7617 7324.0582,-179.5889 7255,-94 7214.5487,-43.8657 7136.5994,-26.7209 7092.4921,-20.9175"/>
+<polygon fill="#000000" stroke="#000000" points="7092.4647,-19.1504 7087.2858,-20.2645 7092.0291,-22.6232 7092.4647,-19.1504"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;regexp -->
+<g id="edge45" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M7437.1363,-386.2412C7550.0977,-377.1733 7715.0234,-357.9573 7854,-318 7891.4717,-307.2265 7897.7844,-296.4438 7934,-282 8006.5911,-253.0486 8054.7613,-287.7269 8100,-224 8113.5275,-204.9441 8132.8202,-137.3469 8099,-94 8067.2503,-53.3069 7910.6324,-30.0501 7841.6834,-21.6373"/>
+<polygon fill="#000000" stroke="#000000" points="7841.6504,-19.8707 7836.4769,-21.01 7841.2316,-23.3456 7841.6504,-19.8707"/>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/docker/distribution/registry/api/errcode -->
+<g id="edge40" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/docker/distribution/registry/api/errcode</title>
+<path fill="none" stroke="#000000" d="M7168.6189,-380.432C6997.8745,-363.1925 6703.8999,-333.5108 6524.8004,-315.4278"/>
+<polygon fill="#000000" stroke="#000000" points="6524.95,-313.684 6519.7994,-314.9228 6524.5983,-317.1663 6524.95,-313.684"/>
+</g>
+<!-- github.com/gorilla/mux -->
+<g id="node38" class="node">
+<title>github.com/gorilla/mux</title>
+<g id="a_node38"><a xlink:href="https://godoc.org/github.com/gorilla/mux" xlink:title="github.com/gorilla/mux" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M7214,-318C7214,-318 7092,-318 7092,-318 7086,-318 7080,-312 7080,-306 7080,-306 7080,-294 7080,-294 7080,-288 7086,-282 7092,-282 7092,-282 7214,-282 7214,-282 7220,-282 7226,-288 7226,-294 7226,-294 7226,-306 7226,-306 7226,-312 7220,-318 7214,-318"/>
+<text text-anchor="middle" x="7153" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/gorilla/mux</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/gorilla/mux -->
+<g id="edge41" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;github.com/gorilla/mux</title>
+<path fill="none" stroke="#000000" d="M7274.0785,-375.8759C7248.9695,-360.1409 7212.6107,-337.356 7186.295,-320.8648"/>
+<polygon fill="#000000" stroke="#000000" points="7186.9988,-319.2407 7181.8327,-318.0685 7185.1403,-322.2065 7186.9988,-319.2407"/>
+</g>
+<!-- net/url -->
+<g id="node39" class="node">
+<title>net/url</title>
+<g id="a_node39"><a xlink:href="https://godoc.org/net/url" xlink:title="net/url" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M8688,-224C8688,-224 8658,-224 8658,-224 8652,-224 8646,-218 8646,-212 8646,-212 8646,-200 8646,-200 8646,-194 8652,-188 8658,-188 8658,-188 8688,-188 8688,-188 8694,-188 8700,-194 8700,-200 8700,-200 8700,-212 8700,-212 8700,-218 8694,-224 8688,-224"/>
+<text text-anchor="middle" x="8673" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/url</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;net/url -->
+<g id="edge44" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M7437.1819,-390.1824C7734.4219,-381.0804 8430.0266,-355.9893 8530,-318 8580.5735,-298.7824 8627.5862,-255.021 8653.0971,-228.2438"/>
+<polygon fill="#000000" stroke="#000000" points="8654.545,-229.2592 8656.7029,-224.4212 8651.9989,-226.8576 8654.545,-229.2592"/>
+</g>
+<!-- unicode -->
+<g id="node40" class="node">
+<title>unicode</title>
+<g id="a_node40"><a xlink:href="https://godoc.org/unicode" xlink:title="unicode" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M7781.5,-318C7781.5,-318 7746.5,-318 7746.5,-318 7740.5,-318 7734.5,-312 7734.5,-306 7734.5,-306 7734.5,-294 7734.5,-294 7734.5,-288 7740.5,-282 7746.5,-282 7746.5,-282 7781.5,-282 7781.5,-282 7787.5,-282 7793.5,-288 7793.5,-294 7793.5,-294 7793.5,-306 7793.5,-306 7793.5,-312 7787.5,-318 7781.5,-318"/>
+<text text-anchor="middle" x="7764" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">unicode</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/api/v2&#45;&gt;unicode -->
+<g id="edge47" class="edge">
+<title>github.com/docker/distribution/registry/api/v2&#45;&gt;unicode</title>
+<path fill="none" stroke="#000000" d="M7426.661,-375.963C7508.3498,-362.8409 7617.3897,-343.0149 7712,-318 7717.7003,-316.4928 7723.6745,-314.6662 7729.4592,-312.7617"/>
+<polygon fill="#000000" stroke="#000000" points="7730.0805,-314.3991 7734.2636,-311.1489 7728.9666,-311.0811 7730.0805,-314.3991"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;bytes -->
+<g id="edge126" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M7079.6381,-297.532C6963.7556,-293.709 6731.2343,-286.3443 6534,-282 4812.8911,-244.0905 4381.5502,-281.9636 2661,-224 2636.0772,-223.1604 889.7949,-142.1176 868,-130 832.907,-110.4889 810.0965,-67.7352 798.6842,-41.0574"/>
+<polygon fill="#000000" stroke="#000000" points="800.2275,-40.2116 796.6855,-36.2724 796.9979,-41.5606 800.2275,-40.2116"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;context -->
+<g id="edge127" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M7226.0685,-292.0247C7258.7543,-288.6296 7297.7805,-284.8128 7333,-282 7763.5103,-247.6174 7879.9557,-310.9183 8303,-224 8308.5353,-222.8627 8314.284,-221.2393 8319.8273,-219.4274"/>
+<polygon fill="#000000" stroke="#000000" points="8320.6784,-220.9863 8324.8482,-217.7189 8319.551,-217.6729 8320.6784,-220.9863"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;errors -->
+<g id="edge128" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M7079.6041,-284.5348C6964.7654,-260.8082 6735.4019,-215.3113 6539,-188 5733.2691,-75.9565 4736.2621,-27.8699 4545.3968,-19.3923"/>
+<polygon fill="#000000" stroke="#000000" points="4545.3202,-17.6373 4540.2477,-19.1645 4545.1655,-21.1339 4545.3202,-17.6373"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;fmt -->
+<g id="edge129" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M7132.8775,-281.8135C7106.2627,-258.3179 7057.302,-216.9978 7011,-188 6929.7994,-137.146 6909.0609,-120.5412 6817,-94 6691.1733,-57.7241 6280.5444,-28.0617 6162.1934,-20.1035"/>
+<polygon fill="#000000" stroke="#000000" points="6162.2166,-18.3512 6157.1108,-19.763 6161.9826,-21.8433 6162.2166,-18.3512"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;net/http -->
+<g id="edge130" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M7079.8346,-295.695C6908.4256,-285.3962 6467.5572,-257.6714 6100,-224 6055.864,-219.9568 6005.0962,-214.1234 5972.3315,-210.2038"/>
+<polygon fill="#000000" stroke="#000000" points="5972.4042,-208.45 5967.2313,-209.5917 5971.9871,-211.9251 5972.4042,-208.45"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;strings -->
+<g id="edge135" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M7143.8457,-281.6846C7133.6161,-258.415 7120.513,-217.7127 7137,-188 7160.635,-145.4051 7206.365,-172.5949 7230,-130 7237.7631,-116.0095 7238.704,-107.4254 7230,-94 7199.5355,-47.0102 7132.463,-28.8147 7092.2726,-21.9607"/>
+<polygon fill="#000000" stroke="#000000" points="7092.2442,-20.183 7087.0275,-21.1045 7091.6803,-23.6373 7092.2442,-20.183"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;path -->
+<g id="edge132" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M7157.8203,-281.8759C7161.8322,-266.7911 7167.5673,-245.227 7171.8988,-228.9405"/>
+<polygon fill="#000000" stroke="#000000" points="7173.6006,-229.3504 7173.1945,-224.0685 7170.2181,-228.4507 7173.6006,-229.3504"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;regexp -->
+<g id="edge133" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M7226.1187,-292.5912C7258.8139,-289.3034 7297.8337,-285.4138 7333,-282 7401.4255,-275.3575 7902.7229,-274.839 7949,-224 8005.1702,-162.2926 7897.1084,-76.5632 7840.4939,-38.0903"/>
+<polygon fill="#000000" stroke="#000000" points="7841.3019,-36.5245 7836.1777,-35.1809 7839.3455,-39.4268 7841.3019,-36.5245"/>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;net/url -->
+<g id="edge131" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M7226.0617,-291.9381C7258.7462,-288.5266 7297.7733,-284.721 7333,-282 7804.742,-245.5619 7923.6957,-252.2299 8396,-224 8484.5989,-218.7044 8589.0865,-211.7119 8640.7438,-208.2051"/>
+<polygon fill="#000000" stroke="#000000" points="8641.0065,-209.9414 8645.8764,-207.8563 8640.7692,-206.4494 8641.0065,-209.9414"/>
+</g>
+<!-- strconv -->
+<g id="node47" class="node">
+<title>strconv</title>
+<g id="a_node47"><a xlink:href="https://godoc.org/strconv" xlink:title="strconv" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3806,-36C3806,-36 3774,-36 3774,-36 3768,-36 3762,-30 3762,-24 3762,-24 3762,-12 3762,-12 3762,-6 3768,0 3774,0 3774,0 3806,0 3806,0 3812,0 3818,-6 3818,-12 3818,-12 3818,-24 3818,-24 3818,-30 3812,-36 3806,-36"/>
+<text text-anchor="middle" x="3790" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strconv</text>
+</a>
+</g>
+</g>
+<!-- github.com/gorilla/mux&#45;&gt;strconv -->
+<g id="edge134" class="edge">
+<title>github.com/gorilla/mux&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M7079.529,-295.6976C6963.0673,-287.8192 6729.4433,-267.6956 6536,-224 6486.647,-212.852 6477.6328,-197.8278 6428,-188 6279.8482,-158.6646 5222.734,-103.4232 5072,-94 4609.5186,-65.0876 4493.4025,-66.147 4031,-36 3956.8218,-31.1638 3869.8856,-24.4002 3823.3797,-20.6934"/>
+<polygon fill="#000000" stroke="#000000" points="3823.3094,-18.9323 3818.186,-20.2787 3823.0308,-22.4212 3823.3094,-18.9323"/>
+</g>
+<!-- github.com/docker/distribution/registry/client -->
+<g id="node41" class="node">
+<title>github.com/docker/distribution/registry/client</title>
+<g id="a_node41"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client" xlink:title="github.com/docker/distribution/registry/client" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M6492,-976C6492,-976 6252,-976 6252,-976 6246,-976 6240,-970 6240,-964 6240,-964 6240,-952 6240,-952 6240,-946 6246,-940 6252,-940 6252,-940 6492,-940 6492,-940 6498,-940 6504,-946 6504,-952 6504,-952 6504,-964 6504,-964 6504,-970 6498,-976 6492,-976"/>
+<text text-anchor="middle" x="6372" y="-954.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;bytes -->
+<g id="edge48" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M6239.8841,-956.3358C5854.1248,-950.9445 4733.7722,-931.3616 4371,-882 4246.771,-865.0965 3944.6315,-778.0785 3822,-752 3381.3697,-658.2968 3269.0093,-645.7194 2826,-564 1987.9671,-409.4128 1695.1914,-616.9007 939,-224 862.6637,-184.3373 816.1983,-85.6669 798.3665,-40.8254"/>
+<polygon fill="#000000" stroke="#000000" points="799.9854,-40.16 796.5312,-36.1436 796.7268,-41.4374 799.9854,-40.16"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;context -->
+<g id="edge49" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M6504.3136,-957.1699C6878.0813,-954.112 7924.0437,-940.3796 8067,-882 8136.0575,-853.7987 8192,-844.5939 8192,-770 8192,-770 8192,-770 8192,-582 8192,-442.923 8020.0072,-393.6002 8103,-282 8156.4484,-210.128 8210.4041,-250.3753 8296,-224 8303.8022,-221.5959 8312.1921,-218.9694 8320.0716,-216.4844"/>
+<polygon fill="#000000" stroke="#000000" points="8320.6613,-218.1334 8324.902,-214.9586 8319.6071,-214.796 8320.6613,-218.1334"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;encoding/json -->
+<g id="edge50" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M6239.7777,-956.6625C5798.2479,-951.7432 4391.4245,-932.256 4194,-882 4159.3286,-873.1741 4156.1632,-856.6242 4122,-846 3992.1491,-805.6187 1831.743,-530.5908 1698,-506 1124.0701,-400.4737 942.8355,-426.1212 440,-130 398.3743,-105.4865 358.8378,-64.9081 336.5774,-39.91"/>
+<polygon fill="#000000" stroke="#000000" points="337.7774,-38.6251 333.1554,-36.0366 335.1544,-40.9424 337.7774,-38.6251"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;errors -->
+<g id="edge51" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M6239.8535,-955.7614C5831.7025,-948.4218 4614.0277,-923.1693 4540,-882 4399.0858,-803.6328 4336,-743.2396 4336,-582 4336,-582 4336,-582 4336,-300 4336,-240.8478 4449.134,-96.0307 4494.5043,-40.3328"/>
+<polygon fill="#000000" stroke="#000000" points="4496.1015,-41.1438 4497.9091,-36.1642 4493.3908,-38.9297 4496.1015,-41.1438"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;fmt -->
+<g id="edge52" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M6239.718,-940.1565C6099.7727,-913.4463 5885.6975,-848.452 5786,-694 5724.2614,-598.3541 5796.7871,-278.3846 5866,-188 5896.3072,-148.4221 5919.9177,-156.7212 5962,-130 6010.5701,-99.1592 6065.9174,-61.8265 6099.3867,-39.0035"/>
+<polygon fill="#000000" stroke="#000000" points="6100.6165,-40.283 6103.7599,-36.0191 6098.6435,-37.3921 6100.6165,-40.283"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution -->
+<g id="edge53" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution</title>
+<path fill="none" stroke="#000000" d="M6384.7227,-939.756C6406.1206,-907.2353 6447,-836.4151 6447,-770 6447,-770 6447,-770 6447,-582 6447,-505.0762 6526.0576,-444.7889 6575.0561,-414.8111"/>
+<polygon fill="#000000" stroke="#000000" points="6576.0343,-416.2647 6579.4074,-412.18 6574.2233,-413.2697 6576.0343,-416.2647"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/reference -->
+<g id="edge54" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/reference</title>
+<path fill="none" stroke="#000000" d="M6504.0124,-951.1909C6818.1362,-933.2294 7584,-878.3297 7584,-770 7584,-770 7584,-770 7584,-488 7584,-418.8237 7526.1934,-354.0101 7491.8451,-321.6043"/>
+<polygon fill="#000000" stroke="#000000" points="7493.0377,-320.3235 7488.1878,-318.1924 7490.6501,-322.8828 7493.0377,-320.3235"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge61" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M6504.1012,-955.4191C6923.204,-946.8979 8206.2239,-918.1341 8390,-882 8509.3379,-858.5358 8558.0615,-870.9581 8647,-788 8847.3161,-601.1534 9024.1967,-326.742 8788,-188 8701.8273,-137.3822 8040.813,-119.2558 7770.5715,-113.9172"/>
+<polygon fill="#000000" stroke="#000000" points="7770.474,-112.165 7765.4406,-113.8164 7770.4052,-115.6643 7770.474,-112.165"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;io -->
+<g id="edge62" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M6239.7013,-948.7476C5866.4357,-921.7716 4832,-840.6402 4832,-770 4832,-770 4832,-770 4832,-488 4832,-315.9977 4923.9692,-292.9614 4979,-130 4989.2387,-99.6803 4998.3302,-63.8257 5003.7509,-41.0032"/>
+<polygon fill="#000000" stroke="#000000" points="5005.4729,-41.3247 5004.9167,-36.0566 5002.0663,-40.5218 5005.4729,-41.3247"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;net/http -->
+<g id="edge64" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M6278.8422,-939.9224C6191.7419,-916.9728 6075,-867.9443 6075,-770 6075,-770 6075,-770 6075,-676 6075,-499.4845 6107.9956,-437.7866 6025,-282 6012.7263,-258.9618 5990.4441,-239.7043 5971.5032,-226.3385"/>
+<polygon fill="#000000" stroke="#000000" points="5972.3761,-224.8144 5967.2688,-223.4079 5970.3843,-227.6923 5972.3761,-224.8144"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;strings -->
+<g id="edge67" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M6473.7946,-939.9905C6616.5597,-911.7897 6859,-851.8501 6859,-770 6859,-770 6859,-770 6859,-676 6859,-416.5922 7003.6542,-123.7693 7047.5321,-40.9145"/>
+<polygon fill="#000000" stroke="#000000" points="7049.2289,-41.451 7050.0324,-36.2149 7046.1389,-39.8071 7049.2289,-41.451"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;time -->
+<g id="edge68" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M6239.7523,-952.6864C5875.4117,-937.8978 4879.4085,-896.4037 4810,-882 4671.8691,-853.3351 4513,-911.0738 4513,-770 4513,-770 4513,-770 4513,-582 4513,-347.5489 4807.5411,-177.7259 4906.2189,-127.4101"/>
+<polygon fill="#000000" stroke="#000000" points="4907.099,-128.9261 4910.7678,-125.1047 4905.5167,-125.8041 4907.099,-128.9261"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/api/errcode -->
+<g id="edge55" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/api/errcode</title>
+<path fill="none" stroke="#000000" d="M6372,-939.8515C6372,-906.3085 6372,-832.3403 6372,-770 6372,-770 6372,-770 6372,-488 6372,-428.8254 6372,-359.1739 6372,-323.5645"/>
+<polygon fill="#000000" stroke="#000000" points="6373.7501,-323.1485 6372,-318.1485 6370.2501,-323.1486 6373.7501,-323.1485"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/api/v2 -->
+<g id="edge56" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/api/v2</title>
+<path fill="none" stroke="#000000" d="M6504.2267,-948.694C6632.9091,-937.7931 6817.984,-916.6723 6881,-882 7093.638,-765.0034 7248.6461,-496.4469 7291.2428,-416.7168"/>
+<polygon fill="#000000" stroke="#000000" points="7292.853,-417.416 7293.6542,-412.1795 7289.7624,-415.7734 7292.853,-417.416"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;net/url -->
+<g id="edge65" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M6504.1536,-955.0086C6856.9815,-946.5867 7815.6192,-920.738 8130,-882 8354.2983,-854.362 8626,-995.9946 8626,-770 8626,-770 8626,-770 8626,-394 8626,-332.9136 8649.6433,-263.9063 8663.3914,-228.9434"/>
+<polygon fill="#000000" stroke="#000000" points="8665.1211,-229.3297 8665.344,-224.037 8661.8692,-228.0355 8665.1211,-229.3297"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge -->
+<g id="node42" class="node">
+<title>github.com/docker/distribution/registry/client/auth/challenge</title>
+<g id="a_node42"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/auth/challenge" xlink:title="github.com/docker/distribution/registry/client/auth/challenge" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M8452.5,-318C8452.5,-318 8129.5,-318 8129.5,-318 8123.5,-318 8117.5,-312 8117.5,-306 8117.5,-306 8117.5,-294 8117.5,-294 8117.5,-288 8123.5,-282 8129.5,-282 8129.5,-282 8452.5,-282 8452.5,-282 8458.5,-282 8464.5,-288 8464.5,-294 8464.5,-294 8464.5,-306 8464.5,-306 8464.5,-312 8458.5,-318 8452.5,-318"/>
+<text text-anchor="middle" x="8291" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/auth/challenge</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/client/auth/challenge -->
+<g id="edge57" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/client/auth/challenge</title>
+<path fill="none" stroke="#000000" d="M6504.2726,-956.7173C6867.6837,-952.5076 7863.5619,-936.1179 8001,-882 8072.589,-853.811 8133,-846.939 8133,-770 8133,-770 8133,-770 8133,-582 8133,-472.5579 8224.6906,-366.2088 8268.3743,-321.7517"/>
+<polygon fill="#000000" stroke="#000000" points="8269.6661,-322.9342 8271.9415,-318.1503 8267.1795,-320.4711 8269.6661,-322.9342"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport -->
+<g id="node43" class="node">
+<title>github.com/docker/distribution/registry/client/transport</title>
+<g id="a_node43"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/transport" xlink:title="github.com/docker/distribution/registry/client/transport" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M5711,-318C5711,-318 5419,-318 5419,-318 5413,-318 5407,-312 5407,-306 5407,-306 5407,-294 5407,-294 5407,-288 5413,-282 5419,-282 5419,-282 5711,-282 5711,-282 5717,-282 5723,-288 5723,-294 5723,-294 5723,-306 5723,-306 5723,-312 5717,-318 5711,-318"/>
+<text text-anchor="middle" x="5565" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/transport</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/client/transport -->
+<g id="edge58" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/client/transport</title>
+<path fill="none" stroke="#000000" d="M6239.9089,-950.9787C6063.6676,-940.3024 5769.6297,-917.4076 5735,-882 5577.0197,-720.4713 5804.7226,-568.2348 5686,-376 5670.4155,-350.7657 5642.996,-332.5249 5618.1953,-320.2493"/>
+<polygon fill="#000000" stroke="#000000" points="5618.8615,-318.6277 5613.5983,-318.0263 5617.3377,-321.7786 5618.8615,-318.6277"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache -->
+<g id="node44" class="node">
+<title>github.com/docker/distribution/registry/storage/cache</title>
+<g id="a_node44"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/storage/cache" xlink:title="github.com/docker/distribution/registry/storage/cache" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M6803,-788C6803,-788 6519,-788 6519,-788 6513,-788 6507,-782 6507,-776 6507,-776 6507,-764 6507,-764 6507,-758 6513,-752 6519,-752 6519,-752 6803,-752 6803,-752 6809,-752 6815,-758 6815,-764 6815,-764 6815,-776 6815,-776 6815,-782 6809,-788 6803,-788"/>
+<text text-anchor="middle" x="6661" y="-766.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/storage/cache</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/storage/cache -->
+<g id="edge59" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/storage/cache</title>
+<path fill="none" stroke="#000000" d="M6399.7105,-939.9738C6453.41,-905.0412 6571.3821,-828.2981 6628.6025,-791.0752"/>
+<polygon fill="#000000" stroke="#000000" points="6629.7118,-792.4413 6632.9487,-788.2479 6627.8032,-789.5075 6629.7118,-792.4413"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory -->
+<g id="node45" class="node">
+<title>github.com/docker/distribution/registry/storage/cache/memory</title>
+<g id="a_node45"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/storage/cache/memory" xlink:title="github.com/docker/distribution/registry/storage/cache/memory" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M7974,-882C7974,-882 7640,-882 7640,-882 7634,-882 7628,-876 7628,-870 7628,-870 7628,-858 7628,-858 7628,-852 7634,-846 7640,-846 7640,-846 7974,-846 7974,-846 7980,-846 7986,-852 7986,-858 7986,-858 7986,-870 7986,-870 7986,-876 7980,-882 7974,-882"/>
+<text text-anchor="middle" x="7807" y="-860.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/storage/cache/memory</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/storage/cache/memory -->
+<g id="edge60" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;github.com/docker/distribution/registry/storage/cache/memory</title>
+<path fill="none" stroke="#000000" d="M6504.0265,-951.0763C6729.9011,-938.9606 7204.9576,-912.301 7606,-882 7611.4833,-881.5857 7617.0498,-881.1546 7622.6686,-880.7102"/>
+<polygon fill="#000000" stroke="#000000" points="7622.9972,-882.4396 7627.8426,-880.2984 7622.7195,-878.9507 7622.9972,-882.4396"/>
+</g>
+<!-- io/ioutil -->
+<g id="node46" class="node">
+<title>io/ioutil</title>
+<g id="a_node46"><a xlink:href="https://godoc.org/io/ioutil" xlink:title="io/ioutil" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M194.5,-36C194.5,-36 159.5,-36 159.5,-36 153.5,-36 147.5,-30 147.5,-24 147.5,-24 147.5,-12 147.5,-12 147.5,-6 153.5,0 159.5,0 159.5,0 194.5,0 194.5,0 200.5,0 206.5,-6 206.5,-12 206.5,-12 206.5,-24 206.5,-24 206.5,-30 200.5,-36 194.5,-36"/>
+<text text-anchor="middle" x="177" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io/ioutil</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;io/ioutil -->
+<g id="edge63" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M6239.6462,-955.3823C5821.9617,-946.83 4534.8487,-918.17 4119,-882 2942.2289,-779.646 2652.824,-702.0907 1488,-506 907.5054,-408.2775 633.3783,-547.1145 218,-130 193.9922,-105.8919 183.9124,-66.591 179.7718,-41.5318"/>
+<polygon fill="#000000" stroke="#000000" points="181.4731,-41.0829 178.9755,-36.4113 178.0146,-41.6208 181.4731,-41.0829"/>
+</g>
+<!-- github.com/docker/distribution/registry/client&#45;&gt;strconv -->
+<g id="edge66" class="edge">
+<title>github.com/docker/distribution/registry/client&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M6239.7899,-955.8941C5837.082,-949.0354 4642.7076,-925.2498 4473,-882 4346.6496,-849.7997 4330.4703,-801.8819 4210,-752 4089.4941,-702.1034 4057.9072,-692.0403 3932,-658 3610.2109,-571.0009 2662.4906,-590.8524 2471,-318 2409.8306,-230.8405 2464.2397,-148.0232 2556,-94 2662.1706,-31.4928 3570.5083,-19.9746 3756.724,-18.2638"/>
+<polygon fill="#000000" stroke="#000000" points="3756.7801,-20.0134 3761.7641,-18.2183 3756.7485,-16.5136 3756.7801,-20.0134"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;fmt -->
+<g id="edge80" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M8328.6997,-281.9734C8370.1564,-259.8527 8427.0374,-221.1934 8396,-188 8330.3613,-117.8019 7628.8466,-137.0462 7533,-130 6984.4918,-89.6763 6315.9404,-33.68 6162.4501,-20.742"/>
+<polygon fill="#000000" stroke="#000000" points="6162.3377,-18.9764 6157.2083,-20.3 6162.0436,-22.464 6162.3377,-18.9764"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;net/http -->
+<g id="edge81" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M8117.4714,-293.3943C8025.5029,-289.9284 7910.6227,-285.6546 7808,-282 7048.9325,-254.9678 6857.5643,-278.8683 6100,-224 6055.795,-220.7984 6005.0427,-214.7768 5972.3003,-210.5842"/>
+<polygon fill="#000000" stroke="#000000" points="5972.3863,-208.8309 5967.2037,-209.9278 5971.9391,-212.3023 5972.3863,-208.8309"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;strings -->
+<g id="edge83" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M8354.05,-281.9158C8411.4297,-262.3707 8482.2084,-228.5182 8447,-188 8354.7791,-81.8711 7292.2228,-28.5492 7092.3176,-19.423"/>
+<polygon fill="#000000" stroke="#000000" points="7092.279,-17.6695 7087.2046,-19.1907 7092.12,-21.1659 7092.279,-17.6695"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;sync -->
+<g id="edge84" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M8366.4468,-281.9368C8420.6772,-267.4232 8487.3815,-245.9097 8506,-224 8550.7905,-171.2917 8549.4278,-83.0679 8545.7841,-41.2899"/>
+<polygon fill="#000000" stroke="#000000" points="8547.5153,-41.0061 8545.3075,-36.1907 8544.0305,-41.3319 8547.5153,-41.0061"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;net/url -->
+<g id="edge82" class="edge">
+<title>github.com/docker/distribution/registry/client/auth/challenge&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M8364.2015,-281.9871C8447.6454,-261.4537 8580.3024,-228.8104 8640.9035,-213.8981"/>
+<polygon fill="#000000" stroke="#000000" points="8641.4039,-215.5772 8645.8409,-212.6831 8640.5675,-212.1786 8641.4039,-215.5772"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;errors -->
+<g id="edge85" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M5546.4034,-281.9175C5502.2626,-240.2626 5385.9328,-137.7716 5267,-94 5131.0614,-43.9697 4671.1276,-23.7774 4545.1406,-19.108"/>
+<polygon fill="#000000" stroke="#000000" points="4545.1567,-17.3575 4540.0958,-18.9228 4545.0282,-20.8552 4545.1567,-17.3575"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;fmt -->
+<g id="edge86" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M5599.8075,-281.9812C5642.9358,-259.7053 5718.7196,-220.7271 5784,-188 5899.1652,-130.264 6036.7561,-63.2418 6097.9408,-33.5389"/>
+<polygon fill="#000000" stroke="#000000" points="6098.8315,-35.0519 6102.5655,-31.2942 6097.3031,-31.9032 6098.8315,-35.0519"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;io -->
+<g id="edge87" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M5569.7605,-281.7436C5574.8055,-258.1725 5579.8257,-216.7708 5562,-188 5515.0876,-112.2831 5468.859,-124.0248 5385,-94 5262.1725,-50.023 5107.1537,-28.8474 5041.1337,-21.3284"/>
+<polygon fill="#000000" stroke="#000000" points="5041.3216,-19.5885 5036.1572,-20.7683 5040.9301,-23.0666 5041.3216,-19.5885"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;net/http -->
+<g id="edge88" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M5636.4768,-281.9871C5716.7203,-261.7649 5843.5722,-229.7968 5903.922,-214.588"/>
+<polygon fill="#000000" stroke="#000000" points="5904.4308,-216.2646 5908.8515,-213.3457 5903.5754,-212.8707 5904.4308,-216.2646"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;sync -->
+<g id="edge91" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M5723.1903,-295.3272C6203.886,-281.0077 7620.6665,-237.8697 7719,-224 8033.0535,-179.7034 8401.0472,-64.4253 8510.6514,-28.7091"/>
+<polygon fill="#000000" stroke="#000000" points="8511.4643,-30.2847 8515.6741,-27.0691 8510.3779,-26.9576 8511.4643,-30.2847"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;regexp -->
+<g id="edge89" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M5723.4072,-295.5813C6204.1798,-282.0003 7616.6639,-240.7779 7662,-224 7724.8827,-200.7285 7743.4936,-186.2413 7780,-130 7797.6904,-102.7464 7804.6033,-65.2148 7807.2956,-41.2767"/>
+<polygon fill="#000000" stroke="#000000" points="7809.0585,-41.2436 7807.8368,-36.089 7805.5774,-40.8803 7809.0585,-41.2436"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/transport&#45;&gt;strconv -->
+<g id="edge90" class="edge">
+<title>github.com/docker/distribution/registry/client/transport&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M5525.9351,-281.9789C5433.3327,-240.2232 5192.2472,-137.1327 4979,-94 4565.2575,-10.3138 4452.1505,-64.6096 4031,-36 3956.8353,-30.9618 3869.8939,-24.2758 3823.3836,-20.6359"/>
+<polygon fill="#000000" stroke="#000000" points="3823.3108,-18.8749 3818.1893,-20.2289 3823.0373,-22.3642 3823.3108,-18.8749"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;context -->
+<g id="edge92" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M6716.7375,-751.9582C6894.0245,-694.1415 7460.0546,-506.2278 7916,-318 7950.9192,-303.5843 7957.0604,-293.6407 7993,-282 8123.4401,-239.7508 8163.0666,-257.5904 8296,-224 8303.8321,-222.0209 8312.1714,-219.5811 8319.9854,-217.1437"/>
+<polygon fill="#000000" stroke="#000000" points="8320.5333,-218.806 8324.7736,-215.6306 8319.4786,-215.4687 8320.5333,-218.806"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;fmt -->
+<g id="edge93" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M6676.2996,-751.879C6701.7461,-719.8261 6750,-650.0702 6750,-582 6750,-582 6750,-582 6750,-488 6750,-391.7401 6647.9792,-150.4378 6570,-94 6504.3493,-46.4849 6252.4305,-25.8879 6162.3012,-19.9345"/>
+<polygon fill="#000000" stroke="#000000" points="6162.2136,-18.1751 6157.1103,-19.596 6161.9859,-21.6677 6162.2136,-18.1751"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/docker/distribution -->
+<g id="edge94" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/docker/distribution</title>
+<path fill="none" stroke="#000000" d="M6658.6849,-751.8647C6650.782,-689.9589 6624.8704,-486.9851 6616.0057,-417.5443"/>
+<polygon fill="#000000" stroke="#000000" points="6617.7173,-417.1319 6615.3481,-412.3938 6614.2455,-417.5752 6617.7173,-417.1319"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge96" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M6815.2696,-757.1066C7262.0172,-715.8381 8536.3819,-568.9679 8715,-224 8722.3568,-209.7917 8725.9013,-199.7116 8715,-188 8683.1785,-153.813 8037.7854,-126.1157 7770.4445,-116.0651"/>
+<polygon fill="#000000" stroke="#000000" points="7770.4297,-114.3134 7765.3675,-115.8747 7770.2984,-117.811 7770.4297,-114.3134"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/docker/distribution/metrics -->
+<g id="edge95" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache&#45;&gt;github.com/docker/distribution/metrics</title>
+<path fill="none" stroke="#000000" d="M6518.238,-751.9871C6378.391,-734.3419 6167.6671,-707.754 6036.1521,-691.1601"/>
+<polygon fill="#000000" stroke="#000000" points="6036.2982,-689.4148 6031.1184,-690.525 6035.86,-692.8872 6036.2982,-689.4148"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;context -->
+<g id="edge97" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M7854.1984,-845.9537C7915.5671,-819.0462 8015,-762.1557 8015,-676 8015,-676 8015,-676 8015,-394 8015,-341.5763 8013.377,-317.4473 8052,-282 8134.1223,-206.6301 8188.5611,-253.6914 8296,-224 8303.8692,-221.8253 8312.285,-219.2873 8320.1681,-216.8146"/>
+<polygon fill="#000000" stroke="#000000" points="8320.7584,-218.4635 8324.998,-215.2874 8319.7031,-215.1264 8320.7584,-218.4635"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution -->
+<g id="edge98" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution</title>
+<path fill="none" stroke="#000000" d="M7760.9254,-845.8634C7572.4308,-771.6654 6860.7045,-491.5051 6663.7736,-413.9862"/>
+<polygon fill="#000000" stroke="#000000" points="6664.2308,-412.2856 6658.9373,-412.0825 6662.9488,-415.5424 6664.2308,-412.2856"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution/reference -->
+<g id="edge99" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution/reference</title>
+<path fill="none" stroke="#000000" d="M7802.662,-845.9752C7795.0297,-812.6333 7780,-738.9799 7780,-676 7780,-676 7780,-676 7780,-488 7780,-390.4814 7666.9653,-342.2761 7576.7509,-319.3323"/>
+<polygon fill="#000000" stroke="#000000" points="7576.9349,-317.5745 7571.6596,-318.0579 7576.085,-320.9697 7576.9349,-317.5745"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/opencontainers/go&#45;digest -->
+<g id="edge101" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/opencontainers/go&#45;digest</title>
+<path fill="none" stroke="#000000" d="M7855.3582,-845.8824C7892.8835,-831.3808 7945.9274,-809.9085 7991,-788 8185.9651,-693.233 8902.9006,-484.296 8825,-282 8804.759,-229.4372 8790.6488,-212.6423 8740,-188 8654.4945,-146.3988 8031.3369,-123.1859 7770.4142,-115.2003"/>
+<polygon fill="#000000" stroke="#000000" points="7770.1293,-113.4409 7765.0782,-115.0376 7770.0225,-116.9393 7770.1293,-113.4409"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;sync -->
+<g id="edge102" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M7986.0494,-849.7626C8233.436,-818.823 8666.1215,-720.9409 8815,-412 8878.872,-279.458 8827.647,-197.4216 8723,-94 8681.5532,-53.0385 8614.6777,-32.8282 8575.1646,-23.964"/>
+<polygon fill="#000000" stroke="#000000" points="8575.271,-22.1961 8570.0126,-22.8379 8574.5235,-25.6153 8575.271,-22.1961"/>
+</g>
+<!-- github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution/registry/storage/cache -->
+<g id="edge100" class="edge">
+<title>github.com/docker/distribution/registry/storage/cache/memory&#45;&gt;github.com/docker/distribution/registry/storage/cache</title>
+<path fill="none" stroke="#000000" d="M7627.9375,-849.3125C7406.4702,-831.1468 7032.8284,-800.499 6820.6984,-783.0992"/>
+<polygon fill="#000000" stroke="#000000" points="6820.5322,-781.3297 6815.4058,-782.6651 6820.246,-784.818 6820.5322,-781.3297"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth -->
+<g id="node48" class="node">
+<title>github.com/docker/distribution/registry/client/auth</title>
+<g id="a_node48"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/auth" xlink:title="github.com/docker/distribution/registry/client/auth" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M6505.5,-1070C6505.5,-1070 6238.5,-1070 6238.5,-1070 6232.5,-1070 6226.5,-1064 6226.5,-1058 6226.5,-1058 6226.5,-1046 6226.5,-1046 6226.5,-1040 6232.5,-1034 6238.5,-1034 6238.5,-1034 6505.5,-1034 6505.5,-1034 6511.5,-1034 6517.5,-1040 6517.5,-1046 6517.5,-1046 6517.5,-1058 6517.5,-1058 6517.5,-1064 6511.5,-1070 6505.5,-1070"/>
+<text text-anchor="middle" x="6372" y="-1048.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/auth</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;encoding/json -->
+<g id="edge69" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M6226.1755,-1051.6206C5275.634,-1048.6835 0,-1025.1245 0,-864 0,-864 0,-864 0,-206 0,-144.7254 177.1668,-70.1826 267.0222,-36.3165"/>
+<polygon fill="#000000" stroke="#000000" points="267.694,-37.9336 271.7601,-34.538 266.464,-34.6569 267.694,-37.9336"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;errors -->
+<g id="edge70" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M6226.4501,-1049.7529C5807.3857,-1042.8268 4602.9937,-1019.5954 4211,-976 3943.9943,-946.3051 3617,-1132.6519 3617,-864 3617,-864 3617,-864 3617,-300 3617,-236.4196 3650.7543,-219.4703 3706,-188 3870.8837,-94.0753 3944.4458,-174.1389 4129,-130 4176.5715,-118.6226 4186.2591,-108.4148 4233,-94 4322.0611,-66.5337 4428.6537,-39.0685 4480.9088,-25.9514"/>
+<polygon fill="#000000" stroke="#000000" points="4481.369,-27.6402 4485.7937,-24.7272 4480.5182,-24.2452 4481.369,-27.6402"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;fmt -->
+<g id="edge71" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M6517.6825,-1036.0464C6696.7013,-1012.3037 6977,-959.828 6977,-864 6977,-864 6977,-864 6977,-488 6977,-248.6903 6821.2694,-206.4028 6610,-94 6531.5052,-52.238 6256.9035,-27.7267 6162.3113,-20.3684"/>
+<polygon fill="#000000" stroke="#000000" points="6162.2618,-18.6094 6157.1419,-19.9694 6161.9924,-22.0991 6162.2618,-18.6094"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;net/http -->
+<g id="edge75" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M6228.4133,-1033.9966C6059.5789,-1009.535 5794.7019,-959.7244 5735,-882 5692.3288,-826.4475 5627.2968,-819.4818 5786,-658 5850.8732,-591.991 5930.4124,-673.9983 5986,-600 6072.6954,-484.591 5985.3755,-294.2517 5950.7715,-228.8766"/>
+<polygon fill="#000000" stroke="#000000" points="5952.2735,-227.9742 5948.3742,-224.3883 5949.1863,-229.6231 5952.2735,-227.9742"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;strings -->
+<g id="edge77" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M6517.7587,-1049.4192C7074.9444,-1038.6094 9039,-991.4654 9039,-864 9039,-864 9039,-864 9039,-394 9039,-264.8348 8949.2914,-246.2369 8834,-188 8580.5237,-59.9621 8484.3531,-124.3424 8202,-94 7763.6098,-46.8894 7227.1976,-24.3483 7092.0602,-19.1787"/>
+<polygon fill="#000000" stroke="#000000" points="7092.1102,-17.4294 7087.0472,-18.9879 7091.977,-20.9269 7092.1102,-17.4294"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;time -->
+<g id="edge79" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M6226.3419,-1049.5351C5749.066,-1041.1172 4260.7701,-1012.0232 4226,-976 4215.3452,-964.9612 4210.6564,-910.7306 4231,-846 4272.4144,-714.2246 4395,-720.13 4395,-582 4395,-582 4395,-582 4395,-488 4395,-339.5203 4423.0245,-276.8316 4542,-188 4665.8865,-95.5016 4735.101,-167.8695 4885,-130 4891.757,-128.2929 4898.8878,-126.1296 4905.6525,-123.8997"/>
+<polygon fill="#000000" stroke="#000000" points="4906.5607,-125.4406 4910.7419,-122.1879 4905.4449,-122.1232 4906.5607,-125.4406"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;sync -->
+<g id="edge78" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M6517.8729,-1050.1556C6946.0802,-1044.2445 8194.7998,-1023.3239 8600,-976 8850.8058,-946.7081 9157,-1116.5106 9157,-864 9157,-864 9157,-864 9157,-206 9157,-83.122 8701.7119,-32.4629 8575.2426,-20.7613"/>
+<polygon fill="#000000" stroke="#000000" points="8575.3139,-19.0106 8570.1751,-20.297 8574.9945,-22.496 8575.3139,-19.0106"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;net/url -->
+<g id="edge76" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M6517.6914,-1048.4038C7042.1603,-1034.5115 8803,-979.2855 8803,-864 8803,-864 8803,-864 8803,-676 8803,-498.245 8715.5539,-295.9138 8684.1013,-228.8883"/>
+<polygon fill="#000000" stroke="#000000" points="8685.6519,-228.0735 8681.9354,-224.2985 8682.4866,-229.5671 8685.6519,-228.0735"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;github.com/docker/distribution/registry/client -->
+<g id="edge72" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;github.com/docker/distribution/registry/client</title>
+<path fill="none" stroke="#000000" d="M6372,-1033.8759C6372,-1018.9211 6372,-997.5983 6372,-981.3629"/>
+<polygon fill="#000000" stroke="#000000" points="6373.7501,-981.0685 6372,-976.0685 6370.2501,-981.0685 6373.7501,-981.0685"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;github.com/docker/distribution/registry/client/auth/challenge -->
+<g id="edge73" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;github.com/docker/distribution/registry/client/auth/challenge</title>
+<path fill="none" stroke="#000000" d="M6517.7862,-1046.1637C7008.012,-1025.8865 8570.6629,-955.6994 8648,-882 8745.255,-789.3195 8685,-716.3436 8685,-582 8685,-582 8685,-582 8685,-488 8685,-418.0804 8480.3356,-351.2901 8366.0226,-319.4382"/>
+<polygon fill="#000000" stroke="#000000" points="8366.2712,-317.6912 8360.9853,-318.0409 8365.3356,-321.0638 8366.2712,-317.6912"/>
+</g>
+<!-- github.com/docker/distribution/registry/client/auth&#45;&gt;github.com/docker/distribution/registry/client/transport -->
+<g id="edge74" class="edge">
+<title>github.com/docker/distribution/registry/client/auth&#45;&gt;github.com/docker/distribution/registry/client/transport</title>
+<path fill="none" stroke="#000000" d="M6226.2491,-1049.4608C5753.214,-1040.8781 4289.2423,-1011.4937 4255,-976 4214.8845,-934.4185 4226.1654,-896.0683 4255,-846 4296.5432,-773.8644 4340.2222,-784.1645 4417,-752 4798.2811,-592.2702 4919.9009,-630.7991 5314,-506 5431.5412,-468.7783 5504.1112,-512.877 5575,-412 5593.4147,-385.7953 5584.1195,-347.1783 5575.068,-322.8209"/>
+<polygon fill="#000000" stroke="#000000" points="5576.6985,-322.1851 5573.2692,-318.1475 5573.4321,-323.4424 5576.6985,-322.1851"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;bytes -->
+<g id="edge150" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2745.4271,-391.6258C2355.5569,-384.7021 1307.1407,-362.172 1157,-318 992.7815,-269.6863 853.1326,-101.8235 806.5441,-40.5832"/>
+<polygon fill="#000000" stroke="#000000" points="807.7632,-39.2939 803.3515,-36.3613 804.9715,-41.405 807.7632,-39.2939"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;encoding/json -->
+<g id="edge151" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M2745.3216,-391.4202C2328.5943,-383.5889 1150.6456,-358.1414 981,-318 724.4301,-257.2907 445.5826,-96.2019 351.2761,-38.7324"/>
+<polygon fill="#000000" stroke="#000000" points="352.1048,-37.1879 346.9256,-36.0749 350.2802,-40.1748 352.1048,-37.1879"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;errors -->
+<g id="edge152" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3036.5076,-391.4024C3353.3538,-384.8957 4085.6045,-364.9329 4188,-318 4261.1455,-284.4738 4255.3458,-243.0814 4314,-188 4373.4428,-132.178 4447.3387,-71.0778 4486.212,-39.5322"/>
+<polygon fill="#000000" stroke="#000000" points="4487.5369,-40.7111 4490.3194,-36.2033 4485.3331,-37.9919 4487.5369,-40.7111"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;expvar -->
+<g id="edge153" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;expvar</title>
+<path fill="none" stroke="#000000" d="M3005.0848,-375.9356C3076.8675,-363.1139 3170.9316,-343.6286 3252,-318 3255.1769,-316.9957 3258.4419,-315.8364 3261.6806,-314.6"/>
+<polygon fill="#000000" stroke="#000000" points="3262.7242,-316.0694 3266.7317,-312.6049 3261.4384,-312.8141 3262.7242,-316.0694"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;fmt -->
+<g id="edge154" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3036.5667,-388.6742C3345.4868,-376.9575 4058.0795,-347.6222 4299,-318 4820.1553,-253.9217 4939.9559,-173.3854 5459,-94 5703.4996,-56.6049 6000.7317,-29.2286 6097.722,-20.7556"/>
+<polygon fill="#000000" stroke="#000000" points="6097.9052,-22.4963 6102.7345,-20.319 6097.6014,-19.0095 6097.9052,-22.4963"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/beorn7/perks/quantile -->
+<g id="edge155" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/beorn7/perks/quantile</title>
+<path fill="none" stroke="#000000" d="M2745.3707,-388.6421C2601.5313,-380.8604 2393.6688,-362.1598 2327,-318 2259.9325,-273.5761 2224.5427,-178.5804 2211.2661,-134.8706"/>
+<polygon fill="#000000" stroke="#000000" points="2212.9349,-134.3427 2209.8291,-130.0512 2209.5809,-135.3429 2212.9349,-134.3427"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;math -->
+<g id="edge164" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M2745.3689,-385.9116C2592.2664,-375.6295 2362.7409,-354.6524 2283,-318 2170.0775,-266.0959 2126.3053,-243.6438 2076,-130 2069.5236,-115.3693 2065.8625,-106.3786 2076,-94 2119.7313,-40.6011 2338.2982,-23.9158 2421.6431,-19.4461"/>
+<polygon fill="#000000" stroke="#000000" points="2422.0324,-21.1782 2426.934,-19.1691 2421.8493,-17.683 2422.0324,-21.1782"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;sort -->
+<g id="edge169" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M2745.3091,-387.0458C2638.3123,-378.5263 2505.7965,-359.6581 2471,-318 2426.3632,-264.5612 2348.4779,-264.4865 2520,-94 2557.7633,-56.4647 2717.2181,-31.3912 2786.5652,-22.0631"/>
+<polygon fill="#000000" stroke="#000000" points="2787.0727,-23.7611 2791.7979,-21.3662 2786.6106,-20.2917 2787.0727,-23.7611"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/cespare/xxhash/v2 -->
+<g id="edge156" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/cespare/xxhash/v2</title>
+<path fill="none" stroke="#000000" d="M2745.4167,-391.5264C2467.1661,-385.6853 1880.4694,-367.5906 1803,-318 1736.3431,-275.3309 1705.8161,-179.0106 1694.9419,-134.8733"/>
+<polygon fill="#000000" stroke="#000000" points="1696.6426,-134.4605 1693.7712,-130.0088 1693.2397,-135.2795 1696.6426,-134.4605"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;strings -->
+<g id="edge170" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3036.5781,-390.5969C3311.8787,-383.3359 3927.5157,-363.1603 4444,-318 5236.1236,-248.7384 5428.4722,-179.5735 6219,-94 6533.5648,-59.9488 6915.4791,-29.2948 7027.6044,-20.512"/>
+<polygon fill="#000000" stroke="#000000" points="7027.8902,-22.2451 7032.7384,-20.1103 7027.6171,-18.7557 7027.8902,-22.2451"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;time -->
+<g id="edge173" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3036.5137,-389.2497C3361.4352,-378.1464 4126.4478,-349.0702 4240,-318 4300.5879,-301.4219 4431.218,-207.2832 4491,-188 4659.4519,-133.6643 4712.9402,-171.519 4885,-130 4891.7749,-128.3652 4898.9134,-126.2336 4905.6802,-124.012"/>
+<polygon fill="#000000" stroke="#000000" points="4906.5877,-125.5533 4910.7702,-122.3022 4905.4732,-122.2355 4906.5877,-125.5533"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;sync -->
+<g id="edge171" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3036.5866,-392.9642C3909.88,-386.6378 8417.8914,-352.3377 8479,-318 8559.4284,-272.8062 8553.2478,-221.2255 8567,-130 8569.385,-114.1788 8569.4375,-109.8132 8567,-94 8564.1782,-75.6934 8557.7449,-55.7508 8552.2487,-40.889"/>
+<polygon fill="#000000" stroke="#000000" points="8553.8275,-40.1156 8550.4273,-36.0534 8550.5521,-41.3493 8553.8275,-40.1156"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;io/ioutil -->
+<g id="edge163" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2745.3098,-392.9164C2342.9486,-389.2287 1222.137,-374.0765 858,-318 582.6435,-275.5955 483.0544,-296.9314 260,-130 227.7133,-105.837 202.293,-65.6788 188.4792,-40.5848"/>
+<polygon fill="#000000" stroke="#000000" points="189.9587,-39.6419 186.0359,-36.0817 186.8823,-41.3111 189.9587,-39.6419"/>
+</g>
+<!-- github.com/golang/protobuf/proto -->
+<g id="node51" class="node">
+<title>github.com/golang/protobuf/proto</title>
+<g id="a_node51"><a xlink:href="https://godoc.org/github.com/golang/protobuf/proto" xlink:title="github.com/golang/protobuf/proto" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2997,-130C2997,-130 2819,-130 2819,-130 2813,-130 2807,-124 2807,-118 2807,-118 2807,-106 2807,-106 2807,-100 2813,-94 2819,-94 2819,-94 2997,-94 2997,-94 3003,-94 3009,-100 3009,-106 3009,-106 3009,-118 3009,-118 3009,-124 3003,-130 2997,-130"/>
+<text text-anchor="middle" x="2908" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/proto</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge157" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M2745.3403,-385.0275C2646.2783,-375.5324 2527.658,-356.3298 2496,-318 2403.268,-205.7251 2650.9011,-148.9478 2801.6176,-125.522"/>
+<polygon fill="#000000" stroke="#000000" points="2802.2621,-127.1935 2806.9374,-124.7027 2801.7293,-123.7343 2802.2621,-127.1935"/>
+</g>
+<!-- sync/atomic -->
+<g id="node53" class="node">
+<title>sync/atomic</title>
+<g id="a_node53"><a xlink:href="https://godoc.org/sync/atomic" xlink:title="sync/atomic" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1985.5,-36C1985.5,-36 1926.5,-36 1926.5,-36 1920.5,-36 1914.5,-30 1914.5,-24 1914.5,-24 1914.5,-12 1914.5,-12 1914.5,-6 1920.5,0 1926.5,0 1926.5,0 1985.5,0 1985.5,0 1991.5,0 1997.5,-6 1997.5,-12 1997.5,-12 1997.5,-24 1997.5,-24 1997.5,-30 1991.5,-36 1985.5,-36"/>
+<text text-anchor="middle" x="1956" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sync/atomic</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;sync/atomic -->
+<g id="edge172" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;sync/atomic</title>
+<path fill="none" stroke="#000000" d="M2745.2362,-385.8993C2540.5555,-373.5308 2187.1282,-348.1677 2136,-318 2028.602,-254.6308 1977.6961,-99.9561 1961.7458,-41.18"/>
+<polygon fill="#000000" stroke="#000000" points="1963.377,-40.5049 1960.3966,-36.1254 1959.9954,-41.4076 1963.377,-40.5049"/>
+</g>
+<!-- unicode/utf8 -->
+<g id="node54" class="node">
+<title>unicode/utf8</title>
+<g id="a_node54"><a xlink:href="https://godoc.org/unicode/utf8" xlink:title="unicode/utf8" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2099.5,-36C2099.5,-36 2038.5,-36 2038.5,-36 2032.5,-36 2026.5,-30 2026.5,-24 2026.5,-24 2026.5,-12 2026.5,-12 2026.5,-6 2032.5,0 2038.5,0 2038.5,0 2099.5,0 2099.5,0 2105.5,0 2111.5,-6 2111.5,-12 2111.5,-12 2111.5,-24 2111.5,-24 2111.5,-30 2105.5,-36 2099.5,-36"/>
+<text text-anchor="middle" x="2069" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">unicode/utf8</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;unicode/utf8 -->
+<g id="edge174" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M2745.2736,-388.2527C2573.1949,-379.6674 2298.5362,-359.7996 2205,-318 2109.3999,-275.2781 2082.5182,-232.8765 2063,-130 2057.2297,-99.5861 2060.94,-63.7543 2064.5813,-40.9626"/>
+<polygon fill="#000000" stroke="#000000" points="2066.3087,-41.2424 2065.4021,-36.0231 2062.856,-40.6686 2066.3087,-41.2424"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/internal -->
+<g id="node58" class="node">
+<title>github.com/prometheus/client_golang/prometheus/internal</title>
+<g id="a_node58"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus/internal" xlink:title="github.com/prometheus/client_golang/prometheus/internal" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2835,-318C2835,-318 2523,-318 2523,-318 2517,-318 2511,-312 2511,-306 2511,-306 2511,-294 2511,-294 2511,-288 2517,-282 2523,-282 2523,-282 2835,-282 2835,-282 2841,-282 2847,-288 2847,-294 2847,-294 2847,-306 2847,-306 2847,-312 2841,-318 2835,-318"/>
+<text text-anchor="middle" x="2679" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus/internal</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/client_golang/prometheus/internal -->
+<g id="edge158" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/client_golang/prometheus/internal</title>
+<path fill="none" stroke="#000000" d="M2850.1243,-375.8759C2814.1969,-359.9458 2761.9732,-336.79 2724.6803,-320.2545"/>
+<polygon fill="#000000" stroke="#000000" points="2725.0304,-318.4955 2719.7502,-318.0685 2723.6117,-321.695 2725.0304,-318.4955"/>
+</g>
+<!-- github.com/prometheus/client_model/go -->
+<g id="node59" class="node">
+<title>github.com/prometheus/client_model/go</title>
+<g id="a_node59"><a xlink:href="https://godoc.org/github.com/prometheus/client_model/go" xlink:title="github.com/prometheus/client_model/go" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3116.5,-224C3116.5,-224 2901.5,-224 2901.5,-224 2895.5,-224 2889.5,-218 2889.5,-212 2889.5,-212 2889.5,-200 2889.5,-200 2889.5,-194 2895.5,-188 2901.5,-188 2901.5,-188 3116.5,-188 3116.5,-188 3122.5,-188 3128.5,-194 3128.5,-200 3128.5,-200 3128.5,-212 3128.5,-212 3128.5,-218 3122.5,-224 3116.5,-224"/>
+<text text-anchor="middle" x="3009" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_model/go</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/client_model/go -->
+<g id="edge159" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/client_model/go</title>
+<path fill="none" stroke="#000000" d="M2902.3143,-375.9738C2923.9325,-341.5313 2971.0624,-266.443 2994.7742,-228.6648"/>
+<polygon fill="#000000" stroke="#000000" points="2996.3706,-229.4132 2997.5465,-224.2479 2993.4062,-227.5525 2996.3706,-229.4132"/>
+</g>
+<!-- github.com/prometheus/common/expfmt -->
+<g id="node60" class="node">
+<title>github.com/prometheus/common/expfmt</title>
+<g id="a_node60"><a xlink:href="https://godoc.org/github.com/prometheus/common/expfmt" xlink:title="github.com/prometheus/common/expfmt" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M4162,-318C4162,-318 3944,-318 3944,-318 3938,-318 3932,-312 3932,-306 3932,-306 3932,-294 3932,-294 3932,-288 3938,-282 3944,-282 3944,-282 4162,-282 4162,-282 4168,-282 4174,-288 4174,-294 4174,-294 4174,-306 4174,-306 4174,-312 4168,-318 4162,-318"/>
+<text text-anchor="middle" x="4053" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/expfmt</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/common/expfmt -->
+<g id="edge160" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/common/expfmt</title>
+<path fill="none" stroke="#000000" d="M3036.6979,-382.2138C3266.5591,-363.6191 3709.4975,-327.7876 3926.6805,-310.2186"/>
+<polygon fill="#000000" stroke="#000000" points="3926.926,-311.9546 3931.7686,-309.807 3926.6437,-308.466 3926.926,-311.9546"/>
+</g>
+<!-- github.com/prometheus/common/model -->
+<g id="node61" class="node">
+<title>github.com/prometheus/common/model</title>
+<g id="a_node61"><a xlink:href="https://godoc.org/github.com/prometheus/common/model" xlink:title="github.com/prometheus/common/model" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M3501.5,-224C3501.5,-224 3288.5,-224 3288.5,-224 3282.5,-224 3276.5,-218 3276.5,-212 3276.5,-212 3276.5,-200 3276.5,-200 3276.5,-194 3282.5,-188 3288.5,-188 3288.5,-188 3501.5,-188 3501.5,-188 3507.5,-188 3513.5,-194 3513.5,-200 3513.5,-200 3513.5,-212 3513.5,-212 3513.5,-218 3507.5,-224 3501.5,-224"/>
+<text text-anchor="middle" x="3395" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/model</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/common/model -->
+<g id="edge161" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/common/model</title>
+<path fill="none" stroke="#000000" d="M2909.3201,-375.7891C2935.6648,-350.6881 2986.843,-305.8615 3039,-282 3112.24,-248.4931 3200.7845,-229.5124 3271.2453,-218.8988"/>
+<polygon fill="#000000" stroke="#000000" points="3271.5986,-220.6156 3276.2868,-218.149 3271.0837,-217.1536 3271.5986,-220.6156"/>
+</g>
+<!-- github.com/prometheus/procfs -->
+<g id="node62" class="node">
+<title>github.com/prometheus/procfs</title>
+<g id="a_node62"><a xlink:href="https://godoc.org/github.com/prometheus/procfs" xlink:title="github.com/prometheus/procfs" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2848,-224C2848,-224 2688,-224 2688,-224 2682,-224 2676,-218 2676,-212 2676,-212 2676,-200 2676,-200 2676,-194 2682,-188 2688,-188 2688,-188 2848,-188 2848,-188 2854,-188 2860,-194 2860,-200 2860,-200 2860,-212 2860,-212 2860,-218 2854,-224 2848,-224"/>
+<text text-anchor="middle" x="2768" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/procfs -->
+<g id="edge162" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;github.com/prometheus/procfs</title>
+<path fill="none" stroke="#000000" d="M2889.589,-375.943C2887.0373,-352.5875 2880.0097,-311.4186 2861,-282 2846.4811,-259.531 2823.1568,-240.4949 2803.4995,-227.0942"/>
+<polygon fill="#000000" stroke="#000000" points="2804.2356,-225.4809 2799.1077,-224.1513 2802.2873,-228.3885 2804.2356,-225.4809"/>
+</g>
+<!-- os -->
+<g id="node63" class="node">
+<title>os</title>
+<g id="a_node63"><a xlink:href="https://godoc.org/os" xlink:title="os" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M935,-36C935,-36 905,-36 905,-36 899,-36 893,-30 893,-24 893,-24 893,-12 893,-12 893,-6 899,0 905,0 905,0 935,0 935,0 941,0 947,-6 947,-12 947,-12 947,-24 947,-24 947,-30 941,-36 935,-36"/>
+<text text-anchor="middle" x="920" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">os</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;os -->
+<g id="edge165" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2745.2218,-391.5894C2528.5925,-386.3475 2109.3667,-369.8191 1757,-318 1386.65,-263.5362 1189.455,-405.4741 936,-130 914.0236,-106.1144 913.7518,-66.3988 916.3211,-41.2416"/>
+<polygon fill="#000000" stroke="#000000" points="918.0789,-41.2711 916.9056,-36.1053 914.6014,-40.8753 918.0789,-41.2711"/>
+</g>
+<!-- path/filepath -->
+<g id="node64" class="node">
+<title>path/filepath</title>
+<g id="a_node64"><a xlink:href="https://godoc.org/path/filepath" xlink:title="path/filepath" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M721.5,-36C721.5,-36 660.5,-36 660.5,-36 654.5,-36 648.5,-30 648.5,-24 648.5,-24 648.5,-12 648.5,-12 648.5,-6 654.5,0 660.5,0 660.5,0 721.5,0 721.5,0 727.5,0 733.5,-6 733.5,-12 733.5,-12 733.5,-24 733.5,-24 733.5,-30 727.5,-36 721.5,-36"/>
+<text text-anchor="middle" x="691" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">path/filepath</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;path/filepath -->
+<g id="edge166" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M2745.2057,-392.837C2340.4356,-388.9197 1222.2875,-373.1784 1066,-318 903.2361,-260.535 758.4737,-99.8093 709.0477,-40.4234"/>
+<polygon fill="#000000" stroke="#000000" points="710.1915,-39.0608 705.6548,-36.3255 707.4956,-41.2929 710.1915,-39.0608"/>
+</g>
+<!-- runtime -->
+<g id="node65" class="node">
+<title>runtime</title>
+<g id="a_node65"><a xlink:href="https://godoc.org/runtime" xlink:title="runtime" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3100.5,-318C3100.5,-318 3065.5,-318 3065.5,-318 3059.5,-318 3053.5,-312 3053.5,-306 3053.5,-306 3053.5,-294 3053.5,-294 3053.5,-288 3059.5,-282 3065.5,-282 3065.5,-282 3100.5,-282 3100.5,-282 3106.5,-282 3112.5,-288 3112.5,-294 3112.5,-294 3112.5,-306 3112.5,-306 3112.5,-312 3106.5,-318 3100.5,-318"/>
+<text text-anchor="middle" x="3083" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">runtime</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;runtime -->
+<g id="edge167" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M2928.0195,-375.8759C2962.8135,-358.8413 3014.4841,-333.5443 3048.4501,-316.915"/>
+<polygon fill="#000000" stroke="#000000" points="3049.6077,-318.2968 3053.3289,-314.5265 3048.0687,-315.1533 3049.6077,-318.2968"/>
+</g>
+<!-- runtime/debug -->
+<g id="node66" class="node">
+<title>runtime/debug</title>
+<g id="a_node66"><a xlink:href="https://godoc.org/runtime/debug" xlink:title="runtime/debug" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3226,-318C3226,-318 3154,-318 3154,-318 3148,-318 3142,-312 3142,-306 3142,-306 3142,-294 3142,-294 3142,-288 3148,-282 3154,-282 3154,-282 3226,-282 3226,-282 3232,-282 3238,-288 3238,-294 3238,-294 3238,-306 3238,-306 3238,-312 3232,-318 3226,-318"/>
+<text text-anchor="middle" x="3190" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">runtime/debug</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus&#45;&gt;runtime/debug -->
+<g id="edge168" class="edge">
+<title>github.com/prometheus/client_golang/prometheus&#45;&gt;runtime/debug</title>
+<path fill="none" stroke="#000000" d="M2948.2964,-375.9871C3002.67,-358.893 3083.7403,-333.406 3136.7412,-316.7436"/>
+<polygon fill="#000000" stroke="#000000" points="3137.5358,-318.3283 3141.7808,-315.1592 3136.4861,-314.9894 3137.5358,-318.3283"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;bufio -->
+<g id="edge177" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M5126.528,-469.5702C5123.7863,-405.2441 5105.1681,-189.4326 4979,-94 4943.4257,-67.0919 4264.15,-28.4404 4105.7989,-19.7703"/>
+<polygon fill="#000000" stroke="#000000" points="4105.4756,-18.0001 4100.3876,-19.4746 4105.2846,-21.4949 4105.4756,-18.0001"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;compress/gzip -->
+<g id="edge178" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;compress/gzip</title>
+<path fill="none" stroke="#000000" d="M5182.3802,-469.9871C5234.6177,-452.9963 5312.3482,-427.7137 5363.5908,-411.0466"/>
+<polygon fill="#000000" stroke="#000000" points="5364.2519,-412.6719 5368.4653,-409.4611 5363.1692,-409.3435 5364.2519,-412.6719"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;crypto/tls -->
+<g id="edge179" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;crypto/tls</title>
+<path fill="none" stroke="#000000" d="M5228.1612,-469.9592C5298.7914,-456.426 5394.9224,-436.1268 5478,-412 5481.1984,-411.0711 5484.4883,-410.038 5487.7776,-408.9497"/>
+<polygon fill="#000000" stroke="#000000" points="5488.7597,-410.4646 5492.9325,-407.2012 5487.6353,-407.1501 5488.7597,-410.4646"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;errors -->
+<g id="edge180" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M5127.8886,-469.8462C5129.3354,-447.4343 5133.007,-408.319 5142,-376 5165.7391,-290.6863 5246.4436,-267.7751 5208,-188 5178.8918,-127.597 5151.7103,-120.2229 5090,-94 4990.2079,-51.5948 4652.1766,-26.8737 4545.5216,-19.9908"/>
+<polygon fill="#000000" stroke="#000000" points="4545.4218,-18.2309 4540.3201,-19.6574 4545.1979,-21.7237 4545.4218,-18.2309"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;fmt -->
+<g id="edge181" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M5138.0653,-469.8163C5153.2846,-445.9495 5182.6162,-403.8311 5216,-376 5374.5087,-243.8555 5877.2661,-101.9599 5900,-94 5970.322,-69.3779 6053.4709,-42.4348 6098.01,-28.1766"/>
+<polygon fill="#000000" stroke="#000000" points="6098.7554,-29.7756 6102.9846,-26.5854 6097.6891,-26.4419 6098.7554,-29.7756"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;io -->
+<g id="edge185" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M5131.0425,-469.8075C5136.5412,-447.0179 5147.6093,-407.1761 5164,-376 5179.057,-347.3608 5189.6037,-344.6175 5208,-318 5236.0443,-277.4229 5253.3566,-271.4009 5267,-224 5276.9241,-189.5208 5258.1752,-100.0412 5253,-94 5225.1906,-61.5374 5101.0136,-34.8869 5041.2903,-23.69"/>
+<polygon fill="#000000" stroke="#000000" points="5041.2989,-21.9117 5036.0633,-22.7186 5040.6594,-25.3528 5041.2989,-21.9117"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net/http -->
+<g id="edge187" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M5299.6741,-472.9192C5384.4059,-461.9733 5486.934,-443.3613 5575,-412 5708.325,-364.5215 5849.506,-269.5817 5908.9503,-227.2369"/>
+<polygon fill="#000000" stroke="#000000" points="5910.0372,-228.6112 5913.0878,-224.2803 5908.0023,-225.7635 5910.0372,-228.6112"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;strings -->
+<g id="edge190" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M5299.5217,-483.4767C5698.7815,-472.5067 6657.2623,-443.0922 6720,-412 6836.4556,-354.2857 6821.466,-284.9779 6908,-188 6931.6521,-161.4933 6939.1431,-156.3225 6963,-130 6990.8085,-99.3175 7021.9998,-62.9353 7041.3233,-40.1544"/>
+<polygon fill="#000000" stroke="#000000" points="7042.8925,-41.0095 7044.7898,-36.0634 7040.2222,-38.7468 7042.8925,-41.0095"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;time -->
+<g id="edge192" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M5068.1432,-469.9389C5039.2521,-458.0558 5006.5405,-439.5059 4987,-412 4925.5388,-325.4849 4930.4007,-189.7752 4935.3135,-135.5417"/>
+<polygon fill="#000000" stroke="#000000" points="4937.0856,-135.3908 4935.8171,-130.2475 4933.6014,-135.0593 4937.0856,-135.3908"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;sync -->
+<g id="edge191" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M5299.5843,-485.3276C5685.5743,-478.6998 6647.8956,-458.5578 7452,-412 7577.7786,-404.7174 8463.6114,-361.0977 8582,-318 8659.6718,-289.7247 8746.8551,-264.2735 8715,-188 8686.5089,-119.7814 8615.8373,-64.9798 8574.4904,-37.4128"/>
+<polygon fill="#000000" stroke="#000000" points="8575.2279,-35.8028 8570.0909,-34.5086 8573.2996,-38.7237 8575.2279,-35.8028"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;strconv -->
+<g id="edge189" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M5082.0851,-469.9055C5047.9032,-455.5784 5000.0724,-434.3218 4960,-412 4803.531,-324.8412 4790.307,-256.9405 4625,-188 4330.3117,-65.1017 3939.0241,-28.597 3823.4673,-20.166"/>
+<polygon fill="#000000" stroke="#000000" points="3823.2871,-18.3987 3818.1745,-19.7856 3823.0361,-21.8897 3823.2871,-18.3987"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/client_golang/prometheus -->
+<g id="edge182" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/client_golang/prometheus</title>
+<path fill="none" stroke="#000000" d="M4954.3326,-480.7412C4530.1974,-462.9108 3442.6818,-417.1923 3042.0924,-400.3518"/>
+<polygon fill="#000000" stroke="#000000" points="3041.8972,-398.5922 3036.8281,-400.1305 3041.7502,-402.0891 3041.8972,-398.5922"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/client_model/go -->
+<g id="edge183" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/client_model/go</title>
+<path fill="none" stroke="#000000" d="M4983.8976,-469.9993C4863.3525,-454.7694 4686.3632,-432.2523 4532,-412 3911.2742,-330.5612 3756.1512,-309.7047 3136,-224 3135.2417,-223.8952 3134.4809,-223.79 3133.7177,-223.6844"/>
+<polygon fill="#000000" stroke="#000000" points="3133.8423,-221.935 3128.6493,-222.9817 3133.3616,-225.4018 3133.8423,-221.935"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/common/expfmt -->
+<g id="edge184" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;github.com/prometheus/common/expfmt</title>
+<path fill="none" stroke="#000000" d="M4954.3519,-476.0251C4852.2047,-465.6657 4721.2593,-446.631 4609,-412 4574.0509,-401.2185 4569.8908,-386.9685 4535,-376 4417.496,-339.0608 4277.9835,-319.5656 4179.3671,-309.6152"/>
+<polygon fill="#000000" stroke="#000000" points="4179.2624,-307.8461 4174.1132,-309.0902 4178.9144,-311.3287 4179.2624,-307.8461"/>
+</g>
+<!-- net -->
+<g id="node67" class="node">
+<title>net</title>
+<g id="a_node67"><a xlink:href="https://godoc.org/net" xlink:title="net" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3493,-130C3493,-130 3463,-130 3463,-130 3457,-130 3451,-124 3451,-118 3451,-118 3451,-106 3451,-106 3451,-100 3457,-94 3463,-94 3463,-94 3493,-94 3493,-94 3499,-94 3505,-100 3505,-106 3505,-106 3505,-118 3505,-118 3505,-124 3499,-130 3493,-130"/>
+<text text-anchor="middle" x="3478" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">net</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net -->
+<g id="edge186" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M4954.4783,-471.3902C4867.2017,-460.0006 4760.4492,-441.4544 4668,-412 4633.1516,-400.8973 4627.7067,-390.1964 4594,-376 4365.975,-279.9616 4311.2416,-242.9661 4070,-188 3826.8944,-132.6092 3750.5483,-202.548 3512,-130 3511.2811,-129.7814 3510.5598,-129.5472 3509.8376,-129.2993"/>
+<polygon fill="#000000" stroke="#000000" points="3510.3707,-127.6297 3505.0753,-127.4822 3509.123,-130.8997 3510.3707,-127.6297"/>
+</g>
+<!-- net/http/httptrace -->
+<g id="node68" class="node">
+<title>net/http/httptrace</title>
+<g id="a_node68"><a xlink:href="https://godoc.org/net/http/httptrace" xlink:title="net/http/httptrace" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M5327.5,-412C5327.5,-412 5242.5,-412 5242.5,-412 5236.5,-412 5230.5,-406 5230.5,-400 5230.5,-400 5230.5,-388 5230.5,-388 5230.5,-382 5236.5,-376 5242.5,-376 5242.5,-376 5327.5,-376 5327.5,-376 5333.5,-376 5339.5,-382 5339.5,-388 5339.5,-388 5339.5,-400 5339.5,-400 5339.5,-406 5333.5,-412 5327.5,-412"/>
+<text text-anchor="middle" x="5285" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http/httptrace</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net/http/httptrace -->
+<g id="edge188" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/promhttp&#45;&gt;net/http/httptrace</title>
+<path fill="none" stroke="#000000" d="M5157.464,-469.8759C5184.0214,-454.0759 5222.5268,-431.1676 5250.2724,-414.6607"/>
+<polygon fill="#000000" stroke="#000000" points="5251.2273,-416.129 5254.6295,-412.0685 5249.4377,-413.121 5251.2273,-416.129"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;bufio -->
+<g id="edge109" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M3009.045,-101.618C3035.3874,-99.0179 3063.748,-96.3127 3090,-94 3281.47,-77.132 3893.548,-31.3675 4040.878,-20.3902"/>
+<polygon fill="#000000" stroke="#000000" points="4041.0653,-22.1312 4045.9215,-20.0145 4040.8052,-18.6409 4041.0653,-22.1312"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;bytes -->
+<g id="edge110" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2806.9948,-103.243C2765.7781,-99.927 2717.6341,-96.3816 2674,-94 1876.5486,-50.474 1671.1762,-129.2455 878,-36 859.3328,-33.8055 838.736,-29.6873 822.1648,-25.9433"/>
+<polygon fill="#000000" stroke="#000000" points="822.4224,-24.207 817.1578,-24.7959 821.6406,-27.6186 822.4224,-24.207"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;encoding -->
+<g id="edge111" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;encoding</title>
+<path fill="none" stroke="#000000" d="M2908,-93.8759C2908,-78.9211 2908,-57.5983 2908,-41.3629"/>
+<polygon fill="#000000" stroke="#000000" points="2909.7501,-41.0685 2908,-36.0685 2906.2501,-41.0685 2909.7501,-41.0685"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;encoding/json -->
+<g id="edge112" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M2806.9994,-103.1577C2765.7834,-99.8291 2717.6386,-96.2974 2674,-94 1768.2213,-46.314 1540.2077,-74.6842 634,-36 540.552,-32.0109 431.6697,-25.3688 369.1905,-21.3653"/>
+<polygon fill="#000000" stroke="#000000" points="369.1344,-19.6081 364.0325,-21.034 368.91,-23.1009 369.1344,-19.6081"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;errors -->
+<g id="edge113" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M3009.1061,-104.0215C3052.2044,-100.7313 3103.05,-96.9958 3149,-94 3680.5307,-59.3454 4329.2376,-27.0024 4480.4366,-19.5868"/>
+<polygon fill="#000000" stroke="#000000" points="4480.696,-21.3263 4485.6043,-19.3335 4480.5246,-17.8305 4480.696,-21.3263"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;fmt -->
+<g id="edge114" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3009.4156,-107.8778C3105.9718,-104.023 3255.3755,-98.2401 3385,-94 4500.7318,-57.5039 5871.944,-24.1882 6097.8239,-18.7684"/>
+<polygon fill="#000000" stroke="#000000" points="6097.9893,-20.5151 6102.9459,-18.6456 6097.9054,-17.0161 6097.9893,-20.5151"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;math -->
+<g id="edge117" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M2821.0014,-93.9871C2719.1213,-72.893 2555.5139,-39.0183 2486.4395,-24.7166"/>
+<polygon fill="#000000" stroke="#000000" points="2486.4872,-22.9394 2481.2362,-23.6392 2485.7775,-26.3667 2486.4872,-22.9394"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;sort -->
+<g id="edge119" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M2890.8399,-93.8759C2876.3113,-78.531 2855.4346,-56.4815 2839.9272,-40.1029"/>
+<polygon fill="#000000" stroke="#000000" points="2840.8159,-38.4961 2836.1074,-36.0685 2838.2743,-40.9025 2840.8159,-38.4961"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;reflect -->
+<g id="edge118" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;reflect</title>
+<path fill="none" stroke="#000000" d="M2806.9336,-104.1818C2765.7078,-101.0041 2717.5736,-97.3085 2674,-94 2330.0256,-67.8823 2235.0899,-117.9466 1900,-36 1896.7635,-35.2085 1893.4584,-34.1922 1890.1945,-33.0462"/>
+<polygon fill="#000000" stroke="#000000" points="1890.4114,-31.2594 1885.115,-31.1522 1889.1885,-34.5389 1890.4114,-31.2594"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;unsafe -->
+<g id="edge125" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;unsafe</title>
+<path fill="none" stroke="#000000" d="M2806.9455,-104.0207C2765.7215,-100.8193 2717.5854,-97.1495 2674,-94 2292.7903,-66.454 2187.195,-127.0709 1816,-36 1813.1688,-35.3054 1810.2873,-34.428 1807.4309,-33.4364"/>
+<polygon fill="#000000" stroke="#000000" points="1807.6295,-31.6433 1802.333,-31.5427 1806.4107,-34.9243 1807.6295,-31.6433"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;io -->
+<g id="edge115" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3009.0023,-105.4918C3067.1791,-101.8553 3141.6913,-97.3881 3208,-94 3571.9973,-75.4014 4764.5826,-27.7303 4976.6135,-19.2877"/>
+<polygon fill="#000000" stroke="#000000" points="4976.8042,-21.0316 4981.7306,-19.084 4976.6649,-17.5344 4976.8042,-21.0316"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;strings -->
+<g id="edge121" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3009.209,-108.1555C3115.4848,-104.211 3287.47,-98.089 3436,-94 3814.7367,-83.5734 6687.9816,-25.5072 7027.298,-18.6596"/>
+<polygon fill="#000000" stroke="#000000" points="7027.6089,-20.4038 7032.5726,-18.5532 7027.5383,-16.9045 7027.6089,-20.4038"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;sync -->
+<g id="edge122" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M3009.203,-107.9203C3115.4739,-103.7838 3287.4551,-97.5073 3436,-94 5397.8448,-47.6781 5888.9843,-74.4035 7851,-36 8105.3856,-31.0208 8412.2961,-21.9665 8510.8738,-18.9827"/>
+<polygon fill="#000000" stroke="#000000" points="8511.019,-20.7292 8515.9636,-18.8284 8510.9129,-17.2308 8511.019,-20.7292"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;strconv -->
+<g id="edge120" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3009.3045,-96.8021C3016.6415,-95.8159 3023.9315,-94.8706 3031,-94 3310.8359,-59.5321 3650.186,-29.8291 3756.5065,-20.8057"/>
+<polygon fill="#000000" stroke="#000000" points="3756.8562,-22.5324 3761.6905,-20.3664 3756.5606,-19.0449 3756.8562,-22.5324"/>
+</g>
+<!-- log -->
+<g id="node52" class="node">
+<title>log</title>
+<g id="a_node52"><a xlink:href="https://godoc.org/log" xlink:title="log" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3012,-36C3012,-36 2982,-36 2982,-36 2976,-36 2970,-30 2970,-24 2970,-24 2970,-12 2970,-12 2970,-6 2976,0 2982,0 2982,0 3012,0 3012,0 3018,0 3024,-6 3024,-12 3024,-12 3024,-24 3024,-24 3024,-30 3018,-36 3012,-36"/>
+<text text-anchor="middle" x="2997" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">log</text>
+</a>
+</g>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;log -->
+<g id="edge116" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;log</title>
+<path fill="none" stroke="#000000" d="M2925.1601,-93.8759C2939.6887,-78.531 2960.5654,-56.4815 2976.0728,-40.1029"/>
+<polygon fill="#000000" stroke="#000000" points="2977.7257,-40.9025 2979.8926,-36.0685 2975.1841,-38.4961 2977.7257,-40.9025"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;sync/atomic -->
+<g id="edge123" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;sync/atomic</title>
+<path fill="none" stroke="#000000" d="M2806.6018,-104.0173C2580.2231,-86.1111 2050.4349,-43.7035 2012,-36 2008.9116,-35.381 2005.7545,-34.6499 2002.5901,-33.8426"/>
+<polygon fill="#000000" stroke="#000000" points="2002.9024,-32.1148 1997.6203,-32.5166 2002.0001,-35.4965 2002.9024,-32.1148"/>
+</g>
+<!-- github.com/golang/protobuf/proto&#45;&gt;unicode/utf8 -->
+<g id="edge124" class="edge">
+<title>github.com/golang/protobuf/proto&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M2806.797,-100.6614C2624.9937,-80.2925 2249.7021,-38.2455 2116.8746,-23.3638"/>
+<polygon fill="#000000" stroke="#000000" points="2117.0356,-21.621 2111.8719,-22.8033 2116.6459,-25.0992 2117.0356,-21.621"/>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil -->
+<g id="node55" class="node">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil</title>
+<g id="a_node55"><a xlink:href="https://godoc.org/github.com/matttproud/golang_protobuf_extensions/pbutil" xlink:title="github.com/matttproud/golang_protobuf_extensions/pbutil" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M4043.5,-224C4043.5,-224 3732.5,-224 3732.5,-224 3726.5,-224 3720.5,-218 3720.5,-212 3720.5,-212 3720.5,-200 3720.5,-200 3720.5,-194 3726.5,-188 3732.5,-188 3732.5,-188 4043.5,-188 4043.5,-188 4049.5,-188 4055.5,-194 4055.5,-200 4055.5,-200 4055.5,-212 4055.5,-212 4055.5,-218 4049.5,-224 4043.5,-224"/>
+<text text-anchor="middle" x="3888" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/matttproud/golang_protobuf_extensions/pbutil</text>
+</a>
+</g>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;encoding/binary -->
+<g id="edge136" class="edge">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;encoding/binary</title>
+<path fill="none" stroke="#000000" d="M3720.4423,-196.9205C3659.8242,-193.8212 3590.8409,-190.5082 3528,-188 3420.0379,-183.6908 1665.4949,-200.9434 1584,-130 1559.0371,-108.2692 1558.6371,-66.9015 1561.3238,-40.9793"/>
+<polygon fill="#000000" stroke="#000000" points="1563.0623,-41.1786 1561.8959,-36.0112 1559.5853,-40.7782 1563.0623,-41.1786"/>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;errors -->
+<g id="edge137" class="edge">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M4005.2542,-187.9658C4074.6439,-175.5071 4163.6252,-156.3919 4240,-130 4274.5686,-118.0546 4280.4394,-108.5385 4314,-94 4371.9713,-68.8868 4441.1874,-43.4474 4480.8265,-29.3"/>
+<polygon fill="#000000" stroke="#000000" points="4481.8599,-30.7897 4485.9831,-27.4638 4480.6858,-27.4925 4481.8599,-30.7897"/>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;io -->
+<g id="edge139" class="edge">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M3995.4862,-187.9738C4240.8222,-146.8291 4832.0188,-47.6811 4976.6602,-23.4236"/>
+<polygon fill="#000000" stroke="#000000" points="4976.9742,-25.1455 4981.6158,-22.5925 4976.3952,-21.6937 4976.9742,-25.1455"/>
+</g>
+<!-- github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge138" class="edge">
+<title>github.com/matttproud/golang_protobuf_extensions/pbutil&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3720.3674,-189.921C3518.0873,-170.5186 3184.0447,-138.4778 3014.2253,-122.189"/>
+<polygon fill="#000000" stroke="#000000" points="3014.1447,-120.4233 3009.0004,-121.6878 3013.8104,-123.9073 3014.1447,-120.4233"/>
+</g>
+<!-- github.com/opencontainers/image&#45;spec/specs&#45;go&#45;&gt;fmt -->
+<g id="edge146" class="edge">
+<title>github.com/opencontainers/image&#45;spec/specs&#45;go&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M6327.243,-93.9871C6278.7427,-75.3025 6204.2135,-46.5904 6162.0198,-30.3355"/>
+<polygon fill="#000000" stroke="#000000" points="6162.5837,-28.6774 6157.2889,-28.5129 6161.3255,-31.9435 6162.5837,-28.6774"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/internal&#45;&gt;sort -->
+<g id="edge176" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/internal&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M2650.3845,-281.8723C2596.3086,-245.3833 2488.923,-160.8516 2540,-94 2570.076,-54.6353 2719.5984,-30.7961 2786.5221,-21.94"/>
+<polygon fill="#000000" stroke="#000000" points="2786.85,-23.6621 2791.5808,-21.2784 2786.3961,-20.1917 2786.85,-23.6621"/>
+</g>
+<!-- github.com/prometheus/client_golang/prometheus/internal&#45;&gt;github.com/prometheus/client_model/go -->
+<g id="edge175" class="edge">
+<title>github.com/prometheus/client_golang/prometheus/internal&#45;&gt;github.com/prometheus/client_model/go</title>
+<path fill="none" stroke="#000000" d="M2742.2369,-281.9871C2799.1222,-265.7834 2882.4828,-242.0382 2940.8356,-225.4165"/>
+<polygon fill="#000000" stroke="#000000" points="2941.3672,-227.0848 2945.6965,-224.0319 2940.4083,-223.7187 2941.3672,-227.0848"/>
+</g>
+<!-- github.com/prometheus/client_model/go&#45;&gt;fmt -->
+<g id="edge193" class="edge">
+<title>github.com/prometheus/client_model/go&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3128.559,-189.9608C3232.0931,-175.6472 3385.7506,-153.387 3519,-130 3598.4977,-116.0471 3616.8239,-103.2929 3697,-94 3946.2411,-65.1114 5826.9332,-24.4153 6097.5907,-18.6814"/>
+<polygon fill="#000000" stroke="#000000" points="6097.9878,-20.4235 6102.9496,-18.568 6097.9137,-16.9243 6097.9878,-20.4235"/>
+</g>
+<!-- github.com/prometheus/client_model/go&#45;&gt;math -->
+<g id="edge195" class="edge">
+<title>github.com/prometheus/client_model/go&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M2928.3279,-187.9739C2874.3206,-174.7355 2802.1901,-154.7734 2741,-130 2709.6665,-117.3143 2704.9641,-107.5625 2674,-94 2608.9556,-65.5101 2529.5461,-40.3224 2486.1415,-27.3277"/>
+<polygon fill="#000000" stroke="#000000" points="2486.578,-25.6318 2481.2864,-25.8801 2485.5779,-28.9859 2486.578,-25.6318"/>
+</g>
+<!-- github.com/prometheus/client_model/go&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge194" class="edge">
+<title>github.com/prometheus/client_model/go&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M2989.5262,-187.8759C2972.8989,-172.401 2948.9451,-150.1073 2931.3027,-133.6877"/>
+<polygon fill="#000000" stroke="#000000" points="2932.2664,-132.1939 2927.414,-130.0685 2929.8819,-134.756 2932.2664,-132.1939"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;bufio -->
+<g id="edge196" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M4063.3033,-281.8818C4077.5121,-258.0874 4105.0242,-216.0491 4137,-188 4176.4948,-153.3553 4213.4875,-175.3562 4240,-130 4273.8598,-72.0742 4162.7447,-37.9965 4105.257,-24.6085"/>
+<polygon fill="#000000" stroke="#000000" points="4105.4682,-22.8617 4100.2038,-23.4527 4104.6878,-26.2736 4105.4682,-22.8617"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;bytes -->
+<g id="edge197" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M3931.9248,-296.1715C3336.2147,-277.0549 742.6913,-190.456 686,-130 653.6676,-95.5205 716.6875,-54.7959 758.015,-33.212"/>
+<polygon fill="#000000" stroke="#000000" points="759.0468,-34.6491 762.6899,-30.8033 757.4436,-31.5378 759.0468,-34.6491"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;fmt -->
+<g id="edge198" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M4140.4495,-281.9593C4198.5397,-268.7757 4275.9767,-248.8816 4342,-224 4375.4786,-211.3832 4380.1317,-199.5298 4414,-188 4620.6135,-117.6622 4679.5014,-121.6566 4896,-94 5371.072,-33.3119 5955.4576,-20.6791 6097.6904,-18.4355"/>
+<polygon fill="#000000" stroke="#000000" points="6097.9843,-20.1813 6102.9566,-18.3541 6097.9301,-16.6817 6097.9843,-20.1813"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;math -->
+<g id="edge206" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3931.612,-297.8032C3689.3614,-292.4417 3128.9556,-275.14 2661,-224 2405.3286,-196.0593 2256.4385,-326.0785 2090,-130 2079.6459,-117.802 2079.9085,-106.4162 2090,-94 2131.7851,-42.5895 2340.5396,-24.763 2421.6753,-19.7152"/>
+<polygon fill="#000000" stroke="#000000" points="2421.9476,-21.4519 2426.8319,-19.4009 2421.7347,-17.9584 2421.9476,-21.4519"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;io -->
+<g id="edge204" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M4100.8741,-281.972C4163.1145,-258.8285 4275.4068,-218.075 4373,-188 4602.1352,-117.3882 4882.8244,-48.3448 4976.7031,-25.7172"/>
+<polygon fill="#000000" stroke="#000000" points="4977.3741,-27.3557 4981.8256,-24.4839 4976.5548,-23.9529 4977.3741,-27.3557"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;mime -->
+<g id="edge207" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;mime</title>
+<path fill="none" stroke="#000000" d="M4154.7539,-281.9871C4276.6517,-260.4082 4474.1025,-225.4545 4551.4513,-211.7619"/>
+<polygon fill="#000000" stroke="#000000" points="4552.1918,-213.4081 4556.8102,-210.8133 4551.5816,-209.9617 4552.1918,-213.4081"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;net/http -->
+<g id="edge208" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M4174.1433,-293.9589C4551.2479,-275.1537 5691.6717,-218.2837 5903.8295,-207.704"/>
+<polygon fill="#000000" stroke="#000000" points="5904.0543,-209.4451 5908.9609,-207.4481 5903.8799,-205.9494 5904.0543,-209.4451"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;strings -->
+<g id="edge210" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M4174.0086,-284.3265C4527.6711,-238.5881 5544.1528,-107.6453 5695,-94 6225.8809,-45.9776 6875.8478,-23.7218 7027.3658,-18.9829"/>
+<polygon fill="#000000" stroke="#000000" points="7027.6013,-20.7266 7032.5444,-18.8217 7027.4924,-17.2282 7027.6013,-20.7266"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;sync -->
+<g id="edge211" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;sync</title>
+<path fill="none" stroke="#000000" d="M4174.1989,-298.4787C4783.1741,-290.7039 7486.7737,-254.5848 7662,-224 7711.4277,-215.3727 7720.7704,-201.8361 7769,-188 7942.0458,-138.3568 7987.1498,-132.5484 8163,-94 8291.4195,-65.849 8445.1975,-36.3926 8510.7969,-24.026"/>
+<polygon fill="#000000" stroke="#000000" points="8511.1527,-25.7399 8515.7424,-23.0946 8510.5048,-22.3003 8511.1527,-25.7399"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;io/ioutil -->
+<g id="edge205" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M3931.9154,-296.8645C3419.4193,-283.5641 1448.9479,-232.1193 1317,-224 878.0635,-196.9903 743.5093,-285.0962 332,-130 278.4832,-109.8297 226.8049,-66.0902 198.7238,-39.6349"/>
+<polygon fill="#000000" stroke="#000000" points="199.9167,-38.3543 195.0861,-36.1798 197.5063,-40.8921 199.9167,-38.3543"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;strconv -->
+<g id="edge209" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3937.3312,-281.9946C3842.242,-266.0122 3720.8126,-242.408 3706,-224 3695.9693,-211.5346 3695.4828,-200.0577 3706,-188 3830.7336,-44.9964 4004.2664,-273.0036 4129,-130 4139.5172,-117.9423 4138.9999,-106.49 4129,-94 4090.8984,-46.4106 3901.5917,-26.5898 3823.6335,-20.3567"/>
+<polygon fill="#000000" stroke="#000000" points="3823.3382,-18.5782 3818.2165,-19.9312 3823.0641,-22.0674 3823.3382,-18.5782"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/golang/protobuf/proto -->
+<g id="edge199" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/golang/protobuf/proto</title>
+<path fill="none" stroke="#000000" d="M3931.7269,-284.3726C3924.3788,-283.5382 3917.0929,-282.7399 3910,-282 3599.5076,-249.6101 3509.3323,-312.6273 3210,-224 3177.5868,-214.403 3174.0971,-201.2548 3143,-188 3089.4194,-165.1619 3026.5367,-145.1626 2979.3086,-131.4195"/>
+<polygon fill="#000000" stroke="#000000" points="2979.7886,-129.7366 2974.4991,-130.0258 2978.8144,-133.0983 2979.7886,-129.7366"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/matttproud/golang_protobuf_extensions/pbutil -->
+<g id="edge200" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/matttproud/golang_protobuf_extensions/pbutil</title>
+<path fill="none" stroke="#000000" d="M4021.1863,-281.8759C3993.4523,-266.0759 3953.241,-243.1676 3924.2662,-226.6607"/>
+<polygon fill="#000000" stroke="#000000" points="3924.9267,-225.023 3919.716,-224.0685 3923.1942,-228.0641 3924.9267,-225.023"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/client_model/go -->
+<g id="edge201" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/client_model/go</title>
+<path fill="none" stroke="#000000" d="M3931.7324,-284.3188C3924.3829,-283.4978 3917.0952,-282.7172 3910,-282 3573.3985,-247.975 3487.5866,-258.1719 3151,-224 3145.4161,-223.4331 3139.7122,-222.8261 3133.9529,-222.1905"/>
+<polygon fill="#000000" stroke="#000000" points="3133.8132,-220.4142 3128.65,-221.5991 3133.4252,-223.8926 3133.8132,-220.4142"/>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/common/model -->
+<g id="edge203" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/common/model</title>
+<path fill="none" stroke="#000000" d="M3931.9295,-282.7042C3814.527,-265.9324 3637.2667,-240.6095 3518.9105,-223.7015"/>
+<polygon fill="#000000" stroke="#000000" points="3518.8687,-221.9278 3513.6714,-222.9531 3518.3737,-225.3927 3518.8687,-221.9278"/>
+</g>
+<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg -->
+<g id="node69" class="node">
+<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</title>
+<g id="a_node69"><a xlink:href="https://godoc.org/github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" xlink:title="github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M4102.5,-130C4102.5,-130 3723.5,-130 3723.5,-130 3717.5,-130 3711.5,-124 3711.5,-118 3711.5,-118 3711.5,-106 3711.5,-106 3711.5,-100 3717.5,-94 3723.5,-94 3723.5,-94 4102.5,-94 4102.5,-94 4108.5,-94 4114.5,-100 4114.5,-106 4114.5,-106 4114.5,-118 4114.5,-118 4114.5,-124 4108.5,-130 4102.5,-130"/>
+<text text-anchor="middle" x="3913" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg -->
+<g id="edge202" class="edge">
+<title>github.com/prometheus/common/expfmt&#45;&gt;github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</title>
+<path fill="none" stroke="#000000" d="M3931.9931,-284.8003C3821.2243,-269.6448 3671.856,-245.5782 3654,-224 3613.43,-174.973 3680.716,-146.8634 3756.8482,-131.0667"/>
+<polygon fill="#000000" stroke="#000000" points="3757.3241,-132.7558 3761.8747,-130.0439 3756.6261,-129.3261 3757.3241,-132.7558"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;encoding/json -->
+<g id="edge215" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M3276.4847,-196.4411C3234.3639,-193.3335 3186.5897,-190.1382 3143,-188 2179.3698,-140.7302 1933.1561,-224.4346 973,-130 746.1167,-107.6853 479.2125,-53.0836 369.267,-29.3381"/>
+<polygon fill="#000000" stroke="#000000" points="369.4079,-27.5782 364.1509,-28.2309 368.6676,-30.999 369.4079,-27.5782"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;fmt -->
+<g id="edge216" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M3513.8579,-194.8106C3746.5703,-173.1679 4278.7314,-124.9805 4727,-94 5274.7415,-56.1448 5943.8744,-26.1054 6097.5163,-19.4015"/>
+<polygon fill="#000000" stroke="#000000" points="6097.8443,-21.1389 6102.7633,-19.1729 6097.6919,-17.6423 6097.8443,-21.1389"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;math -->
+<g id="edge217" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;math</title>
+<path fill="none" stroke="#000000" d="M3276.4444,-196.6988C3126.9984,-183.9989 2879.0969,-159.417 2792,-130 2761.8115,-119.8038 2759.381,-106.3316 2730,-94 2645.1546,-58.3892 2538.6744,-34.5361 2486.2292,-24.0597"/>
+<polygon fill="#000000" stroke="#000000" points="2486.2648,-22.2829 2481.02,-23.0278 2485.5846,-25.7161 2486.2648,-22.2829"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;sort -->
+<g id="edge219" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3386.9757,-187.6426C3374.6814,-161.9342 3348.5908,-115.904 3311,-94 3136.386,7.7468 3054.7305,-90.06 2860,-36 2857.191,-35.2202 2854.3253,-34.2826 2851.4796,-33.2502"/>
+<polygon fill="#000000" stroke="#000000" points="2851.6909,-31.4572 2846.3957,-31.3031 2850.439,-34.7257 2851.6909,-31.4572"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;strings -->
+<g id="edge221" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M3513.5034,-198.9667C3571.5235,-195.5676 3642.412,-191.4808 3706,-188 3955.7362,-174.3297 4589.3184,-198.0252 4830,-130 4862.1536,-120.9122 4863.7727,-102.823 4896,-94 5003.4125,-64.5932 6766.5533,-24.4995 7027.7632,-18.7083"/>
+<polygon fill="#000000" stroke="#000000" points="7027.9824,-20.4539 7032.9424,-18.5936 7027.9049,-16.9548 7027.9824,-20.4539"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;time -->
+<g id="edge222" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M3513.8693,-198.8735C3571.8198,-195.4622 3642.5426,-191.3919 3706,-188 3968.8295,-173.9513 4631.069,-182.4239 4889,-130 4894.5757,-128.8667 4900.3636,-127.2068 4905.9243,-125.3466"/>
+<polygon fill="#000000" stroke="#000000" points="4906.8104,-126.891 4910.9546,-123.5913 4905.6572,-123.5864 4906.8104,-126.891"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;regexp -->
+<g id="edge218" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M3513.8408,-198.2807C3571.7846,-194.7317 3642.5101,-190.7168 3706,-188 4635.2846,-148.2345 4869.3497,-182.5343 5798,-130 5985.4942,-119.3933 6031.5645,-105.5977 6219,-94 6891.0971,-52.4136 7065.7829,-126.9134 7733,-36 7747.5983,-34.0109 7763.4957,-30.4676 7776.9596,-27.0478"/>
+<polygon fill="#000000" stroke="#000000" points="7777.3996,-28.7417 7781.8029,-25.7966 7776.5241,-25.3529 7777.3996,-28.7417"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;strconv -->
+<g id="edge220" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3432.8743,-187.9738C3510.0594,-151.2375 3684.3973,-68.2616 3757.1466,-33.6366"/>
+<polygon fill="#000000" stroke="#000000" points="3758.0268,-35.1558 3761.7894,-31.4268 3756.5226,-31.9955 3758.0268,-35.1558"/>
+</g>
+<!-- github.com/prometheus/common/model&#45;&gt;unicode/utf8 -->
+<g id="edge223" class="edge">
+<title>github.com/prometheus/common/model&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M3276.4459,-197.1325C3234.3215,-194.0875 3186.5551,-190.7549 3143,-188 3026.0563,-180.6033 2178.0763,-207.2858 2090,-130 2065.2889,-108.3163 2063.8107,-67.5893 2065.8154,-41.6797"/>
+<polygon fill="#000000" stroke="#000000" points="2067.5862,-41.524 2066.2889,-36.3879 2064.1002,-41.212 2067.5862,-41.524"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;bufio -->
+<g id="edge224" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M2860.1973,-189.4064C2922.9973,-176.6734 3007.4028,-156.7325 3079,-130 3111.7743,-117.763 3115.3641,-103.619 3149,-94 3170.9353,-87.7271 3879.1592,-32.9238 4040.5072,-20.498"/>
+<polygon fill="#000000" stroke="#000000" points="4040.7174,-22.2371 4045.5683,-20.1083 4040.4487,-18.7474 4040.7174,-22.2371"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;bytes -->
+<g id="edge225" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M2675.7376,-194.4557C2653.2767,-191.9852 2629.3002,-189.6312 2607,-188 2421.7165,-174.447 1110.8609,-192.748 936,-130 884.1264,-111.3854 835.8644,-66.997 809.9036,-40.0255"/>
+<polygon fill="#000000" stroke="#000000" points="810.9534,-38.5904 806.2373,-36.1776 808.4194,-41.0048 810.9534,-38.5904"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;encoding/hex -->
+<g id="edge226" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;encoding/hex</title>
+<path fill="none" stroke="#000000" d="M2738.5001,-187.8759C2712.8889,-172.1409 2675.8029,-149.356 2648.9609,-132.8648"/>
+<polygon fill="#000000" stroke="#000000" points="2649.5857,-131.1949 2644.4094,-130.0685 2647.7535,-134.177 2649.5857,-131.1949"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;errors -->
+<g id="edge227" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2860.2169,-189.7843C2951.9679,-173.1431 3086.072,-147.3056 3136,-130 3169.8041,-118.2831 3173.4338,-103.229 3208,-94 3271.3705,-77.0803 4284.7677,-28.7296 4480.4407,-19.524"/>
+<polygon fill="#000000" stroke="#000000" points="4480.8089,-21.2587 4485.7211,-19.2758 4480.6444,-17.7626 4480.8089,-21.2587"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;fmt -->
+<g id="edge228" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2860.1849,-190.2835C2865.1936,-189.4948 2870.1568,-188.7286 2875,-188 3068.7456,-158.8554 3120.2674,-170.3161 3312,-130 3368.1586,-118.1914 3379.3046,-102.8809 3436,-94 3574.0801,-72.3709 5801.8008,-24.8989 6097.6407,-18.6774"/>
+<polygon fill="#000000" stroke="#000000" points="6097.7075,-20.4264 6102.6696,-18.5717 6097.6339,-16.9272 6097.7075,-20.4264"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;sort -->
+<g id="edge237" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M2675.8621,-191.3063C2602.2923,-175.4564 2517.4112,-145.3188 2556,-94 2583.9218,-56.8671 2722.5829,-32.1255 2786.5585,-22.5056"/>
+<polygon fill="#000000" stroke="#000000" points="2787.0846,-24.1967 2791.7728,-21.7302 2786.5697,-20.7347 2787.0846,-24.1967"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;io -->
+<g id="edge231" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M2860.2875,-189.803C2946.605,-174.5904 3079.0962,-151.0778 3194,-130 3278.9659,-114.414 3299.2023,-104.0435 3385,-94 3548.5057,-74.86 4762.7311,-27.4873 4976.8067,-19.2355"/>
+<polygon fill="#000000" stroke="#000000" points="4977.042,-20.9779 4981.9709,-19.0366 4976.9072,-17.4805 4977.042,-20.9779"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;strings -->
+<g id="edge239" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2860.12,-189.7793C2865.1455,-189.1206 2870.13,-188.5203 2875,-188 3429.7715,-128.7253 3571.7371,-157.2577 4129,-130 4439.2636,-114.8239 4516.6496,-107.2837 4827,-94 5724.9205,-55.5671 6826.6242,-24.4482 7027.7206,-18.8858"/>
+<polygon fill="#000000" stroke="#000000" points="7027.9071,-20.6314 7032.8568,-18.7438 7027.8103,-17.1327 7027.9071,-20.6314"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;time -->
+<g id="edge240" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2860.11,-189.6816C2865.1381,-189.0482 2870.1259,-188.48 2875,-188 3740.0866,-102.8082 3964.8492,-214.5365 4830,-130 4855.5711,-127.5014 4884.2468,-122.6234 4905.6204,-118.5751"/>
+<polygon fill="#000000" stroke="#000000" points="4906.0329,-120.278 4910.6147,-117.6192 4905.3748,-116.8404 4906.0329,-120.278"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;regexp -->
+<g id="edge236" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M2860.1088,-189.6692C2865.1372,-189.039 2870.1254,-188.4749 2875,-188 3806.0588,-97.298 4044.1339,-163.5053 4979,-130 5341.7865,-116.9979 5432.2291,-107.4301 5795,-94 6225.5645,-78.0601 7305.8456,-92.3829 7733,-36 7747.6065,-34.072 7763.5051,-30.538 7776.9677,-27.1085"/>
+<polygon fill="#000000" stroke="#000000" points="7777.4097,-28.8019 7781.8105,-25.853 7776.5313,-25.4139 7777.4097,-28.8019"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;io/ioutil -->
+<g id="edge232" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M2675.93,-204.1347C2337.4098,-197.016 1162.0729,-169.9343 789,-130 567.1156,-106.2492 304.7835,-47.8911 211.7921,-26.2506"/>
+<polygon fill="#000000" stroke="#000000" points="211.9678,-24.4946 206.7009,-25.063 211.1727,-27.9031 211.9678,-24.4946"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;strconv -->
+<g id="edge238" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M2843.0279,-187.9432C2894.5743,-174.5016 2964.0704,-154.3166 3023,-130 3054.2483,-117.1058 3057.7117,-104.0091 3090,-94 3217.2742,-54.5459 3635.4287,-27.1732 3756.7358,-19.9178"/>
+<polygon fill="#000000" stroke="#000000" points="3757.0601,-21.6517 3761.9472,-19.6076 3756.852,-18.1579 3757.0601,-21.6517"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;os -->
+<g id="edge234" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M2675.7367,-194.4682C2653.2758,-191.9973 2629.2996,-189.64 2607,-188 2516.4097,-181.3378 1048.8732,-179.9425 973,-130 942.6783,-110.0412 929.3021,-67.7293 923.7064,-41.2061"/>
+<polygon fill="#000000" stroke="#000000" points="925.3863,-40.6822 922.6883,-36.1233 921.9545,-41.3697 925.3863,-40.6822"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;path/filepath -->
+<g id="edge235" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M2675.7363,-203.6399C2307.0262,-194.0371 955.0432,-157.1234 868,-130 807.8608,-111.2601 748.0675,-66.601 715.6871,-39.6794"/>
+<polygon fill="#000000" stroke="#000000" points="716.4499,-38.0354 711.4939,-36.1645 714.2015,-40.7177 716.4499,-38.0354"/>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;net -->
+<g id="edge233" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M2860.1616,-190.1213C2865.1764,-189.3744 2870.1472,-188.6616 2875,-188 3120.2991,-154.5599 3187.849,-186.0008 3429,-130 3434.5422,-128.713 3440.3131,-126.9746 3445.8671,-125.0838"/>
+<polygon fill="#000000" stroke="#000000" points="3446.7596,-126.6249 3450.894,-123.3129 3445.5966,-123.3237 3446.7596,-126.6249"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/fs -->
+<g id="node70" class="node">
+<title>github.com/prometheus/procfs/internal/fs</title>
+<g id="a_node70"><a xlink:href="https://godoc.org/github.com/prometheus/procfs/internal/fs" xlink:title="github.com/prometheus/procfs/internal/fs" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1218,-130C1218,-130 1000,-130 1000,-130 994,-130 988,-124 988,-118 988,-118 988,-106 988,-106 988,-100 994,-94 1000,-94 1000,-94 1218,-94 1218,-94 1224,-94 1230,-100 1230,-106 1230,-106 1230,-118 1230,-118 1230,-124 1224,-130 1218,-130"/>
+<text text-anchor="middle" x="1109" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs/internal/fs</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;github.com/prometheus/procfs/internal/fs -->
+<g id="edge229" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;github.com/prometheus/procfs/internal/fs</title>
+<path fill="none" stroke="#000000" d="M2675.7282,-194.5807C2653.2676,-192.1061 2629.2936,-189.7189 2607,-188 2002.9109,-141.4241 1848.5866,-182.6896 1245,-130 1241.808,-129.7214 1238.5759,-129.4223 1235.3168,-129.1057"/>
+<polygon fill="#000000" stroke="#000000" points="1235.3235,-127.3478 1230.1748,-128.5943 1234.977,-130.8307 1235.3235,-127.3478"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util -->
+<g id="node71" class="node">
+<title>github.com/prometheus/procfs/internal/util</title>
+<g id="a_node71"><a xlink:href="https://godoc.org/github.com/prometheus/procfs/internal/util" xlink:title="github.com/prometheus/procfs/internal/util" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1498.5,-130C1498.5,-130 1271.5,-130 1271.5,-130 1265.5,-130 1259.5,-124 1259.5,-118 1259.5,-118 1259.5,-106 1259.5,-106 1259.5,-100 1265.5,-94 1271.5,-94 1271.5,-94 1498.5,-94 1498.5,-94 1504.5,-94 1510.5,-100 1510.5,-106 1510.5,-106 1510.5,-118 1510.5,-118 1510.5,-124 1504.5,-130 1498.5,-130"/>
+<text text-anchor="middle" x="1385" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs/internal/util</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/procfs&#45;&gt;github.com/prometheus/procfs/internal/util -->
+<g id="edge230" class="edge">
+<title>github.com/prometheus/procfs&#45;&gt;github.com/prometheus/procfs/internal/util</title>
+<path fill="none" stroke="#000000" d="M2675.7137,-194.7618C2653.2536,-192.2811 2629.2835,-189.8458 2607,-188 2130.6027,-148.5382 2009.2005,-171.7691 1533,-130 1527.4724,-129.5152 1521.8327,-128.9853 1516.1385,-128.4213"/>
+<polygon fill="#000000" stroke="#000000" points="1516.0454,-126.6532 1510.8954,-127.8942 1515.6952,-130.1357 1516.0454,-126.6532"/>
+</g>
+<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;sort -->
+<g id="edge212" class="edge">
+<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;sort</title>
+<path fill="none" stroke="#000000" d="M3711.2676,-99.2119C3417.0353,-80.3042 2900.5299,-46.0417 2860,-36 2857.1704,-35.2989 2854.29,-34.417 2851.4344,-33.4223"/>
+<polygon fill="#000000" stroke="#000000" points="2851.6339,-31.6292 2846.3375,-31.5246 2850.4126,-34.9093 2851.6339,-31.6292"/>
+</g>
+<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;strings -->
+<g id="edge214" class="edge">
+<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M4114.6463,-105.9769C4760.3299,-86.6905 6750.839,-27.2346 7027.8701,-18.9597"/>
+<polygon fill="#000000" stroke="#000000" points="7028.0233,-20.706 7032.9687,-18.8074 7027.9187,-17.2076 7028.0233,-20.706"/>
+</g>
+<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;strconv -->
+<g id="edge213" class="edge">
+<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M3889.2844,-93.8759C3868.8651,-78.2709 3839.3729,-55.7321 3817.8386,-39.275"/>
+<polygon fill="#000000" stroke="#000000" points="3818.6782,-37.7141 3813.6428,-36.0685 3816.5529,-40.495 3818.6782,-37.7141"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/fs&#45;&gt;fmt -->
+<g id="edge241" class="edge">
+<title>github.com/prometheus/procfs/internal/fs&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1230.1613,-95.237C1235.1691,-94.7743 1240.1307,-94.3581 1245,-94 2088.3759,-31.9721 4204.4538,-49.4906 5050,-36 5463.8003,-29.3979 5966.7728,-20.8017 6097.647,-18.556"/>
+<polygon fill="#000000" stroke="#000000" points="6097.9043,-20.302 6102.8735,-18.4663 6097.8442,-16.8025 6097.9043,-20.302"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/fs&#45;&gt;os -->
+<g id="edge242" class="edge">
+<title>github.com/prometheus/procfs/internal/fs&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M1072.5589,-93.8759C1037.5929,-76.4854 985.3125,-50.4835 951.9372,-33.8841"/>
+<polygon fill="#000000" stroke="#000000" points="952.4107,-32.1652 947.1545,-31.5054 950.8521,-35.299 952.4107,-32.1652"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/fs&#45;&gt;path/filepath -->
+<g id="edge243" class="edge">
+<title>github.com/prometheus/procfs/internal/fs&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M1010.328,-93.9457C937.418,-79.9477 835.9676,-59.0895 748,-36 744.9041,-35.1874 741.7267,-34.3093 738.5325,-33.3933"/>
+<polygon fill="#000000" stroke="#000000" points="738.7985,-31.648 733.5084,-31.9266 737.8177,-35.0077 738.7985,-31.648"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;bytes -->
+<g id="edge244" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M1259.4257,-95.0623C1156.643,-80.746 1007.5985,-58.9513 878,-36 859.4922,-32.7224 838.9048,-28.5409 822.2987,-25.0334"/>
+<polygon fill="#000000" stroke="#000000" points="822.5341,-23.2944 817.2797,-23.9682 821.8074,-26.7181 822.5341,-23.2944"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;strings -->
+<g id="edge248" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M1510.69,-99.1423C1534.9898,-97.078 1560.269,-95.2214 1584,-94 3620.1343,10.8012 4132.446,-69.5255 6171,-36 6505.6463,-30.4965 6911.1019,-21.4043 7027.4023,-18.7495"/>
+<polygon fill="#000000" stroke="#000000" points="7027.7618,-20.4918 7032.7204,-18.628 7027.6817,-16.9927 7027.7618,-20.4918"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;io/ioutil -->
+<g id="edge245" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M1259.3559,-95.5184C1254.1718,-94.9783 1249.0372,-94.469 1244,-94 806.4688,-53.2649 692.1238,-97.3209 257,-36 242.0596,-33.8945 225.7781,-30.4472 211.8267,-27.13"/>
+<polygon fill="#000000" stroke="#000000" points="212.07,-25.3885 206.7989,-25.9165 211.2487,-28.7908 212.07,-25.3885"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;strconv -->
+<g id="edge247" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;strconv</title>
+<path fill="none" stroke="#000000" d="M1510.7137,-99.5584C1535.0113,-97.4567 1560.2837,-95.48 1584,-94 2033.1948,-65.9688 3515.2882,-25.3937 3756.6419,-18.8932"/>
+<polygon fill="#000000" stroke="#000000" points="3756.8296,-20.6388 3761.7807,-18.7548 3756.7353,-17.1401 3756.8296,-20.6388"/>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;os -->
+<g id="edge246" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M1295.8935,-93.9871C1191.2879,-72.841 1023.1476,-38.8514 952.7045,-24.6112"/>
+<polygon fill="#000000" stroke="#000000" points="952.6484,-22.8146 947.4007,-23.5391 951.9548,-26.2452 952.6484,-22.8146"/>
+</g>
+<!-- syscall -->
+<g id="node72" class="node">
+<title>syscall</title>
+<g id="a_node72"><a xlink:href="https://godoc.org/syscall" xlink:title="syscall" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1400,-36C1400,-36 1370,-36 1370,-36 1364,-36 1358,-30 1358,-24 1358,-24 1358,-12 1358,-12 1358,-6 1364,0 1370,0 1370,0 1400,0 1400,0 1406,0 1412,-6 1412,-12 1412,-12 1412,-24 1412,-24 1412,-30 1406,-36 1400,-36"/>
+<text text-anchor="middle" x="1385" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">syscall</text>
+</a>
+</g>
+</g>
+<!-- github.com/prometheus/procfs/internal/util&#45;&gt;syscall -->
+<g id="edge249" class="edge">
+<title>github.com/prometheus/procfs/internal/util&#45;&gt;syscall</title>
+<path fill="none" stroke="#000000" d="M1385,-93.8759C1385,-78.9211 1385,-57.5983 1385,-41.3629"/>
+<polygon fill="#000000" stroke="#000000" points="1386.7501,-41.0685 1385,-36.0685 1383.2501,-41.0685 1386.7501,-41.0685"/>
+</g>
+</g>
+</svg>
diff --git a/images/dot/containerd.dot b/images/dot/containerd.dot
new file mode 100644
index 0000000..f674396
--- /dev/null
+++ b/images/dot/containerd.dot
@@ -0,0 +1,316 @@
+digraph godep {
+nodesep=0.4
+ranksep=0.8
+node [shape="box",style="rounded,filled"]
+edge [arrowsize="0.5"]
+"bufio" [label="bufio" color="palegreen" URL="https://godoc.org/bufio" target="_blank"];
+"bytes" [label="bytes" color="palegreen" URL="https://godoc.org/bytes" target="_blank"];
+"compress/gzip" [label="compress/gzip" color="palegreen" URL="https://godoc.org/compress/gzip" target="_blank"];
+"container/list" [label="container/list" color="palegreen" URL="https://godoc.org/container/list" target="_blank"];
+"context" [label="context" color="palegreen" URL="https://godoc.org/context" target="_blank"];
+"crypto" [label="crypto" color="palegreen" URL="https://godoc.org/crypto" target="_blank"];
+"encoding" [label="encoding" color="palegreen" URL="https://godoc.org/encoding" target="_blank"];
+"encoding/base64" [label="encoding/base64" color="palegreen" URL="https://godoc.org/encoding/base64" target="_blank"];
+"encoding/json" [label="encoding/json" color="palegreen" URL="https://godoc.org/encoding/json" target="_blank"];
+"errors" [label="errors" color="palegreen" URL="https://godoc.org/errors" target="_blank"];
+"fmt" [label="fmt" color="palegreen" URL="https://godoc.org/fmt" target="_blank"];
+"github.com/containerd/containerd/archive/compression" [label="github.com/containerd/containerd/archive/compression" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/archive/compression" target="_blank"];
+"github.com/containerd/containerd/archive/compression" -> "bufio";
+"github.com/containerd/containerd/archive/compression" -> "bytes";
+"github.com/containerd/containerd/archive/compression" -> "compress/gzip";
+"github.com/containerd/containerd/archive/compression" -> "context";
+"github.com/containerd/containerd/archive/compression" -> "fmt";
+"github.com/containerd/containerd/archive/compression" -> "github.com/containerd/containerd/log";
+"github.com/containerd/containerd/archive/compression" -> "io";
+"github.com/containerd/containerd/archive/compression" -> "os";
+"github.com/containerd/containerd/archive/compression" -> "os/exec";
+"github.com/containerd/containerd/archive/compression" -> "strconv";
+"github.com/containerd/containerd/archive/compression" -> "sync";
+"github.com/containerd/containerd/content" [label="github.com/containerd/containerd/content" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/content" target="_blank"];
+"github.com/containerd/containerd/content" -> "context";
+"github.com/containerd/containerd/content" -> "github.com/containerd/containerd/errdefs";
+"github.com/containerd/containerd/content" -> "github.com/opencontainers/go-digest";
+"github.com/containerd/containerd/content" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containerd/containerd/content" -> "github.com/pkg/errors";
+"github.com/containerd/containerd/content" -> "io";
+"github.com/containerd/containerd/content" -> "io/ioutil";
+"github.com/containerd/containerd/content" -> "math/rand";
+"github.com/containerd/containerd/content" -> "sync";
+"github.com/containerd/containerd/content" -> "time";
+"github.com/containerd/containerd/errdefs" [label="github.com/containerd/containerd/errdefs" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/errdefs" target="_blank"];
+"github.com/containerd/containerd/errdefs" -> "context";
+"github.com/containerd/containerd/errdefs" -> "github.com/pkg/errors";
+"github.com/containerd/containerd/errdefs" -> "google.golang.org/grpc/codes";
+"github.com/containerd/containerd/errdefs" -> "google.golang.org/grpc/status";
+"github.com/containerd/containerd/errdefs" -> "strings";
+"github.com/containerd/containerd/images" [label="github.com/containerd/containerd/images" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/images" target="_blank"];
+"github.com/containerd/containerd/images" -> "context";
+"github.com/containerd/containerd/images" -> "encoding/json";
+"github.com/containerd/containerd/images" -> "fmt";
+"github.com/containerd/containerd/images" -> "github.com/containerd/containerd/content";
+"github.com/containerd/containerd/images" -> "github.com/containerd/containerd/errdefs";
+"github.com/containerd/containerd/images" -> "github.com/containerd/containerd/log";
+"github.com/containerd/containerd/images" -> "github.com/containerd/containerd/platforms";
+"github.com/containerd/containerd/images" -> "github.com/opencontainers/go-digest";
+"github.com/containerd/containerd/images" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containerd/containerd/images" -> "github.com/pkg/errors";
+"github.com/containerd/containerd/images" -> "golang.org/x/sync/errgroup";
+"github.com/containerd/containerd/images" -> "golang.org/x/sync/semaphore";
+"github.com/containerd/containerd/images" -> "io";
+"github.com/containerd/containerd/images" -> "sort";
+"github.com/containerd/containerd/images" -> "strings";
+"github.com/containerd/containerd/images" -> "time";
+"github.com/containerd/containerd/labels" [label="github.com/containerd/containerd/labels" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/labels" target="_blank"];
+"github.com/containerd/containerd/labels" -> "github.com/containerd/containerd/errdefs";
+"github.com/containerd/containerd/labels" -> "github.com/pkg/errors";
+"github.com/containerd/containerd/log" [label="github.com/containerd/containerd/log" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/log" target="_blank"];
+"github.com/containerd/containerd/log" -> "context";
+"github.com/containerd/containerd/log" -> "github.com/sirupsen/logrus";
+"github.com/containerd/containerd/log" -> "sync/atomic";
+"github.com/containerd/containerd/platforms" [label="github.com/containerd/containerd/platforms" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/platforms" target="_blank"];
+"github.com/containerd/containerd/platforms" -> "bufio";
+"github.com/containerd/containerd/platforms" -> "github.com/containerd/containerd/errdefs";
+"github.com/containerd/containerd/platforms" -> "github.com/containerd/containerd/log";
+"github.com/containerd/containerd/platforms" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containerd/containerd/platforms" -> "github.com/pkg/errors";
+"github.com/containerd/containerd/platforms" -> "os";
+"github.com/containerd/containerd/platforms" -> "regexp";
+"github.com/containerd/containerd/platforms" -> "runtime";
+"github.com/containerd/containerd/platforms" -> "strconv";
+"github.com/containerd/containerd/platforms" -> "strings";
+"github.com/containerd/containerd/reference" [label="github.com/containerd/containerd/reference" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/reference" target="_blank"];
+"github.com/containerd/containerd/reference" -> "errors";
+"github.com/containerd/containerd/reference" -> "fmt";
+"github.com/containerd/containerd/reference" -> "github.com/opencontainers/go-digest";
+"github.com/containerd/containerd/reference" -> "net/url";
+"github.com/containerd/containerd/reference" -> "path";
+"github.com/containerd/containerd/reference" -> "regexp";
+"github.com/containerd/containerd/reference" -> "strings";
+"github.com/containerd/containerd/remotes" [label="github.com/containerd/containerd/remotes" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/remotes" target="_blank"];
+"github.com/containerd/containerd/remotes" -> "context";
+"github.com/containerd/containerd/remotes" -> "fmt";
+"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/content";
+"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/errdefs";
+"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/images";
+"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/log";
+"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/platforms";
+"github.com/containerd/containerd/remotes" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containerd/containerd/remotes" -> "github.com/pkg/errors";
+"github.com/containerd/containerd/remotes" -> "github.com/sirupsen/logrus";
+"github.com/containerd/containerd/remotes" -> "io";
+"github.com/containerd/containerd/remotes" -> "strings";
+"github.com/containerd/containerd/remotes" -> "sync";
+"github.com/containerd/containerd/remotes/docker" [label="github.com/containerd/containerd/remotes/docker" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/remotes/docker" target="_blank"];
+"github.com/containerd/containerd/remotes/docker" -> "bytes";
+"github.com/containerd/containerd/remotes/docker" -> "context";
+"github.com/containerd/containerd/remotes/docker" -> "encoding/base64";
+"github.com/containerd/containerd/remotes/docker" -> "encoding/json";
+"github.com/containerd/containerd/remotes/docker" -> "fmt";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/content";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/errdefs";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/images";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/labels";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/log";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/reference";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/remotes";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/remotes/docker/schema1";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/version";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/docker/distribution/registry/api/errcode";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/opencontainers/go-digest";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/pkg/errors";
+"github.com/containerd/containerd/remotes/docker" -> "github.com/sirupsen/logrus";
+"github.com/containerd/containerd/remotes/docker" -> "golang.org/x/net/context/ctxhttp";
+"github.com/containerd/containerd/remotes/docker" -> "io";
+"github.com/containerd/containerd/remotes/docker" -> "io/ioutil";
+"github.com/containerd/containerd/remotes/docker" -> "net/http";
+"github.com/containerd/containerd/remotes/docker" -> "net/url";
+"github.com/containerd/containerd/remotes/docker" -> "path";
+"github.com/containerd/containerd/remotes/docker" -> "sort";
+"github.com/containerd/containerd/remotes/docker" -> "strings";
+"github.com/containerd/containerd/remotes/docker" -> "sync";
+"github.com/containerd/containerd/remotes/docker" -> "time";
+"github.com/containerd/containerd/remotes/docker/schema1" [label="github.com/containerd/containerd/remotes/docker/schema1" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/remotes/docker/schema1" target="_blank"];
+"github.com/containerd/containerd/remotes/docker/schema1" -> "bytes";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "context";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "encoding/base64";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "encoding/json";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "fmt";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/archive/compression";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/content";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/errdefs";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/images";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/log";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/remotes";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/opencontainers/go-digest";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/opencontainers/image-spec/specs-go";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/pkg/errors";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "golang.org/x/sync/errgroup";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "io";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "io/ioutil";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "strconv";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "strings";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "sync";
+"github.com/containerd/containerd/remotes/docker/schema1" -> "time";
+"github.com/containerd/containerd/version" [label="github.com/containerd/containerd/version" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/version" target="_blank"];
+"github.com/docker/distribution/registry/api/errcode" [label="github.com/docker/distribution/registry/api/errcode" color="palegoldenrod" URL="https://godoc.org/github.com/docker/distribution/registry/api/errcode" target="_blank"];
+"github.com/docker/distribution/registry/api/errcode" -> "encoding/json";
+"github.com/docker/distribution/registry/api/errcode" -> "fmt";
+"github.com/docker/distribution/registry/api/errcode" -> "net/http";
+"github.com/docker/distribution/registry/api/errcode" -> "sort";
+"github.com/docker/distribution/registry/api/errcode" -> "strings";
+"github.com/docker/distribution/registry/api/errcode" -> "sync";
+"github.com/golang/protobuf/proto" [label="github.com/golang/protobuf/proto" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/proto" target="_blank"];
+"github.com/golang/protobuf/proto" -> "bufio";
+"github.com/golang/protobuf/proto" -> "bytes";
+"github.com/golang/protobuf/proto" -> "encoding";
+"github.com/golang/protobuf/proto" -> "encoding/json";
+"github.com/golang/protobuf/proto" -> "errors";
+"github.com/golang/protobuf/proto" -> "fmt";
+"github.com/golang/protobuf/proto" -> "io";
+"github.com/golang/protobuf/proto" -> "log";
+"github.com/golang/protobuf/proto" -> "math";
+"github.com/golang/protobuf/proto" -> "os";
+"github.com/golang/protobuf/proto" -> "reflect";
+"github.com/golang/protobuf/proto" -> "sort";
+"github.com/golang/protobuf/proto" -> "strconv";
+"github.com/golang/protobuf/proto" -> "strings";
+"github.com/golang/protobuf/proto" -> "sync";
+"github.com/golang/protobuf/proto" -> "sync/atomic";
+"github.com/golang/protobuf/proto" -> "unicode/utf8";
+"github.com/golang/protobuf/proto" -> "unsafe";
+"github.com/golang/protobuf/ptypes" [label="github.com/golang/protobuf/ptypes" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/ptypes" target="_blank"];
+"github.com/golang/protobuf/ptypes" -> "errors";
+"github.com/golang/protobuf/ptypes" -> "fmt";
+"github.com/golang/protobuf/ptypes" -> "github.com/golang/protobuf/proto";
+"github.com/golang/protobuf/ptypes" -> "github.com/golang/protobuf/ptypes/any";
+"github.com/golang/protobuf/ptypes" -> "github.com/golang/protobuf/ptypes/duration";
+"github.com/golang/protobuf/ptypes" -> "github.com/golang/protobuf/ptypes/timestamp";
+"github.com/golang/protobuf/ptypes" -> "reflect";
+"github.com/golang/protobuf/ptypes" -> "strings";
+"github.com/golang/protobuf/ptypes" -> "time";
+"github.com/golang/protobuf/ptypes/any" [label="github.com/golang/protobuf/ptypes/any" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/ptypes/any" target="_blank"];
+"github.com/golang/protobuf/ptypes/any" -> "fmt";
+"github.com/golang/protobuf/ptypes/any" -> "github.com/golang/protobuf/proto";
+"github.com/golang/protobuf/ptypes/any" -> "math";
+"github.com/golang/protobuf/ptypes/duration" [label="github.com/golang/protobuf/ptypes/duration" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/ptypes/duration" target="_blank"];
+"github.com/golang/protobuf/ptypes/duration" -> "fmt";
+"github.com/golang/protobuf/ptypes/duration" -> "github.com/golang/protobuf/proto";
+"github.com/golang/protobuf/ptypes/duration" -> "math";
+"github.com/golang/protobuf/ptypes/timestamp" [label="github.com/golang/protobuf/ptypes/timestamp" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/ptypes/timestamp" target="_blank"];
+"github.com/golang/protobuf/ptypes/timestamp" -> "fmt";
+"github.com/golang/protobuf/ptypes/timestamp" -> "github.com/golang/protobuf/proto";
+"github.com/golang/protobuf/ptypes/timestamp" -> "math";
+"github.com/opencontainers/go-digest" [label="github.com/opencontainers/go-digest" color="palegoldenrod" URL="https://godoc.org/github.com/opencontainers/go-digest" target="_blank"];
+"github.com/opencontainers/go-digest" -> "crypto";
+"github.com/opencontainers/go-digest" -> "fmt";
+"github.com/opencontainers/go-digest" -> "hash";
+"github.com/opencontainers/go-digest" -> "io";
+"github.com/opencontainers/go-digest" -> "regexp";
+"github.com/opencontainers/go-digest" -> "strings";
+"github.com/opencontainers/image-spec/specs-go" [label="github.com/opencontainers/image-spec/specs-go" color="palegoldenrod" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go" target="_blank"];
+"github.com/opencontainers/image-spec/specs-go" -> "fmt";
+"github.com/opencontainers/image-spec/specs-go/v1" [label="github.com/opencontainers/image-spec/specs-go/v1" color="palegoldenrod" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" target="_blank"];
+"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/go-digest";
+"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/image-spec/specs-go";
+"github.com/opencontainers/image-spec/specs-go/v1" -> "time";
+"github.com/pkg/errors" [label="github.com/pkg/errors" color="palegoldenrod" URL="https://godoc.org/github.com/pkg/errors" target="_blank"];
+"github.com/pkg/errors" -> "fmt";
+"github.com/pkg/errors" -> "io";
+"github.com/pkg/errors" -> "path";
+"github.com/pkg/errors" -> "runtime";
+"github.com/pkg/errors" -> "strings";
+"github.com/sirupsen/logrus" [label="github.com/sirupsen/logrus" color="palegoldenrod" URL="https://godoc.org/github.com/sirupsen/logrus" target="_blank"];
+"github.com/sirupsen/logrus" -> "bufio";
+"github.com/sirupsen/logrus" -> "bytes";
+"github.com/sirupsen/logrus" -> "context";
+"github.com/sirupsen/logrus" -> "encoding/json";
+"github.com/sirupsen/logrus" -> "fmt";
+"github.com/sirupsen/logrus" -> "golang.org/x/sys/unix";
+"github.com/sirupsen/logrus" -> "io";
+"github.com/sirupsen/logrus" -> "log";
+"github.com/sirupsen/logrus" -> "os";
+"github.com/sirupsen/logrus" -> "reflect";
+"github.com/sirupsen/logrus" -> "runtime";
+"github.com/sirupsen/logrus" -> "sort";
+"github.com/sirupsen/logrus" -> "strings";
+"github.com/sirupsen/logrus" -> "sync";
+"github.com/sirupsen/logrus" -> "sync/atomic";
+"github.com/sirupsen/logrus" -> "time";
+"golang.org/x/net/context/ctxhttp" [label="golang.org/x/net/context/ctxhttp" color="palegoldenrod" URL="https://godoc.org/golang.org/x/net/context/ctxhttp" target="_blank"];
+"golang.org/x/net/context/ctxhttp" -> "context";
+"golang.org/x/net/context/ctxhttp" -> "io";
+"golang.org/x/net/context/ctxhttp" -> "net/http";
+"golang.org/x/net/context/ctxhttp" -> "net/url";
+"golang.org/x/net/context/ctxhttp" -> "strings";
+"golang.org/x/sync/errgroup" [label="golang.org/x/sync/errgroup" color="palegoldenrod" URL="https://godoc.org/golang.org/x/sync/errgroup" target="_blank"];
+"golang.org/x/sync/errgroup" -> "context";
+"golang.org/x/sync/errgroup" -> "sync";
+"golang.org/x/sync/semaphore" [label="golang.org/x/sync/semaphore" color="palegoldenrod" URL="https://godoc.org/golang.org/x/sync/semaphore" target="_blank"];
+"golang.org/x/sync/semaphore" -> "container/list";
+"golang.org/x/sync/semaphore" -> "context";
+"golang.org/x/sync/semaphore" -> "sync";
+"golang.org/x/sys/unix" [label="golang.org/x/sys/unix" color="paleturquoise" URL="https://godoc.org/golang.org/x/sys/unix" target="_blank"];
+"golang.org/x/sys/unix" -> "bytes";
+"golang.org/x/sys/unix" -> "runtime";
+"golang.org/x/sys/unix" -> "sort";
+"golang.org/x/sys/unix" -> "strings";
+"golang.org/x/sys/unix" -> "sync";
+"golang.org/x/sys/unix" -> "syscall";
+"golang.org/x/sys/unix" -> "time";
+"golang.org/x/sys/unix" -> "unsafe";
+"google.golang.org/genproto/googleapis/rpc/status" [label="google.golang.org/genproto/googleapis/rpc/status" color="paleturquoise" URL="https://godoc.org/google.golang.org/genproto/googleapis/rpc/status" target="_blank"];
+"google.golang.org/genproto/googleapis/rpc/status" -> "fmt";
+"google.golang.org/genproto/googleapis/rpc/status" -> "github.com/golang/protobuf/proto";
+"google.golang.org/genproto/googleapis/rpc/status" -> "github.com/golang/protobuf/ptypes/any";
+"google.golang.org/genproto/googleapis/rpc/status" -> "math";
+"google.golang.org/grpc/codes" [label="google.golang.org/grpc/codes" color="palegoldenrod" URL="https://godoc.org/google.golang.org/grpc/codes" target="_blank"];
+"google.golang.org/grpc/codes" -> "fmt";
+"google.golang.org/grpc/codes" -> "strconv";
+"google.golang.org/grpc/connectivity" [label="google.golang.org/grpc/connectivity" color="paleturquoise" URL="https://godoc.org/google.golang.org/grpc/connectivity" target="_blank"];
+"google.golang.org/grpc/connectivity" -> "context";
+"google.golang.org/grpc/connectivity" -> "google.golang.org/grpc/grpclog";
+"google.golang.org/grpc/grpclog" [label="google.golang.org/grpc/grpclog" color="paleturquoise" URL="https://godoc.org/google.golang.org/grpc/grpclog" target="_blank"];
+"google.golang.org/grpc/grpclog" -> "io";
+"google.golang.org/grpc/grpclog" -> "io/ioutil";
+"google.golang.org/grpc/grpclog" -> "log";
+"google.golang.org/grpc/grpclog" -> "os";
+"google.golang.org/grpc/grpclog" -> "strconv";
+"google.golang.org/grpc/internal" [label="google.golang.org/grpc/internal" color="paleturquoise" URL="https://godoc.org/google.golang.org/grpc/internal" target="_blank"];
+"google.golang.org/grpc/internal" -> "context";
+"google.golang.org/grpc/internal" -> "google.golang.org/grpc/connectivity";
+"google.golang.org/grpc/internal" -> "time";
+"google.golang.org/grpc/status" [label="google.golang.org/grpc/status" color="palegoldenrod" URL="https://godoc.org/google.golang.org/grpc/status" target="_blank"];
+"google.golang.org/grpc/status" -> "context";
+"google.golang.org/grpc/status" -> "errors";
+"google.golang.org/grpc/status" -> "fmt";
+"google.golang.org/grpc/status" -> "github.com/golang/protobuf/proto";
+"google.golang.org/grpc/status" -> "github.com/golang/protobuf/ptypes";
+"google.golang.org/grpc/status" -> "google.golang.org/genproto/googleapis/rpc/status";
+"google.golang.org/grpc/status" -> "google.golang.org/grpc/codes";
+"google.golang.org/grpc/status" -> "google.golang.org/grpc/internal";
+"hash" [label="hash" color="palegreen" URL="https://godoc.org/hash" target="_blank"];
+"io" [label="io" color="palegreen" URL="https://godoc.org/io" target="_blank"];
+"io/ioutil" [label="io/ioutil" color="palegreen" URL="https://godoc.org/io/ioutil" target="_blank"];
+"log" [label="log" color="palegreen" URL="https://godoc.org/log" target="_blank"];
+"math" [label="math" color="palegreen" URL="https://godoc.org/math" target="_blank"];
+"math/rand" [label="math/rand" color="palegreen" URL="https://godoc.org/math/rand" target="_blank"];
+"net/http" [label="net/http" color="palegreen" URL="https://godoc.org/net/http" target="_blank"];
+"net/url" [label="net/url" color="palegreen" URL="https://godoc.org/net/url" target="_blank"];
+"os" [label="os" color="palegreen" URL="https://godoc.org/os" target="_blank"];
+"os/exec" [label="os/exec" color="palegreen" URL="https://godoc.org/os/exec" target="_blank"];
+"path" [label="path" color="palegreen" URL="https://godoc.org/path" target="_blank"];
+"reflect" [label="reflect" color="palegreen" URL="https://godoc.org/reflect" target="_blank"];
+"regexp" [label="regexp" color="palegreen" URL="https://godoc.org/regexp" target="_blank"];
+"runtime" [label="runtime" color="palegreen" URL="https://godoc.org/runtime" target="_blank"];
+"sort" [label="sort" color="palegreen" URL="https://godoc.org/sort" target="_blank"];
+"strconv" [label="strconv" color="palegreen" URL="https://godoc.org/strconv" target="_blank"];
+"strings" [label="strings" color="palegreen" URL="https://godoc.org/strings" target="_blank"];
+"sync" [label="sync" color="palegreen" URL="https://godoc.org/sync" target="_blank"];
+"sync/atomic" [label="sync/atomic" color="palegreen" URL="https://godoc.org/sync/atomic" target="_blank"];
+"syscall" [label="syscall" color="palegreen" URL="https://godoc.org/syscall" target="_blank"];
+"time" [label="time" color="palegreen" URL="https://godoc.org/time" target="_blank"];
+"unicode/utf8" [label="unicode/utf8" color="palegreen" URL="https://godoc.org/unicode/utf8" target="_blank"];
+"unsafe" [label="unsafe" color="palegreen" URL="https://godoc.org/unsafe" target="_blank"];
+}
diff --git a/images/dot/containers.dot b/images/dot/containers.dot
new file mode 100644
index 0000000..3e53f84
--- /dev/null
+++ b/images/dot/containers.dot
@@ -0,0 +1,831 @@
+digraph godep {
+rankdir="LR"
+nodesep=0.4
+ranksep=0.8
+node [shape="box",style="rounded,filled"]
+edge [arrowsize="0.5"]
+"bufio" [label="bufio" color="palegreen" URL="https://godoc.org/bufio" target="_blank"];
+"bytes" [label="bytes" color="palegreen" URL="https://godoc.org/bytes" target="_blank"];
+"compress/bzip2" [label="compress/bzip2" color="palegreen" URL="https://godoc.org/compress/bzip2" target="_blank"];
+"compress/gzip" [label="compress/gzip" color="palegreen" URL="https://godoc.org/compress/gzip" target="_blank"];
+"context" [label="context" color="palegreen" URL="https://godoc.org/context" target="_blank"];
+"crypto" [label="crypto" color="palegreen" URL="https://godoc.org/crypto" target="_blank"];
+"crypto/ecdsa" [label="crypto/ecdsa" color="palegreen" URL="https://godoc.org/crypto/ecdsa" target="_blank"];
+"crypto/elliptic" [label="crypto/elliptic" color="palegreen" URL="https://godoc.org/crypto/elliptic" target="_blank"];
+"crypto/rand" [label="crypto/rand" color="palegreen" URL="https://godoc.org/crypto/rand" target="_blank"];
+"crypto/rsa" [label="crypto/rsa" color="palegreen" URL="https://godoc.org/crypto/rsa" target="_blank"];
+"crypto/sha256" [label="crypto/sha256" color="palegreen" URL="https://godoc.org/crypto/sha256" target="_blank"];
+"crypto/sha512" [label="crypto/sha512" color="palegreen" URL="https://godoc.org/crypto/sha512" target="_blank"];
+"crypto/tls" [label="crypto/tls" color="palegreen" URL="https://godoc.org/crypto/tls" target="_blank"];
+"crypto/x509" [label="crypto/x509" color="palegreen" URL="https://godoc.org/crypto/x509" target="_blank"];
+"crypto/x509/pkix" [label="crypto/x509/pkix" color="palegreen" URL="https://godoc.org/crypto/x509/pkix" target="_blank"];
+"encoding" [label="encoding" color="palegreen" URL="https://godoc.org/encoding" target="_blank"];
+"encoding/base32" [label="encoding/base32" color="palegreen" URL="https://godoc.org/encoding/base32" target="_blank"];
+"encoding/base64" [label="encoding/base64" color="palegreen" URL="https://godoc.org/encoding/base64" target="_blank"];
+"encoding/binary" [label="encoding/binary" color="palegreen" URL="https://godoc.org/encoding/binary" target="_blank"];
+"encoding/hex" [label="encoding/hex" color="palegreen" URL="https://godoc.org/encoding/hex" target="_blank"];
+"encoding/json" [label="encoding/json" color="palegreen" URL="https://godoc.org/encoding/json" target="_blank"];
+"encoding/pem" [label="encoding/pem" color="palegreen" URL="https://godoc.org/encoding/pem" target="_blank"];
+"errors" [label="errors" color="palegreen" URL="https://godoc.org/errors" target="_blank"];
+"expvar" [label="expvar" color="palegreen" URL="https://godoc.org/expvar" target="_blank"];
+"fmt" [label="fmt" color="palegreen" URL="https://godoc.org/fmt" target="_blank"];
+"github.com/BurntSushi/toml" [label="github.com/BurntSushi/toml" color="paleturquoise" URL="https://godoc.org/github.com/BurntSushi/toml" target="_blank"];
+"github.com/BurntSushi/toml" -> "bufio";
+"github.com/BurntSushi/toml" -> "encoding";
+"github.com/BurntSushi/toml" -> "errors";
+"github.com/BurntSushi/toml" -> "fmt";
+"github.com/BurntSushi/toml" -> "io";
+"github.com/BurntSushi/toml" -> "io/ioutil";
+"github.com/BurntSushi/toml" -> "math";
+"github.com/BurntSushi/toml" -> "reflect";
+"github.com/BurntSushi/toml" -> "sort";
+"github.com/BurntSushi/toml" -> "strconv";
+"github.com/BurntSushi/toml" -> "strings";
+"github.com/BurntSushi/toml" -> "sync";
+"github.com/BurntSushi/toml" -> "time";
+"github.com/BurntSushi/toml" -> "unicode";
+"github.com/BurntSushi/toml" -> "unicode/utf8";
+"github.com/beorn7/perks/quantile" [label="github.com/beorn7/perks/quantile" color="paleturquoise" URL="https://godoc.org/github.com/beorn7/perks/quantile" target="_blank"];
+"github.com/beorn7/perks/quantile" -> "math";
+"github.com/beorn7/perks/quantile" -> "sort";
+"github.com/cespare/xxhash/v2" [label="github.com/cespare/xxhash/v2" color="paleturquoise" URL="https://godoc.org/github.com/cespare/xxhash/v2" target="_blank"];
+"github.com/cespare/xxhash/v2" -> "encoding/binary";
+"github.com/cespare/xxhash/v2" -> "errors";
+"github.com/cespare/xxhash/v2" -> "math/bits";
+"github.com/cespare/xxhash/v2" -> "reflect";
+"github.com/cespare/xxhash/v2" -> "unsafe";
+"github.com/containers/image/docker" [label="github.com/containers/image/docker" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/docker" target="_blank"];
+"github.com/containers/image/docker" -> "bytes";
+"github.com/containers/image/docker" -> "context";
+"github.com/containers/image/docker" -> "crypto/rand";
+"github.com/containers/image/docker" -> "crypto/tls";
+"github.com/containers/image/docker" -> "encoding/json";
+"github.com/containers/image/docker" -> "errors";
+"github.com/containers/image/docker" -> "fmt";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/docker/policyconfiguration";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/docker/reference";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/image";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/manifest";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/pkg/blobinfocache/none";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/pkg/docker/config";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/pkg/sysregistriesv2";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/pkg/tlsclientconfig";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/transports";
+"github.com/containers/image/docker" -> "github.com/containers/image/v5/types";
+"github.com/containers/image/docker" -> "github.com/docker/distribution/registry/api/errcode";
+"github.com/containers/image/docker" -> "github.com/docker/distribution/registry/api/v2";
+"github.com/containers/image/docker" -> "github.com/docker/distribution/registry/client";
+"github.com/containers/image/docker" -> "github.com/docker/go-connections/tlsconfig";
+"github.com/containers/image/docker" -> "github.com/ghodss/yaml";
+"github.com/containers/image/docker" -> "github.com/opencontainers/go-digest";
+"github.com/containers/image/docker" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containers/image/docker" -> "github.com/pkg/errors";
+"github.com/containers/image/docker" -> "github.com/sirupsen/logrus";
+"github.com/containers/image/docker" -> "io";
+"github.com/containers/image/docker" -> "io/ioutil";
+"github.com/containers/image/docker" -> "mime";
+"github.com/containers/image/docker" -> "net/http";
+"github.com/containers/image/docker" -> "net/url";
+"github.com/containers/image/docker" -> "os";
+"github.com/containers/image/docker" -> "path";
+"github.com/containers/image/docker" -> "path/filepath";
+"github.com/containers/image/docker" -> "strconv";
+"github.com/containers/image/docker" -> "strings";
+"github.com/containers/image/docker" -> "sync";
+"github.com/containers/image/docker" -> "time";
+"github.com/containers/image/v5/docker/policyconfiguration" [label="github.com/containers/image/v5/docker/policyconfiguration" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/docker/policyconfiguration" target="_blank"];
+"github.com/containers/image/v5/docker/policyconfiguration" -> "github.com/containers/image/v5/docker/reference";
+"github.com/containers/image/v5/docker/policyconfiguration" -> "github.com/pkg/errors";
+"github.com/containers/image/v5/docker/policyconfiguration" -> "strings";
+"github.com/containers/image/v5/docker/reference" [label="github.com/containers/image/v5/docker/reference" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/docker/reference" target="_blank"];
+"github.com/containers/image/v5/docker/reference" -> "errors";
+"github.com/containers/image/v5/docker/reference" -> "fmt";
+"github.com/containers/image/v5/docker/reference" -> "github.com/opencontainers/go-digest";
+"github.com/containers/image/v5/docker/reference" -> "path";
+"github.com/containers/image/v5/docker/reference" -> "regexp";
+"github.com/containers/image/v5/docker/reference" -> "strings";
+"github.com/containers/image/v5/image" [label="github.com/containers/image/v5/image" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/image" target="_blank"];
+"github.com/containers/image/v5/image" -> "bytes";
+"github.com/containers/image/v5/image" -> "context";
+"github.com/containers/image/v5/image" -> "crypto/sha256";
+"github.com/containers/image/v5/image" -> "encoding/hex";
+"github.com/containers/image/v5/image" -> "encoding/json";
+"github.com/containers/image/v5/image" -> "fmt";
+"github.com/containers/image/v5/image" -> "github.com/containers/image/v5/docker/reference";
+"github.com/containers/image/v5/image" -> "github.com/containers/image/v5/manifest";
+"github.com/containers/image/v5/image" -> "github.com/containers/image/v5/pkg/blobinfocache/none";
+"github.com/containers/image/v5/image" -> "github.com/containers/image/v5/types";
+"github.com/containers/image/v5/image" -> "github.com/opencontainers/go-digest";
+"github.com/containers/image/v5/image" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containers/image/v5/image" -> "github.com/pkg/errors";
+"github.com/containers/image/v5/image" -> "github.com/sirupsen/logrus";
+"github.com/containers/image/v5/image" -> "io/ioutil";
+"github.com/containers/image/v5/image" -> "strings";
+"github.com/containers/image/v5/internal/pkg/keyctl" [label="github.com/containers/image/v5/internal/pkg/keyctl" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/internal/pkg/keyctl" target="_blank"];
+"github.com/containers/image/v5/internal/pkg/keyctl" -> "golang.org/x/sys/unix";
+"github.com/containers/image/v5/internal/pkg/keyctl" -> "unsafe";
+"github.com/containers/image/v5/manifest" [label="github.com/containers/image/v5/manifest" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/manifest" target="_blank"];
+"github.com/containers/image/v5/manifest" -> "encoding/json";
+"github.com/containers/image/v5/manifest" -> "fmt";
+"github.com/containers/image/v5/manifest" -> "github.com/containers/image/v5/docker/reference";
+"github.com/containers/image/v5/manifest" -> "github.com/containers/image/v5/pkg/compression";
+"github.com/containers/image/v5/manifest" -> "github.com/containers/image/v5/pkg/strslice";
+"github.com/containers/image/v5/manifest" -> "github.com/containers/image/v5/types";
+"github.com/containers/image/v5/manifest" -> "github.com/containers/libtrust";
+"github.com/containers/image/v5/manifest" -> "github.com/containers/ocicrypt/spec";
+"github.com/containers/image/v5/manifest" -> "github.com/docker/docker/api/types/versions";
+"github.com/containers/image/v5/manifest" -> "github.com/opencontainers/go-digest";
+"github.com/containers/image/v5/manifest" -> "github.com/opencontainers/image-spec/specs-go";
+"github.com/containers/image/v5/manifest" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containers/image/v5/manifest" -> "github.com/pkg/errors";
+"github.com/containers/image/v5/manifest" -> "github.com/sirupsen/logrus";
+"github.com/containers/image/v5/manifest" -> "regexp";
+"github.com/containers/image/v5/manifest" -> "runtime";
+"github.com/containers/image/v5/manifest" -> "strings";
+"github.com/containers/image/v5/manifest" -> "time";
+"github.com/containers/image/v5/pkg/blobinfocache/none" [label="github.com/containers/image/v5/pkg/blobinfocache/none" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/blobinfocache/none" target="_blank"];
+"github.com/containers/image/v5/pkg/blobinfocache/none" -> "github.com/containers/image/v5/types";
+"github.com/containers/image/v5/pkg/blobinfocache/none" -> "github.com/opencontainers/go-digest";
+"github.com/containers/image/v5/pkg/compression" [label="github.com/containers/image/v5/pkg/compression" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/compression" target="_blank"];
+"github.com/containers/image/v5/pkg/compression" -> "bytes";
+"github.com/containers/image/v5/pkg/compression" -> "compress/bzip2";
+"github.com/containers/image/v5/pkg/compression" -> "fmt";
+"github.com/containers/image/v5/pkg/compression" -> "github.com/containers/image/v5/pkg/compression/internal";
+"github.com/containers/image/v5/pkg/compression" -> "github.com/containers/image/v5/pkg/compression/types";
+"github.com/containers/image/v5/pkg/compression" -> "github.com/klauspost/compress/zstd";
+"github.com/containers/image/v5/pkg/compression" -> "github.com/klauspost/pgzip";
+"github.com/containers/image/v5/pkg/compression" -> "github.com/pkg/errors";
+"github.com/containers/image/v5/pkg/compression" -> "github.com/sirupsen/logrus";
+"github.com/containers/image/v5/pkg/compression" -> "github.com/ulikunitz/xz";
+"github.com/containers/image/v5/pkg/compression" -> "io";
+"github.com/containers/image/v5/pkg/compression" -> "io/ioutil";
+"github.com/containers/image/v5/pkg/compression/internal" [label="github.com/containers/image/v5/pkg/compression/internal" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/compression/internal" target="_blank"];
+"github.com/containers/image/v5/pkg/compression/internal" -> "io";
+"github.com/containers/image/v5/pkg/compression/types" [label="github.com/containers/image/v5/pkg/compression/types" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/compression/types" target="_blank"];
+"github.com/containers/image/v5/pkg/compression/types" -> "github.com/containers/image/v5/pkg/compression/internal";
+"github.com/containers/image/v5/pkg/docker/config" [label="github.com/containers/image/v5/pkg/docker/config" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/docker/config" target="_blank"];
+"github.com/containers/image/v5/pkg/docker/config" -> "encoding/base64";
+"github.com/containers/image/v5/pkg/docker/config" -> "encoding/json";
+"github.com/containers/image/v5/pkg/docker/config" -> "fmt";
+"github.com/containers/image/v5/pkg/docker/config" -> "github.com/containers/image/v5/internal/pkg/keyctl";
+"github.com/containers/image/v5/pkg/docker/config" -> "github.com/containers/image/v5/types";
+"github.com/containers/image/v5/pkg/docker/config" -> "github.com/docker/docker-credential-helpers/client";
+"github.com/containers/image/v5/pkg/docker/config" -> "github.com/docker/docker-credential-helpers/credentials";
+"github.com/containers/image/v5/pkg/docker/config" -> "github.com/docker/docker/pkg/homedir";
+"github.com/containers/image/v5/pkg/docker/config" -> "github.com/pkg/errors";
+"github.com/containers/image/v5/pkg/docker/config" -> "github.com/sirupsen/logrus";
+"github.com/containers/image/v5/pkg/docker/config" -> "io/ioutil";
+"github.com/containers/image/v5/pkg/docker/config" -> "os";
+"github.com/containers/image/v5/pkg/docker/config" -> "path/filepath";
+"github.com/containers/image/v5/pkg/docker/config" -> "strings";
+"github.com/containers/image/v5/pkg/strslice" [label="github.com/containers/image/v5/pkg/strslice" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/strslice" target="_blank"];
+"github.com/containers/image/v5/pkg/strslice" -> "encoding/json";
+"github.com/containers/image/v5/pkg/sysregistriesv2" [label="github.com/containers/image/v5/pkg/sysregistriesv2" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/sysregistriesv2" target="_blank"];
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "fmt";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/BurntSushi/toml";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/containers/image/v5/docker/reference";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/containers/image/v5/types";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/pkg/errors";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/sirupsen/logrus";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "io/ioutil";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "os";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "path/filepath";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "regexp";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "strings";
+"github.com/containers/image/v5/pkg/sysregistriesv2" -> "sync";
+"github.com/containers/image/v5/pkg/tlsclientconfig" [label="github.com/containers/image/v5/pkg/tlsclientconfig" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/tlsclientconfig" target="_blank"];
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "crypto/tls";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "github.com/docker/go-connections/sockets";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "github.com/docker/go-connections/tlsconfig";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "github.com/pkg/errors";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "github.com/sirupsen/logrus";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "io/ioutil";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "net";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "net/http";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "os";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "path/filepath";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "strings";
+"github.com/containers/image/v5/pkg/tlsclientconfig" -> "time";
+"github.com/containers/image/v5/transports" [label="github.com/containers/image/v5/transports" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/transports" target="_blank"];
+"github.com/containers/image/v5/transports" -> "fmt";
+"github.com/containers/image/v5/transports" -> "github.com/containers/image/v5/types";
+"github.com/containers/image/v5/transports" -> "sort";
+"github.com/containers/image/v5/transports" -> "sync";
+"github.com/containers/image/v5/types" [label="github.com/containers/image/v5/types" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/types" target="_blank"];
+"github.com/containers/image/v5/types" -> "context";
+"github.com/containers/image/v5/types" -> "github.com/containers/image/v5/docker/reference";
+"github.com/containers/image/v5/types" -> "github.com/containers/image/v5/pkg/compression/types";
+"github.com/containers/image/v5/types" -> "github.com/opencontainers/go-digest";
+"github.com/containers/image/v5/types" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/containers/image/v5/types" -> "io";
+"github.com/containers/image/v5/types" -> "time";
+"github.com/containers/libtrust" [label="github.com/containers/libtrust" color="paleturquoise" URL="https://godoc.org/github.com/containers/libtrust" target="_blank"];
+"github.com/containers/libtrust" -> "bytes";
+"github.com/containers/libtrust" -> "crypto";
+"github.com/containers/libtrust" -> "crypto/ecdsa";
+"github.com/containers/libtrust" -> "crypto/elliptic";
+"github.com/containers/libtrust" -> "crypto/rand";
+"github.com/containers/libtrust" -> "crypto/rsa";
+"github.com/containers/libtrust" -> "crypto/sha256";
+"github.com/containers/libtrust" -> "crypto/sha512";
+"github.com/containers/libtrust" -> "crypto/tls";
+"github.com/containers/libtrust" -> "crypto/x509";
+"github.com/containers/libtrust" -> "crypto/x509/pkix";
+"github.com/containers/libtrust" -> "encoding/base32";
+"github.com/containers/libtrust" -> "encoding/base64";
+"github.com/containers/libtrust" -> "encoding/binary";
+"github.com/containers/libtrust" -> "encoding/json";
+"github.com/containers/libtrust" -> "encoding/pem";
+"github.com/containers/libtrust" -> "errors";
+"github.com/containers/libtrust" -> "fmt";
+"github.com/containers/libtrust" -> "io";
+"github.com/containers/libtrust" -> "io/ioutil";
+"github.com/containers/libtrust" -> "math/big";
+"github.com/containers/libtrust" -> "net";
+"github.com/containers/libtrust" -> "net/url";
+"github.com/containers/libtrust" -> "os";
+"github.com/containers/libtrust" -> "path";
+"github.com/containers/libtrust" -> "path/filepath";
+"github.com/containers/libtrust" -> "sort";
+"github.com/containers/libtrust" -> "strings";
+"github.com/containers/libtrust" -> "sync";
+"github.com/containers/libtrust" -> "time";
+"github.com/containers/libtrust" -> "unicode";
+"github.com/containers/ocicrypt/spec" [label="github.com/containers/ocicrypt/spec" color="paleturquoise" URL="https://godoc.org/github.com/containers/ocicrypt/spec" target="_blank"];
+"github.com/docker/distribution" [label="github.com/docker/distribution" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution" target="_blank"];
+"github.com/docker/distribution" -> "context";
+"github.com/docker/distribution" -> "errors";
+"github.com/docker/distribution" -> "fmt";
+"github.com/docker/distribution" -> "github.com/docker/distribution/reference";
+"github.com/docker/distribution" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/docker/distribution" -> "io";
+"github.com/docker/distribution" -> "mime";
+"github.com/docker/distribution" -> "net/http";
+"github.com/docker/distribution" -> "strings";
+"github.com/docker/distribution" -> "time";
+"github.com/docker/distribution/digestset" [label="github.com/docker/distribution/digestset" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/digestset" target="_blank"];
+"github.com/docker/distribution/digestset" -> "errors";
+"github.com/docker/distribution/digestset" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/digestset" -> "sort";
+"github.com/docker/distribution/digestset" -> "strings";
+"github.com/docker/distribution/digestset" -> "sync";
+"github.com/docker/distribution/metrics" [label="github.com/docker/distribution/metrics" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/metrics" target="_blank"];
+"github.com/docker/distribution/metrics" -> "github.com/docker/go-metrics";
+"github.com/docker/distribution/reference" [label="github.com/docker/distribution/reference" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/reference" target="_blank"];
+"github.com/docker/distribution/reference" -> "errors";
+"github.com/docker/distribution/reference" -> "fmt";
+"github.com/docker/distribution/reference" -> "github.com/docker/distribution/digestset";
+"github.com/docker/distribution/reference" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/reference" -> "path";
+"github.com/docker/distribution/reference" -> "regexp";
+"github.com/docker/distribution/reference" -> "strings";
+"github.com/docker/distribution/registry/api/errcode" [label="github.com/docker/distribution/registry/api/errcode" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/api/errcode" target="_blank"];
+"github.com/docker/distribution/registry/api/errcode" -> "encoding/json";
+"github.com/docker/distribution/registry/api/errcode" -> "fmt";
+"github.com/docker/distribution/registry/api/errcode" -> "net/http";
+"github.com/docker/distribution/registry/api/errcode" -> "sort";
+"github.com/docker/distribution/registry/api/errcode" -> "strings";
+"github.com/docker/distribution/registry/api/errcode" -> "sync";
+"github.com/docker/distribution/registry/api/v2" [label="github.com/docker/distribution/registry/api/v2" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/api/v2" target="_blank"];
+"github.com/docker/distribution/registry/api/v2" -> "fmt";
+"github.com/docker/distribution/registry/api/v2" -> "github.com/docker/distribution/reference";
+"github.com/docker/distribution/registry/api/v2" -> "github.com/docker/distribution/registry/api/errcode";
+"github.com/docker/distribution/registry/api/v2" -> "github.com/gorilla/mux";
+"github.com/docker/distribution/registry/api/v2" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/registry/api/v2" -> "net/http";
+"github.com/docker/distribution/registry/api/v2" -> "net/url";
+"github.com/docker/distribution/registry/api/v2" -> "regexp";
+"github.com/docker/distribution/registry/api/v2" -> "strings";
+"github.com/docker/distribution/registry/api/v2" -> "unicode";
+"github.com/docker/distribution/registry/client" [label="github.com/docker/distribution/registry/client" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client" target="_blank"];
+"github.com/docker/distribution/registry/client" -> "bytes";
+"github.com/docker/distribution/registry/client" -> "context";
+"github.com/docker/distribution/registry/client" -> "encoding/json";
+"github.com/docker/distribution/registry/client" -> "errors";
+"github.com/docker/distribution/registry/client" -> "fmt";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/reference";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/api/errcode";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/api/v2";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/client/auth/challenge";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/client/transport";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/storage/cache";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/storage/cache/memory";
+"github.com/docker/distribution/registry/client" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/registry/client" -> "io";
+"github.com/docker/distribution/registry/client" -> "io/ioutil";
+"github.com/docker/distribution/registry/client" -> "net/http";
+"github.com/docker/distribution/registry/client" -> "net/url";
+"github.com/docker/distribution/registry/client" -> "strconv";
+"github.com/docker/distribution/registry/client" -> "strings";
+"github.com/docker/distribution/registry/client" -> "time";
+"github.com/docker/distribution/registry/client/auth/challenge" [label="github.com/docker/distribution/registry/client/auth/challenge" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/auth/challenge" target="_blank"];
+"github.com/docker/distribution/registry/client/auth/challenge" -> "fmt";
+"github.com/docker/distribution/registry/client/auth/challenge" -> "net/http";
+"github.com/docker/distribution/registry/client/auth/challenge" -> "net/url";
+"github.com/docker/distribution/registry/client/auth/challenge" -> "strings";
+"github.com/docker/distribution/registry/client/auth/challenge" -> "sync";
+"github.com/docker/distribution/registry/client/transport" [label="github.com/docker/distribution/registry/client/transport" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/transport" target="_blank"];
+"github.com/docker/distribution/registry/client/transport" -> "errors";
+"github.com/docker/distribution/registry/client/transport" -> "fmt";
+"github.com/docker/distribution/registry/client/transport" -> "io";
+"github.com/docker/distribution/registry/client/transport" -> "net/http";
+"github.com/docker/distribution/registry/client/transport" -> "regexp";
+"github.com/docker/distribution/registry/client/transport" -> "strconv";
+"github.com/docker/distribution/registry/client/transport" -> "sync";
+"github.com/docker/distribution/registry/storage/cache" [label="github.com/docker/distribution/registry/storage/cache" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/storage/cache" target="_blank"];
+"github.com/docker/distribution/registry/storage/cache" -> "context";
+"github.com/docker/distribution/registry/storage/cache" -> "fmt";
+"github.com/docker/distribution/registry/storage/cache" -> "github.com/docker/distribution";
+"github.com/docker/distribution/registry/storage/cache" -> "github.com/docker/distribution/metrics";
+"github.com/docker/distribution/registry/storage/cache" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/registry/storage/cache/memory" [label="github.com/docker/distribution/registry/storage/cache/memory" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/storage/cache/memory" target="_blank"];
+"github.com/docker/distribution/registry/storage/cache/memory" -> "context";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution/reference";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution/registry/storage/cache";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "sync";
+"github.com/docker/docker-credential-helpers/client" [label="github.com/docker/docker-credential-helpers/client" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker-credential-helpers/client" target="_blank"];
+"github.com/docker/docker-credential-helpers/client" -> "bytes";
+"github.com/docker/docker-credential-helpers/client" -> "encoding/json";
+"github.com/docker/docker-credential-helpers/client" -> "fmt";
+"github.com/docker/docker-credential-helpers/client" -> "github.com/docker/docker-credential-helpers/credentials";
+"github.com/docker/docker-credential-helpers/client" -> "io";
+"github.com/docker/docker-credential-helpers/client" -> "os";
+"github.com/docker/docker-credential-helpers/client" -> "os/exec";
+"github.com/docker/docker-credential-helpers/client" -> "strings";
+"github.com/docker/docker-credential-helpers/credentials" [label="github.com/docker/docker-credential-helpers/credentials" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker-credential-helpers/credentials" target="_blank"];
+"github.com/docker/docker-credential-helpers/credentials" -> "bufio";
+"github.com/docker/docker-credential-helpers/credentials" -> "bytes";
+"github.com/docker/docker-credential-helpers/credentials" -> "encoding/json";
+"github.com/docker/docker-credential-helpers/credentials" -> "fmt";
+"github.com/docker/docker-credential-helpers/credentials" -> "io";
+"github.com/docker/docker-credential-helpers/credentials" -> "os";
+"github.com/docker/docker-credential-helpers/credentials" -> "strings";
+"github.com/docker/docker/api/types/versions" [label="github.com/docker/docker/api/types/versions" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/api/types/versions" target="_blank"];
+"github.com/docker/docker/api/types/versions" -> "strconv";
+"github.com/docker/docker/api/types/versions" -> "strings";
+"github.com/docker/docker/pkg/homedir" [label="github.com/docker/docker/pkg/homedir" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/homedir" target="_blank"];
+"github.com/docker/docker/pkg/homedir" -> "github.com/docker/docker/pkg/idtools";
+"github.com/docker/docker/pkg/homedir" -> "github.com/opencontainers/runc/libcontainer/user";
+"github.com/docker/docker/pkg/homedir" -> "os";
+"github.com/docker/docker/pkg/idtools" [label="github.com/docker/docker/pkg/idtools" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/idtools" target="_blank"];
+"github.com/docker/docker/pkg/idtools" -> "bufio";
+"github.com/docker/docker/pkg/idtools" -> "bytes";
+"github.com/docker/docker/pkg/idtools" -> "fmt";
+"github.com/docker/docker/pkg/idtools" -> "github.com/docker/docker/pkg/system";
+"github.com/docker/docker/pkg/idtools" -> "github.com/opencontainers/runc/libcontainer/user";
+"github.com/docker/docker/pkg/idtools" -> "io";
+"github.com/docker/docker/pkg/idtools" -> "os";
+"github.com/docker/docker/pkg/idtools" -> "os/exec";
+"github.com/docker/docker/pkg/idtools" -> "path/filepath";
+"github.com/docker/docker/pkg/idtools" -> "regexp";
+"github.com/docker/docker/pkg/idtools" -> "sort";
+"github.com/docker/docker/pkg/idtools" -> "strconv";
+"github.com/docker/docker/pkg/idtools" -> "strings";
+"github.com/docker/docker/pkg/idtools" -> "sync";
+"github.com/docker/docker/pkg/idtools" -> "syscall";
+"github.com/docker/docker/pkg/mount" [label="github.com/docker/docker/pkg/mount" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/mount" target="_blank"];
+"github.com/docker/docker/pkg/mount" -> "bufio";
+"github.com/docker/docker/pkg/mount" -> "fmt";
+"github.com/docker/docker/pkg/mount" -> "github.com/pkg/errors";
+"github.com/docker/docker/pkg/mount" -> "github.com/sirupsen/logrus";
+"github.com/docker/docker/pkg/mount" -> "golang.org/x/sys/unix";
+"github.com/docker/docker/pkg/mount" -> "io";
+"github.com/docker/docker/pkg/mount" -> "os";
+"github.com/docker/docker/pkg/mount" -> "sort";
+"github.com/docker/docker/pkg/mount" -> "strconv";
+"github.com/docker/docker/pkg/mount" -> "strings";
+"github.com/docker/docker/pkg/system" [label="github.com/docker/docker/pkg/system" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/system" target="_blank"];
+"github.com/docker/docker/pkg/system" -> "bufio";
+"github.com/docker/docker/pkg/system" -> "errors";
+"github.com/docker/docker/pkg/system" -> "fmt";
+"github.com/docker/docker/pkg/system" -> "github.com/docker/docker/pkg/mount";
+"github.com/docker/docker/pkg/system" -> "github.com/docker/go-units";
+"github.com/docker/docker/pkg/system" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/docker/docker/pkg/system" -> "github.com/pkg/errors";
+"github.com/docker/docker/pkg/system" -> "golang.org/x/sys/unix";
+"github.com/docker/docker/pkg/system" -> "io";
+"github.com/docker/docker/pkg/system" -> "io/ioutil";
+"github.com/docker/docker/pkg/system" -> "os";
+"github.com/docker/docker/pkg/system" -> "os/exec";
+"github.com/docker/docker/pkg/system" -> "path/filepath";
+"github.com/docker/docker/pkg/system" -> "runtime";
+"github.com/docker/docker/pkg/system" -> "strconv";
+"github.com/docker/docker/pkg/system" -> "strings";
+"github.com/docker/docker/pkg/system" -> "syscall";
+"github.com/docker/docker/pkg/system" -> "time";
+"github.com/docker/docker/pkg/system" -> "unsafe";
+"github.com/docker/go-connections/sockets" [label="github.com/docker/go-connections/sockets" color="paleturquoise" URL="https://godoc.org/github.com/docker/go-connections/sockets" target="_blank"];
+"github.com/docker/go-connections/sockets" -> "crypto/tls";
+"github.com/docker/go-connections/sockets" -> "errors";
+"github.com/docker/go-connections/sockets" -> "fmt";
+"github.com/docker/go-connections/sockets" -> "golang.org/x/net/proxy";
+"github.com/docker/go-connections/sockets" -> "net";
+"github.com/docker/go-connections/sockets" -> "net/http";
+"github.com/docker/go-connections/sockets" -> "net/url";
+"github.com/docker/go-connections/sockets" -> "os";
+"github.com/docker/go-connections/sockets" -> "strings";
+"github.com/docker/go-connections/sockets" -> "sync";
+"github.com/docker/go-connections/sockets" -> "syscall";
+"github.com/docker/go-connections/sockets" -> "time";
+"github.com/docker/go-connections/tlsconfig" [label="github.com/docker/go-connections/tlsconfig" color="paleturquoise" URL="https://godoc.org/github.com/docker/go-connections/tlsconfig" target="_blank"];
+"github.com/docker/go-connections/tlsconfig" -> "crypto/tls";
+"github.com/docker/go-connections/tlsconfig" -> "crypto/x509";
+"github.com/docker/go-connections/tlsconfig" -> "encoding/pem";
+"github.com/docker/go-connections/tlsconfig" -> "fmt";
+"github.com/docker/go-connections/tlsconfig" -> "github.com/pkg/errors";
+"github.com/docker/go-connections/tlsconfig" -> "io/ioutil";
+"github.com/docker/go-connections/tlsconfig" -> "os";
+"github.com/docker/go-connections/tlsconfig" -> "runtime";
+"github.com/docker/go-metrics" [label="github.com/docker/go-metrics" color="paleturquoise" URL="https://godoc.org/github.com/docker/go-metrics" target="_blank"];
+"github.com/docker/go-metrics" -> "fmt";
+"github.com/docker/go-metrics" -> "github.com/prometheus/client_golang/prometheus";
+"github.com/docker/go-metrics" -> "github.com/prometheus/client_golang/prometheus/promhttp";
+"github.com/docker/go-metrics" -> "net/http";
+"github.com/docker/go-metrics" -> "sync";
+"github.com/docker/go-metrics" -> "time";
+"github.com/docker/go-units" [label="github.com/docker/go-units" color="palegoldenrod" URL="https://godoc.org/github.com/docker/go-units" target="_blank"];
+"github.com/docker/go-units" -> "fmt";
+"github.com/docker/go-units" -> "regexp";
+"github.com/docker/go-units" -> "strconv";
+"github.com/docker/go-units" -> "strings";
+"github.com/docker/go-units" -> "time";
+"github.com/ghodss/yaml" [label="github.com/ghodss/yaml" color="paleturquoise" URL="https://godoc.org/github.com/ghodss/yaml" target="_blank"];
+"github.com/ghodss/yaml" -> "bytes";
+"github.com/ghodss/yaml" -> "encoding";
+"github.com/ghodss/yaml" -> "encoding/json";
+"github.com/ghodss/yaml" -> "fmt";
+"github.com/ghodss/yaml" -> "gopkg.in/yaml.v2";
+"github.com/ghodss/yaml" -> "reflect";
+"github.com/ghodss/yaml" -> "sort";
+"github.com/ghodss/yaml" -> "strconv";
+"github.com/ghodss/yaml" -> "strings";
+"github.com/ghodss/yaml" -> "sync";
+"github.com/ghodss/yaml" -> "unicode";
+"github.com/ghodss/yaml" -> "unicode/utf8";
+"github.com/golang/protobuf/proto" [label="github.com/golang/protobuf/proto" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/proto" target="_blank"];
+"github.com/golang/protobuf/proto" -> "bufio";
+"github.com/golang/protobuf/proto" -> "bytes";
+"github.com/golang/protobuf/proto" -> "encoding";
+"github.com/golang/protobuf/proto" -> "encoding/json";
+"github.com/golang/protobuf/proto" -> "errors";
+"github.com/golang/protobuf/proto" -> "fmt";
+"github.com/golang/protobuf/proto" -> "io";
+"github.com/golang/protobuf/proto" -> "log";
+"github.com/golang/protobuf/proto" -> "math";
+"github.com/golang/protobuf/proto" -> "reflect";
+"github.com/golang/protobuf/proto" -> "sort";
+"github.com/golang/protobuf/proto" -> "strconv";
+"github.com/golang/protobuf/proto" -> "strings";
+"github.com/golang/protobuf/proto" -> "sync";
+"github.com/golang/protobuf/proto" -> "sync/atomic";
+"github.com/golang/protobuf/proto" -> "unicode/utf8";
+"github.com/golang/protobuf/proto" -> "unsafe";
+"github.com/gorilla/mux" [label="github.com/gorilla/mux" color="paleturquoise" URL="https://godoc.org/github.com/gorilla/mux" target="_blank"];
+"github.com/gorilla/mux" -> "bytes";
+"github.com/gorilla/mux" -> "context";
+"github.com/gorilla/mux" -> "errors";
+"github.com/gorilla/mux" -> "fmt";
+"github.com/gorilla/mux" -> "net/http";
+"github.com/gorilla/mux" -> "net/url";
+"github.com/gorilla/mux" -> "path";
+"github.com/gorilla/mux" -> "regexp";
+"github.com/gorilla/mux" -> "strconv";
+"github.com/gorilla/mux" -> "strings";
+"github.com/klauspost/compress/flate" [label="github.com/klauspost/compress/flate" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/flate" target="_blank"];
+"github.com/klauspost/compress/flate" -> "bufio";
+"github.com/klauspost/compress/flate" -> "bytes";
+"github.com/klauspost/compress/flate" -> "encoding/binary";
+"github.com/klauspost/compress/flate" -> "fmt";
+"github.com/klauspost/compress/flate" -> "io";
+"github.com/klauspost/compress/flate" -> "math";
+"github.com/klauspost/compress/flate" -> "math/bits";
+"github.com/klauspost/compress/flate" -> "sort";
+"github.com/klauspost/compress/flate" -> "strconv";
+"github.com/klauspost/compress/flate" -> "sync";
+"github.com/klauspost/compress/fse" [label="github.com/klauspost/compress/fse" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/fse" target="_blank"];
+"github.com/klauspost/compress/fse" -> "errors";
+"github.com/klauspost/compress/fse" -> "fmt";
+"github.com/klauspost/compress/fse" -> "io";
+"github.com/klauspost/compress/fse" -> "math/bits";
+"github.com/klauspost/compress/huff0" [label="github.com/klauspost/compress/huff0" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/huff0" target="_blank"];
+"github.com/klauspost/compress/huff0" -> "errors";
+"github.com/klauspost/compress/huff0" -> "fmt";
+"github.com/klauspost/compress/huff0" -> "github.com/klauspost/compress/fse";
+"github.com/klauspost/compress/huff0" -> "io";
+"github.com/klauspost/compress/huff0" -> "math";
+"github.com/klauspost/compress/huff0" -> "math/bits";
+"github.com/klauspost/compress/huff0" -> "runtime";
+"github.com/klauspost/compress/huff0" -> "sync";
+"github.com/klauspost/compress/snappy" [label="github.com/klauspost/compress/snappy" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/snappy" target="_blank"];
+"github.com/klauspost/compress/snappy" -> "encoding/binary";
+"github.com/klauspost/compress/snappy" -> "errors";
+"github.com/klauspost/compress/snappy" -> "hash/crc32";
+"github.com/klauspost/compress/snappy" -> "io";
+"github.com/klauspost/compress/zstd" [label="github.com/klauspost/compress/zstd" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/zstd" target="_blank"];
+"github.com/klauspost/compress/zstd" -> "bytes";
+"github.com/klauspost/compress/zstd" -> "crypto/rand";
+"github.com/klauspost/compress/zstd" -> "encoding/binary";
+"github.com/klauspost/compress/zstd" -> "encoding/hex";
+"github.com/klauspost/compress/zstd" -> "errors";
+"github.com/klauspost/compress/zstd" -> "fmt";
+"github.com/klauspost/compress/zstd" -> "github.com/klauspost/compress/huff0";
+"github.com/klauspost/compress/zstd" -> "github.com/klauspost/compress/snappy";
+"github.com/klauspost/compress/zstd" -> "github.com/klauspost/compress/zstd/internal/xxhash";
+"github.com/klauspost/compress/zstd" -> "hash";
+"github.com/klauspost/compress/zstd" -> "hash/crc32";
+"github.com/klauspost/compress/zstd" -> "io";
+"github.com/klauspost/compress/zstd" -> "io/ioutil";
+"github.com/klauspost/compress/zstd" -> "log";
+"github.com/klauspost/compress/zstd" -> "math";
+"github.com/klauspost/compress/zstd" -> "math/bits";
+"github.com/klauspost/compress/zstd" -> "runtime";
+"github.com/klauspost/compress/zstd" -> "runtime/debug";
+"github.com/klauspost/compress/zstd" -> "strconv";
+"github.com/klauspost/compress/zstd" -> "strings";
+"github.com/klauspost/compress/zstd" -> "sync";
+"github.com/klauspost/compress/zstd/internal/xxhash" [label="github.com/klauspost/compress/zstd/internal/xxhash" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/zstd/internal/xxhash" target="_blank"];
+"github.com/klauspost/compress/zstd/internal/xxhash" -> "encoding/binary";
+"github.com/klauspost/compress/zstd/internal/xxhash" -> "errors";
+"github.com/klauspost/compress/zstd/internal/xxhash" -> "math/bits";
+"github.com/klauspost/pgzip" [label="github.com/klauspost/pgzip" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/pgzip" target="_blank"];
+"github.com/klauspost/pgzip" -> "bufio";
+"github.com/klauspost/pgzip" -> "bytes";
+"github.com/klauspost/pgzip" -> "errors";
+"github.com/klauspost/pgzip" -> "fmt";
+"github.com/klauspost/pgzip" -> "github.com/klauspost/compress/flate";
+"github.com/klauspost/pgzip" -> "hash";
+"github.com/klauspost/pgzip" -> "hash/crc32";
+"github.com/klauspost/pgzip" -> "io";
+"github.com/klauspost/pgzip" -> "sync";
+"github.com/klauspost/pgzip" -> "time";
+"github.com/matttproud/golang_protobuf_extensions/pbutil" [label="github.com/matttproud/golang_protobuf_extensions/pbutil" color="paleturquoise" URL="https://godoc.org/github.com/matttproud/golang_protobuf_extensions/pbutil" target="_blank"];
+"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "encoding/binary";
+"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "errors";
+"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "github.com/golang/protobuf/proto";
+"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "io";
+"github.com/opencontainers/go-digest" [label="github.com/opencontainers/go-digest" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/go-digest" target="_blank"];
+"github.com/opencontainers/go-digest" -> "crypto";
+"github.com/opencontainers/go-digest" -> "fmt";
+"github.com/opencontainers/go-digest" -> "hash";
+"github.com/opencontainers/go-digest" -> "io";
+"github.com/opencontainers/go-digest" -> "regexp";
+"github.com/opencontainers/go-digest" -> "strings";
+"github.com/opencontainers/image-spec/specs-go" [label="github.com/opencontainers/image-spec/specs-go" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go" target="_blank"];
+"github.com/opencontainers/image-spec/specs-go" -> "fmt";
+"github.com/opencontainers/image-spec/specs-go/v1" [label="github.com/opencontainers/image-spec/specs-go/v1" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" target="_blank"];
+"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/go-digest";
+"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/image-spec/specs-go";
+"github.com/opencontainers/image-spec/specs-go/v1" -> "time";
+"github.com/opencontainers/runc/libcontainer/user" [label="github.com/opencontainers/runc/libcontainer/user" color="palegoldenrod" URL="https://godoc.org/github.com/opencontainers/runc/libcontainer/user" target="_blank"];
+"github.com/opencontainers/runc/libcontainer/user" -> "bufio";
+"github.com/opencontainers/runc/libcontainer/user" -> "errors";
+"github.com/opencontainers/runc/libcontainer/user" -> "fmt";
+"github.com/opencontainers/runc/libcontainer/user" -> "golang.org/x/sys/unix";
+"github.com/opencontainers/runc/libcontainer/user" -> "io";
+"github.com/opencontainers/runc/libcontainer/user" -> "os";
+"github.com/opencontainers/runc/libcontainer/user" -> "os/user";
+"github.com/opencontainers/runc/libcontainer/user" -> "strconv";
+"github.com/opencontainers/runc/libcontainer/user" -> "strings";
+"github.com/pkg/errors" [label="github.com/pkg/errors" color="paleturquoise" URL="https://godoc.org/github.com/pkg/errors" target="_blank"];
+"github.com/pkg/errors" -> "fmt";
+"github.com/pkg/errors" -> "io";
+"github.com/pkg/errors" -> "path";
+"github.com/pkg/errors" -> "runtime";
+"github.com/pkg/errors" -> "strings";
+"github.com/prometheus/client_golang/prometheus" [label="github.com/prometheus/client_golang/prometheus" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus" target="_blank"];
+"github.com/prometheus/client_golang/prometheus" -> "bytes";
+"github.com/prometheus/client_golang/prometheus" -> "encoding/json";
+"github.com/prometheus/client_golang/prometheus" -> "errors";
+"github.com/prometheus/client_golang/prometheus" -> "expvar";
+"github.com/prometheus/client_golang/prometheus" -> "fmt";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/beorn7/perks/quantile";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/cespare/xxhash/v2";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/golang/protobuf/proto";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/client_golang/prometheus/internal";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/client_model/go";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/common/expfmt";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/common/model";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/procfs";
+"github.com/prometheus/client_golang/prometheus" -> "io/ioutil";
+"github.com/prometheus/client_golang/prometheus" -> "math";
+"github.com/prometheus/client_golang/prometheus" -> "os";
+"github.com/prometheus/client_golang/prometheus" -> "path/filepath";
+"github.com/prometheus/client_golang/prometheus" -> "runtime";
+"github.com/prometheus/client_golang/prometheus" -> "runtime/debug";
+"github.com/prometheus/client_golang/prometheus" -> "sort";
+"github.com/prometheus/client_golang/prometheus" -> "strings";
+"github.com/prometheus/client_golang/prometheus" -> "sync";
+"github.com/prometheus/client_golang/prometheus" -> "sync/atomic";
+"github.com/prometheus/client_golang/prometheus" -> "time";
+"github.com/prometheus/client_golang/prometheus" -> "unicode/utf8";
+"github.com/prometheus/client_golang/prometheus/internal" [label="github.com/prometheus/client_golang/prometheus/internal" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus/internal" target="_blank"];
+"github.com/prometheus/client_golang/prometheus/internal" -> "github.com/prometheus/client_model/go";
+"github.com/prometheus/client_golang/prometheus/internal" -> "sort";
+"github.com/prometheus/client_golang/prometheus/promhttp" [label="github.com/prometheus/client_golang/prometheus/promhttp" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp" target="_blank"];
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "bufio";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "compress/gzip";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "crypto/tls";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "errors";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "fmt";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/client_golang/prometheus";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/client_model/go";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/common/expfmt";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "io";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "net";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "net/http";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "net/http/httptrace";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "strconv";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "strings";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "sync";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "time";
+"github.com/prometheus/client_model/go" [label="github.com/prometheus/client_model/go" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_model/go" target="_blank"];
+"github.com/prometheus/client_model/go" -> "fmt";
+"github.com/prometheus/client_model/go" -> "github.com/golang/protobuf/proto";
+"github.com/prometheus/client_model/go" -> "math";
+"github.com/prometheus/common/expfmt" [label="github.com/prometheus/common/expfmt" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/expfmt" target="_blank"];
+"github.com/prometheus/common/expfmt" -> "bufio";
+"github.com/prometheus/common/expfmt" -> "bytes";
+"github.com/prometheus/common/expfmt" -> "fmt";
+"github.com/prometheus/common/expfmt" -> "github.com/golang/protobuf/proto";
+"github.com/prometheus/common/expfmt" -> "github.com/matttproud/golang_protobuf_extensions/pbutil";
+"github.com/prometheus/common/expfmt" -> "github.com/prometheus/client_model/go";
+"github.com/prometheus/common/expfmt" -> "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg";
+"github.com/prometheus/common/expfmt" -> "github.com/prometheus/common/model";
+"github.com/prometheus/common/expfmt" -> "io";
+"github.com/prometheus/common/expfmt" -> "io/ioutil";
+"github.com/prometheus/common/expfmt" -> "math";
+"github.com/prometheus/common/expfmt" -> "mime";
+"github.com/prometheus/common/expfmt" -> "net/http";
+"github.com/prometheus/common/expfmt" -> "strconv";
+"github.com/prometheus/common/expfmt" -> "strings";
+"github.com/prometheus/common/expfmt" -> "sync";
+"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" [label="github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" target="_blank"];
+"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "sort";
+"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "strconv";
+"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "strings";
+"github.com/prometheus/common/model" [label="github.com/prometheus/common/model" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/model" target="_blank"];
+"github.com/prometheus/common/model" -> "encoding/json";
+"github.com/prometheus/common/model" -> "fmt";
+"github.com/prometheus/common/model" -> "math";
+"github.com/prometheus/common/model" -> "regexp";
+"github.com/prometheus/common/model" -> "sort";
+"github.com/prometheus/common/model" -> "strconv";
+"github.com/prometheus/common/model" -> "strings";
+"github.com/prometheus/common/model" -> "time";
+"github.com/prometheus/common/model" -> "unicode/utf8";
+"github.com/prometheus/procfs" [label="github.com/prometheus/procfs" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs" target="_blank"];
+"github.com/prometheus/procfs" -> "bufio";
+"github.com/prometheus/procfs" -> "bytes";
+"github.com/prometheus/procfs" -> "encoding/hex";
+"github.com/prometheus/procfs" -> "errors";
+"github.com/prometheus/procfs" -> "fmt";
+"github.com/prometheus/procfs" -> "github.com/prometheus/procfs/internal/fs";
+"github.com/prometheus/procfs" -> "github.com/prometheus/procfs/internal/util";
+"github.com/prometheus/procfs" -> "io";
+"github.com/prometheus/procfs" -> "io/ioutil";
+"github.com/prometheus/procfs" -> "net";
+"github.com/prometheus/procfs" -> "os";
+"github.com/prometheus/procfs" -> "path/filepath";
+"github.com/prometheus/procfs" -> "regexp";
+"github.com/prometheus/procfs" -> "sort";
+"github.com/prometheus/procfs" -> "strconv";
+"github.com/prometheus/procfs" -> "strings";
+"github.com/prometheus/procfs" -> "time";
+"github.com/prometheus/procfs/internal/fs" [label="github.com/prometheus/procfs/internal/fs" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs/internal/fs" target="_blank"];
+"github.com/prometheus/procfs/internal/fs" -> "fmt";
+"github.com/prometheus/procfs/internal/fs" -> "os";
+"github.com/prometheus/procfs/internal/fs" -> "path/filepath";
+"github.com/prometheus/procfs/internal/util" [label="github.com/prometheus/procfs/internal/util" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs/internal/util" target="_blank"];
+"github.com/prometheus/procfs/internal/util" -> "bytes";
+"github.com/prometheus/procfs/internal/util" -> "io/ioutil";
+"github.com/prometheus/procfs/internal/util" -> "os";
+"github.com/prometheus/procfs/internal/util" -> "strconv";
+"github.com/prometheus/procfs/internal/util" -> "strings";
+"github.com/prometheus/procfs/internal/util" -> "syscall";
+"github.com/sirupsen/logrus" [label="github.com/sirupsen/logrus" color="paleturquoise" URL="https://godoc.org/github.com/sirupsen/logrus" target="_blank"];
+"github.com/sirupsen/logrus" -> "bufio";
+"github.com/sirupsen/logrus" -> "bytes";
+"github.com/sirupsen/logrus" -> "context";
+"github.com/sirupsen/logrus" -> "encoding/json";
+"github.com/sirupsen/logrus" -> "fmt";
+"github.com/sirupsen/logrus" -> "golang.org/x/sys/unix";
+"github.com/sirupsen/logrus" -> "io";
+"github.com/sirupsen/logrus" -> "log";
+"github.com/sirupsen/logrus" -> "os";
+"github.com/sirupsen/logrus" -> "reflect";
+"github.com/sirupsen/logrus" -> "runtime";
+"github.com/sirupsen/logrus" -> "sort";
+"github.com/sirupsen/logrus" -> "strings";
+"github.com/sirupsen/logrus" -> "sync";
+"github.com/sirupsen/logrus" -> "sync/atomic";
+"github.com/sirupsen/logrus" -> "time";
+"github.com/ulikunitz/xz" [label="github.com/ulikunitz/xz" color="paleturquoise" URL="https://godoc.org/github.com/ulikunitz/xz" target="_blank"];
+"github.com/ulikunitz/xz" -> "bytes";
+"github.com/ulikunitz/xz" -> "crypto/sha256";
+"github.com/ulikunitz/xz" -> "errors";
+"github.com/ulikunitz/xz" -> "fmt";
+"github.com/ulikunitz/xz" -> "github.com/ulikunitz/xz/internal/xlog";
+"github.com/ulikunitz/xz" -> "github.com/ulikunitz/xz/lzma";
+"github.com/ulikunitz/xz" -> "hash";
+"github.com/ulikunitz/xz" -> "hash/crc32";
+"github.com/ulikunitz/xz" -> "hash/crc64";
+"github.com/ulikunitz/xz" -> "io";
+"github.com/ulikunitz/xz/internal/hash" [label="github.com/ulikunitz/xz/internal/hash" color="paleturquoise" URL="https://godoc.org/github.com/ulikunitz/xz/internal/hash" target="_blank"];
+"github.com/ulikunitz/xz/internal/xlog" [label="github.com/ulikunitz/xz/internal/xlog" color="paleturquoise" URL="https://godoc.org/github.com/ulikunitz/xz/internal/xlog" target="_blank"];
+"github.com/ulikunitz/xz/internal/xlog" -> "fmt";
+"github.com/ulikunitz/xz/internal/xlog" -> "io";
+"github.com/ulikunitz/xz/internal/xlog" -> "os";
+"github.com/ulikunitz/xz/internal/xlog" -> "runtime";
+"github.com/ulikunitz/xz/internal/xlog" -> "sync";
+"github.com/ulikunitz/xz/internal/xlog" -> "time";
+"github.com/ulikunitz/xz/lzma" [label="github.com/ulikunitz/xz/lzma" color="paleturquoise" URL="https://godoc.org/github.com/ulikunitz/xz/lzma" target="_blank"];
+"github.com/ulikunitz/xz/lzma" -> "bufio";
+"github.com/ulikunitz/xz/lzma" -> "bytes";
+"github.com/ulikunitz/xz/lzma" -> "errors";
+"github.com/ulikunitz/xz/lzma" -> "fmt";
+"github.com/ulikunitz/xz/lzma" -> "github.com/ulikunitz/xz/internal/hash";
+"github.com/ulikunitz/xz/lzma" -> "github.com/ulikunitz/xz/internal/xlog";
+"github.com/ulikunitz/xz/lzma" -> "io";
+"github.com/ulikunitz/xz/lzma" -> "unicode";
+"golang.org/x/net/internal/socks" [label="golang.org/x/net/internal/socks" color="paleturquoise" URL="https://godoc.org/golang.org/x/net/internal/socks" target="_blank"];
+"golang.org/x/net/internal/socks" -> "context";
+"golang.org/x/net/internal/socks" -> "errors";
+"golang.org/x/net/internal/socks" -> "io";
+"golang.org/x/net/internal/socks" -> "net";
+"golang.org/x/net/internal/socks" -> "strconv";
+"golang.org/x/net/internal/socks" -> "time";
+"golang.org/x/net/proxy" [label="golang.org/x/net/proxy" color="paleturquoise" URL="https://godoc.org/golang.org/x/net/proxy" target="_blank"];
+"golang.org/x/net/proxy" -> "context";
+"golang.org/x/net/proxy" -> "errors";
+"golang.org/x/net/proxy" -> "golang.org/x/net/internal/socks";
+"golang.org/x/net/proxy" -> "net";
+"golang.org/x/net/proxy" -> "net/url";
+"golang.org/x/net/proxy" -> "os";
+"golang.org/x/net/proxy" -> "strings";
+"golang.org/x/net/proxy" -> "sync";
+"golang.org/x/sys/unix" [label="golang.org/x/sys/unix" color="paleturquoise" URL="https://godoc.org/golang.org/x/sys/unix" target="_blank"];
+"golang.org/x/sys/unix" -> "bytes";
+"golang.org/x/sys/unix" -> "encoding/binary";
+"golang.org/x/sys/unix" -> "net";
+"golang.org/x/sys/unix" -> "runtime";
+"golang.org/x/sys/unix" -> "sort";
+"golang.org/x/sys/unix" -> "strings";
+"golang.org/x/sys/unix" -> "sync";
+"golang.org/x/sys/unix" -> "syscall";
+"golang.org/x/sys/unix" -> "time";
+"golang.org/x/sys/unix" -> "unsafe";
+"gopkg.in/yaml.v2" [label="gopkg.in/yaml.v2" color="paleturquoise" URL="https://godoc.org/gopkg.in/yaml.v2" target="_blank"];
+"gopkg.in/yaml.v2" -> "bytes";
+"gopkg.in/yaml.v2" -> "encoding";
+"gopkg.in/yaml.v2" -> "encoding/base64";
+"gopkg.in/yaml.v2" -> "errors";
+"gopkg.in/yaml.v2" -> "fmt";
+"gopkg.in/yaml.v2" -> "io";
+"gopkg.in/yaml.v2" -> "math";
+"gopkg.in/yaml.v2" -> "reflect";
+"gopkg.in/yaml.v2" -> "regexp";
+"gopkg.in/yaml.v2" -> "sort";
+"gopkg.in/yaml.v2" -> "strconv";
+"gopkg.in/yaml.v2" -> "strings";
+"gopkg.in/yaml.v2" -> "sync";
+"gopkg.in/yaml.v2" -> "time";
+"gopkg.in/yaml.v2" -> "unicode";
+"gopkg.in/yaml.v2" -> "unicode/utf8";
+"hash" [label="hash" color="palegreen" URL="https://godoc.org/hash" target="_blank"];
+"hash/crc32" [label="hash/crc32" color="palegreen" URL="https://godoc.org/hash/crc32" target="_blank"];
+"hash/crc64" [label="hash/crc64" color="palegreen" URL="https://godoc.org/hash/crc64" target="_blank"];
+"io" [label="io" color="palegreen" URL="https://godoc.org/io" target="_blank"];
+"io/ioutil" [label="io/ioutil" color="palegreen" URL="https://godoc.org/io/ioutil" target="_blank"];
+"log" [label="log" color="palegreen" URL="https://godoc.org/log" target="_blank"];
+"math" [label="math" color="palegreen" URL="https://godoc.org/math" target="_blank"];
+"math/big" [label="math/big" color="palegreen" URL="https://godoc.org/math/big" target="_blank"];
+"math/bits" [label="math/bits" color="palegreen" URL="https://godoc.org/math/bits" target="_blank"];
+"mime" [label="mime" color="palegreen" URL="https://godoc.org/mime" target="_blank"];
+"net" [label="net" color="palegreen" URL="https://godoc.org/net" target="_blank"];
+"net/http" [label="net/http" color="palegreen" URL="https://godoc.org/net/http" target="_blank"];
+"net/http/httptrace" [label="net/http/httptrace" color="palegreen" URL="https://godoc.org/net/http/httptrace" target="_blank"];
+"net/url" [label="net/url" color="palegreen" URL="https://godoc.org/net/url" target="_blank"];
+"os" [label="os" color="palegreen" URL="https://godoc.org/os" target="_blank"];
+"os/exec" [label="os/exec" color="palegreen" URL="https://godoc.org/os/exec" target="_blank"];
+"os/user" [label="os/user" color="palegreen" URL="https://godoc.org/os/user" target="_blank"];
+"path" [label="path" color="palegreen" URL="https://godoc.org/path" target="_blank"];
+"path/filepath" [label="path/filepath" color="palegreen" URL="https://godoc.org/path/filepath" target="_blank"];
+"reflect" [label="reflect" color="palegreen" URL="https://godoc.org/reflect" target="_blank"];
+"regexp" [label="regexp" color="palegreen" URL="https://godoc.org/regexp" target="_blank"];
+"runtime" [label="runtime" color="palegreen" URL="https://godoc.org/runtime" target="_blank"];
+"runtime/debug" [label="runtime/debug" color="palegreen" URL="https://godoc.org/runtime/debug" target="_blank"];
+"sort" [label="sort" color="palegreen" URL="https://godoc.org/sort" target="_blank"];
+"strconv" [label="strconv" color="palegreen" URL="https://godoc.org/strconv" target="_blank"];
+"strings" [label="strings" color="palegreen" URL="https://godoc.org/strings" target="_blank"];
+"sync" [label="sync" color="palegreen" URL="https://godoc.org/sync" target="_blank"];
+"sync/atomic" [label="sync/atomic" color="palegreen" URL="https://godoc.org/sync/atomic" target="_blank"];
+"syscall" [label="syscall" color="palegreen" URL="https://godoc.org/syscall" target="_blank"];
+"time" [label="time" color="palegreen" URL="https://godoc.org/time" target="_blank"];
+"unicode" [label="unicode" color="palegreen" URL="https://godoc.org/unicode" target="_blank"];
+"unicode/utf8" [label="unicode/utf8" color="palegreen" URL="https://godoc.org/unicode/utf8" target="_blank"];
+"unsafe" [label="unsafe" color="palegreen" URL="https://godoc.org/unsafe" target="_blank"];
+}
diff --git a/images/dot/docker.dot b/images/dot/docker.dot
new file mode 100644
index 0000000..90dc677
--- /dev/null
+++ b/images/dot/docker.dot
@@ -0,0 +1,327 @@
+digraph godep {
+nodesep=0.4
+ranksep=0.8
+node [shape="box",style="rounded,filled"]
+edge [arrowsize="0.5"]
+"bufio" [label="bufio" color="palegreen" URL="https://godoc.org/bufio" target="_blank"];
+"bytes" [label="bytes" color="palegreen" URL="https://godoc.org/bytes" target="_blank"];
+"compress/gzip" [label="compress/gzip" color="palegreen" URL="https://godoc.org/compress/gzip" target="_blank"];
+"context" [label="context" color="palegreen" URL="https://godoc.org/context" target="_blank"];
+"crypto" [label="crypto" color="palegreen" URL="https://godoc.org/crypto" target="_blank"];
+"crypto/tls" [label="crypto/tls" color="palegreen" URL="https://godoc.org/crypto/tls" target="_blank"];
+"encoding" [label="encoding" color="palegreen" URL="https://godoc.org/encoding" target="_blank"];
+"encoding/binary" [label="encoding/binary" color="palegreen" URL="https://godoc.org/encoding/binary" target="_blank"];
+"encoding/hex" [label="encoding/hex" color="palegreen" URL="https://godoc.org/encoding/hex" target="_blank"];
+"encoding/json" [label="encoding/json" color="palegreen" URL="https://godoc.org/encoding/json" target="_blank"];
+"errors" [label="errors" color="palegreen" URL="https://godoc.org/errors" target="_blank"];
+"expvar" [label="expvar" color="palegreen" URL="https://godoc.org/expvar" target="_blank"];
+"fmt" [label="fmt" color="palegreen" URL="https://godoc.org/fmt" target="_blank"];
+"github.com/beorn7/perks/quantile" [label="github.com/beorn7/perks/quantile" color="paleturquoise" URL="https://godoc.org/github.com/beorn7/perks/quantile" target="_blank"];
+"github.com/beorn7/perks/quantile" -> "math";
+"github.com/beorn7/perks/quantile" -> "sort";
+"github.com/cespare/xxhash/v2" [label="github.com/cespare/xxhash/v2" color="paleturquoise" URL="https://godoc.org/github.com/cespare/xxhash/v2" target="_blank"];
+"github.com/cespare/xxhash/v2" -> "encoding/binary";
+"github.com/cespare/xxhash/v2" -> "errors";
+"github.com/cespare/xxhash/v2" -> "math/bits";
+"github.com/cespare/xxhash/v2" -> "reflect";
+"github.com/cespare/xxhash/v2" -> "unsafe";
+"github.com/docker/distribution" [label="github.com/docker/distribution" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution" target="_blank"];
+"github.com/docker/distribution" -> "context";
+"github.com/docker/distribution" -> "errors";
+"github.com/docker/distribution" -> "fmt";
+"github.com/docker/distribution" -> "github.com/docker/distribution/reference";
+"github.com/docker/distribution" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution" -> "github.com/opencontainers/image-spec/specs-go/v1";
+"github.com/docker/distribution" -> "io";
+"github.com/docker/distribution" -> "mime";
+"github.com/docker/distribution" -> "net/http";
+"github.com/docker/distribution" -> "strings";
+"github.com/docker/distribution" -> "time";
+"github.com/docker/distribution/digestset" [label="github.com/docker/distribution/digestset" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/digestset" target="_blank"];
+"github.com/docker/distribution/digestset" -> "errors";
+"github.com/docker/distribution/digestset" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/digestset" -> "sort";
+"github.com/docker/distribution/digestset" -> "strings";
+"github.com/docker/distribution/digestset" -> "sync";
+"github.com/docker/distribution/metrics" [label="github.com/docker/distribution/metrics" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/metrics" target="_blank"];
+"github.com/docker/distribution/metrics" -> "github.com/docker/go-metrics";
+"github.com/docker/distribution/reference" [label="github.com/docker/distribution/reference" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/reference" target="_blank"];
+"github.com/docker/distribution/reference" -> "errors";
+"github.com/docker/distribution/reference" -> "fmt";
+"github.com/docker/distribution/reference" -> "github.com/docker/distribution/digestset";
+"github.com/docker/distribution/reference" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/reference" -> "path";
+"github.com/docker/distribution/reference" -> "regexp";
+"github.com/docker/distribution/reference" -> "strings";
+"github.com/docker/distribution/registry/api/errcode" [label="github.com/docker/distribution/registry/api/errcode" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/api/errcode" target="_blank"];
+"github.com/docker/distribution/registry/api/errcode" -> "encoding/json";
+"github.com/docker/distribution/registry/api/errcode" -> "fmt";
+"github.com/docker/distribution/registry/api/errcode" -> "net/http";
+"github.com/docker/distribution/registry/api/errcode" -> "sort";
+"github.com/docker/distribution/registry/api/errcode" -> "strings";
+"github.com/docker/distribution/registry/api/errcode" -> "sync";
+"github.com/docker/distribution/registry/api/v2" [label="github.com/docker/distribution/registry/api/v2" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/api/v2" target="_blank"];
+"github.com/docker/distribution/registry/api/v2" -> "fmt";
+"github.com/docker/distribution/registry/api/v2" -> "github.com/docker/distribution/reference";
+"github.com/docker/distribution/registry/api/v2" -> "github.com/docker/distribution/registry/api/errcode";
+"github.com/docker/distribution/registry/api/v2" -> "github.com/gorilla/mux";
+"github.com/docker/distribution/registry/api/v2" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/registry/api/v2" -> "net/http";
+"github.com/docker/distribution/registry/api/v2" -> "net/url";
+"github.com/docker/distribution/registry/api/v2" -> "regexp";
+"github.com/docker/distribution/registry/api/v2" -> "strings";
+"github.com/docker/distribution/registry/api/v2" -> "unicode";
+"github.com/docker/distribution/registry/client" [label="github.com/docker/distribution/registry/client" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client" target="_blank"];
+"github.com/docker/distribution/registry/client" -> "bytes";
+"github.com/docker/distribution/registry/client" -> "context";
+"github.com/docker/distribution/registry/client" -> "encoding/json";
+"github.com/docker/distribution/registry/client" -> "errors";
+"github.com/docker/distribution/registry/client" -> "fmt";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/reference";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/api/errcode";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/api/v2";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/client/auth/challenge";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/client/transport";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/storage/cache";
+"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/storage/cache/memory";
+"github.com/docker/distribution/registry/client" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/registry/client" -> "io";
+"github.com/docker/distribution/registry/client" -> "io/ioutil";
+"github.com/docker/distribution/registry/client" -> "net/http";
+"github.com/docker/distribution/registry/client" -> "net/url";
+"github.com/docker/distribution/registry/client" -> "strconv";
+"github.com/docker/distribution/registry/client" -> "strings";
+"github.com/docker/distribution/registry/client" -> "time";
+"github.com/docker/distribution/registry/client/auth" [label="github.com/docker/distribution/registry/client/auth" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/auth" target="_blank"];
+"github.com/docker/distribution/registry/client/auth" -> "encoding/json";
+"github.com/docker/distribution/registry/client/auth" -> "errors";
+"github.com/docker/distribution/registry/client/auth" -> "fmt";
+"github.com/docker/distribution/registry/client/auth" -> "github.com/docker/distribution/registry/client";
+"github.com/docker/distribution/registry/client/auth" -> "github.com/docker/distribution/registry/client/auth/challenge";
+"github.com/docker/distribution/registry/client/auth" -> "github.com/docker/distribution/registry/client/transport";
+"github.com/docker/distribution/registry/client/auth" -> "net/http";
+"github.com/docker/distribution/registry/client/auth" -> "net/url";
+"github.com/docker/distribution/registry/client/auth" -> "strings";
+"github.com/docker/distribution/registry/client/auth" -> "sync";
+"github.com/docker/distribution/registry/client/auth" -> "time";
+"github.com/docker/distribution/registry/client/auth/challenge" [label="github.com/docker/distribution/registry/client/auth/challenge" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/auth/challenge" target="_blank"];
+"github.com/docker/distribution/registry/client/auth/challenge" -> "fmt";
+"github.com/docker/distribution/registry/client/auth/challenge" -> "net/http";
+"github.com/docker/distribution/registry/client/auth/challenge" -> "net/url";
+"github.com/docker/distribution/registry/client/auth/challenge" -> "strings";
+"github.com/docker/distribution/registry/client/auth/challenge" -> "sync";
+"github.com/docker/distribution/registry/client/transport" [label="github.com/docker/distribution/registry/client/transport" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/transport" target="_blank"];
+"github.com/docker/distribution/registry/client/transport" -> "errors";
+"github.com/docker/distribution/registry/client/transport" -> "fmt";
+"github.com/docker/distribution/registry/client/transport" -> "io";
+"github.com/docker/distribution/registry/client/transport" -> "net/http";
+"github.com/docker/distribution/registry/client/transport" -> "regexp";
+"github.com/docker/distribution/registry/client/transport" -> "strconv";
+"github.com/docker/distribution/registry/client/transport" -> "sync";
+"github.com/docker/distribution/registry/storage/cache" [label="github.com/docker/distribution/registry/storage/cache" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/storage/cache" target="_blank"];
+"github.com/docker/distribution/registry/storage/cache" -> "context";
+"github.com/docker/distribution/registry/storage/cache" -> "fmt";
+"github.com/docker/distribution/registry/storage/cache" -> "github.com/docker/distribution";
+"github.com/docker/distribution/registry/storage/cache" -> "github.com/docker/distribution/metrics";
+"github.com/docker/distribution/registry/storage/cache" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/registry/storage/cache/memory" [label="github.com/docker/distribution/registry/storage/cache/memory" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/storage/cache/memory" target="_blank"];
+"github.com/docker/distribution/registry/storage/cache/memory" -> "context";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution/reference";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution/registry/storage/cache";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/opencontainers/go-digest";
+"github.com/docker/distribution/registry/storage/cache/memory" -> "sync";
+"github.com/docker/go-metrics" [label="github.com/docker/go-metrics" color="paleturquoise" URL="https://godoc.org/github.com/docker/go-metrics" target="_blank"];
+"github.com/docker/go-metrics" -> "fmt";
+"github.com/docker/go-metrics" -> "github.com/prometheus/client_golang/prometheus";
+"github.com/docker/go-metrics" -> "github.com/prometheus/client_golang/prometheus/promhttp";
+"github.com/docker/go-metrics" -> "net/http";
+"github.com/docker/go-metrics" -> "sync";
+"github.com/docker/go-metrics" -> "time";
+"github.com/golang/protobuf/proto" [label="github.com/golang/protobuf/proto" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/proto" target="_blank"];
+"github.com/golang/protobuf/proto" -> "bufio";
+"github.com/golang/protobuf/proto" -> "bytes";
+"github.com/golang/protobuf/proto" -> "encoding";
+"github.com/golang/protobuf/proto" -> "encoding/json";
+"github.com/golang/protobuf/proto" -> "errors";
+"github.com/golang/protobuf/proto" -> "fmt";
+"github.com/golang/protobuf/proto" -> "io";
+"github.com/golang/protobuf/proto" -> "log";
+"github.com/golang/protobuf/proto" -> "math";
+"github.com/golang/protobuf/proto" -> "reflect";
+"github.com/golang/protobuf/proto" -> "sort";
+"github.com/golang/protobuf/proto" -> "strconv";
+"github.com/golang/protobuf/proto" -> "strings";
+"github.com/golang/protobuf/proto" -> "sync";
+"github.com/golang/protobuf/proto" -> "sync/atomic";
+"github.com/golang/protobuf/proto" -> "unicode/utf8";
+"github.com/golang/protobuf/proto" -> "unsafe";
+"github.com/gorilla/mux" [label="github.com/gorilla/mux" color="paleturquoise" URL="https://godoc.org/github.com/gorilla/mux" target="_blank"];
+"github.com/gorilla/mux" -> "bytes";
+"github.com/gorilla/mux" -> "context";
+"github.com/gorilla/mux" -> "errors";
+"github.com/gorilla/mux" -> "fmt";
+"github.com/gorilla/mux" -> "net/http";
+"github.com/gorilla/mux" -> "net/url";
+"github.com/gorilla/mux" -> "path";
+"github.com/gorilla/mux" -> "regexp";
+"github.com/gorilla/mux" -> "strconv";
+"github.com/gorilla/mux" -> "strings";
+"github.com/matttproud/golang_protobuf_extensions/pbutil" [label="github.com/matttproud/golang_protobuf_extensions/pbutil" color="paleturquoise" URL="https://godoc.org/github.com/matttproud/golang_protobuf_extensions/pbutil" target="_blank"];
+"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "encoding/binary";
+"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "errors";
+"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "github.com/golang/protobuf/proto";
+"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "io";
+"github.com/opencontainers/go-digest" [label="github.com/opencontainers/go-digest" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/go-digest" target="_blank"];
+"github.com/opencontainers/go-digest" -> "crypto";
+"github.com/opencontainers/go-digest" -> "fmt";
+"github.com/opencontainers/go-digest" -> "hash";
+"github.com/opencontainers/go-digest" -> "io";
+"github.com/opencontainers/go-digest" -> "regexp";
+"github.com/opencontainers/go-digest" -> "strings";
+"github.com/opencontainers/image-spec/specs-go" [label="github.com/opencontainers/image-spec/specs-go" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go" target="_blank"];
+"github.com/opencontainers/image-spec/specs-go" -> "fmt";
+"github.com/opencontainers/image-spec/specs-go/v1" [label="github.com/opencontainers/image-spec/specs-go/v1" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" target="_blank"];
+"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/go-digest";
+"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/image-spec/specs-go";
+"github.com/opencontainers/image-spec/specs-go/v1" -> "time";
+"github.com/prometheus/client_golang/prometheus" [label="github.com/prometheus/client_golang/prometheus" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus" target="_blank"];
+"github.com/prometheus/client_golang/prometheus" -> "bytes";
+"github.com/prometheus/client_golang/prometheus" -> "encoding/json";
+"github.com/prometheus/client_golang/prometheus" -> "errors";
+"github.com/prometheus/client_golang/prometheus" -> "expvar";
+"github.com/prometheus/client_golang/prometheus" -> "fmt";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/beorn7/perks/quantile";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/cespare/xxhash/v2";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/golang/protobuf/proto";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/client_golang/prometheus/internal";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/client_model/go";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/common/expfmt";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/common/model";
+"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/procfs";
+"github.com/prometheus/client_golang/prometheus" -> "io/ioutil";
+"github.com/prometheus/client_golang/prometheus" -> "math";
+"github.com/prometheus/client_golang/prometheus" -> "os";
+"github.com/prometheus/client_golang/prometheus" -> "path/filepath";
+"github.com/prometheus/client_golang/prometheus" -> "runtime";
+"github.com/prometheus/client_golang/prometheus" -> "runtime/debug";
+"github.com/prometheus/client_golang/prometheus" -> "sort";
+"github.com/prometheus/client_golang/prometheus" -> "strings";
+"github.com/prometheus/client_golang/prometheus" -> "sync";
+"github.com/prometheus/client_golang/prometheus" -> "sync/atomic";
+"github.com/prometheus/client_golang/prometheus" -> "time";
+"github.com/prometheus/client_golang/prometheus" -> "unicode/utf8";
+"github.com/prometheus/client_golang/prometheus/internal" [label="github.com/prometheus/client_golang/prometheus/internal" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus/internal" target="_blank"];
+"github.com/prometheus/client_golang/prometheus/internal" -> "github.com/prometheus/client_model/go";
+"github.com/prometheus/client_golang/prometheus/internal" -> "sort";
+"github.com/prometheus/client_golang/prometheus/promhttp" [label="github.com/prometheus/client_golang/prometheus/promhttp" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp" target="_blank"];
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "bufio";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "compress/gzip";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "crypto/tls";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "errors";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "fmt";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/client_golang/prometheus";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/client_model/go";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/common/expfmt";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "io";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "net";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "net/http";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "net/http/httptrace";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "strconv";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "strings";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "sync";
+"github.com/prometheus/client_golang/prometheus/promhttp" -> "time";
+"github.com/prometheus/client_model/go" [label="github.com/prometheus/client_model/go" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_model/go" target="_blank"];
+"github.com/prometheus/client_model/go" -> "fmt";
+"github.com/prometheus/client_model/go" -> "github.com/golang/protobuf/proto";
+"github.com/prometheus/client_model/go" -> "math";
+"github.com/prometheus/common/expfmt" [label="github.com/prometheus/common/expfmt" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/expfmt" target="_blank"];
+"github.com/prometheus/common/expfmt" -> "bufio";
+"github.com/prometheus/common/expfmt" -> "bytes";
+"github.com/prometheus/common/expfmt" -> "fmt";
+"github.com/prometheus/common/expfmt" -> "github.com/golang/protobuf/proto";
+"github.com/prometheus/common/expfmt" -> "github.com/matttproud/golang_protobuf_extensions/pbutil";
+"github.com/prometheus/common/expfmt" -> "github.com/prometheus/client_model/go";
+"github.com/prometheus/common/expfmt" -> "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg";
+"github.com/prometheus/common/expfmt" -> "github.com/prometheus/common/model";
+"github.com/prometheus/common/expfmt" -> "io";
+"github.com/prometheus/common/expfmt" -> "io/ioutil";
+"github.com/prometheus/common/expfmt" -> "math";
+"github.com/prometheus/common/expfmt" -> "mime";
+"github.com/prometheus/common/expfmt" -> "net/http";
+"github.com/prometheus/common/expfmt" -> "strconv";
+"github.com/prometheus/common/expfmt" -> "strings";
+"github.com/prometheus/common/expfmt" -> "sync";
+"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" [label="github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" target="_blank"];
+"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "sort";
+"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "strconv";
+"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "strings";
+"github.com/prometheus/common/model" [label="github.com/prometheus/common/model" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/model" target="_blank"];
+"github.com/prometheus/common/model" -> "encoding/json";
+"github.com/prometheus/common/model" -> "fmt";
+"github.com/prometheus/common/model" -> "math";
+"github.com/prometheus/common/model" -> "regexp";
+"github.com/prometheus/common/model" -> "sort";
+"github.com/prometheus/common/model" -> "strconv";
+"github.com/prometheus/common/model" -> "strings";
+"github.com/prometheus/common/model" -> "time";
+"github.com/prometheus/common/model" -> "unicode/utf8";
+"github.com/prometheus/procfs" [label="github.com/prometheus/procfs" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs" target="_blank"];
+"github.com/prometheus/procfs" -> "bufio";
+"github.com/prometheus/procfs" -> "bytes";
+"github.com/prometheus/procfs" -> "encoding/hex";
+"github.com/prometheus/procfs" -> "errors";
+"github.com/prometheus/procfs" -> "fmt";
+"github.com/prometheus/procfs" -> "github.com/prometheus/procfs/internal/fs";
+"github.com/prometheus/procfs" -> "github.com/prometheus/procfs/internal/util";
+"github.com/prometheus/procfs" -> "io";
+"github.com/prometheus/procfs" -> "io/ioutil";
+"github.com/prometheus/procfs" -> "net";
+"github.com/prometheus/procfs" -> "os";
+"github.com/prometheus/procfs" -> "path/filepath";
+"github.com/prometheus/procfs" -> "regexp";
+"github.com/prometheus/procfs" -> "sort";
+"github.com/prometheus/procfs" -> "strconv";
+"github.com/prometheus/procfs" -> "strings";
+"github.com/prometheus/procfs" -> "time";
+"github.com/prometheus/procfs/internal/fs" [label="github.com/prometheus/procfs/internal/fs" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs/internal/fs" target="_blank"];
+"github.com/prometheus/procfs/internal/fs" -> "fmt";
+"github.com/prometheus/procfs/internal/fs" -> "os";
+"github.com/prometheus/procfs/internal/fs" -> "path/filepath";
+"github.com/prometheus/procfs/internal/util" [label="github.com/prometheus/procfs/internal/util" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs/internal/util" target="_blank"];
+"github.com/prometheus/procfs/internal/util" -> "bytes";
+"github.com/prometheus/procfs/internal/util" -> "io/ioutil";
+"github.com/prometheus/procfs/internal/util" -> "os";
+"github.com/prometheus/procfs/internal/util" -> "strconv";
+"github.com/prometheus/procfs/internal/util" -> "strings";
+"github.com/prometheus/procfs/internal/util" -> "syscall";
+"hash" [label="hash" color="palegreen" URL="https://godoc.org/hash" target="_blank"];
+"io" [label="io" color="palegreen" URL="https://godoc.org/io" target="_blank"];
+"io/ioutil" [label="io/ioutil" color="palegreen" URL="https://godoc.org/io/ioutil" target="_blank"];
+"log" [label="log" color="palegreen" URL="https://godoc.org/log" target="_blank"];
+"math" [label="math" color="palegreen" URL="https://godoc.org/math" target="_blank"];
+"math/bits" [label="math/bits" color="palegreen" URL="https://godoc.org/math/bits" target="_blank"];
+"mime" [label="mime" color="palegreen" URL="https://godoc.org/mime" target="_blank"];
+"net" [label="net" color="palegreen" URL="https://godoc.org/net" target="_blank"];
+"net/http" [label="net/http" color="palegreen" URL="https://godoc.org/net/http" target="_blank"];
+"net/http/httptrace" [label="net/http/httptrace" color="palegreen" URL="https://godoc.org/net/http/httptrace" target="_blank"];
+"net/url" [label="net/url" color="palegreen" URL="https://godoc.org/net/url" target="_blank"];
+"os" [label="os" color="palegreen" URL="https://godoc.org/os" target="_blank"];
+"path" [label="path" color="palegreen" URL="https://godoc.org/path" target="_blank"];
+"path/filepath" [label="path/filepath" color="palegreen" URL="https://godoc.org/path/filepath" target="_blank"];
+"reflect" [label="reflect" color="palegreen" URL="https://godoc.org/reflect" target="_blank"];
+"regexp" [label="regexp" color="palegreen" URL="https://godoc.org/regexp" target="_blank"];
+"runtime" [label="runtime" color="palegreen" URL="https://godoc.org/runtime" target="_blank"];
+"runtime/debug" [label="runtime/debug" color="palegreen" URL="https://godoc.org/runtime/debug" target="_blank"];
+"sort" [label="sort" color="palegreen" URL="https://godoc.org/sort" target="_blank"];
+"strconv" [label="strconv" color="palegreen" URL="https://godoc.org/strconv" target="_blank"];
+"strings" [label="strings" color="palegreen" URL="https://godoc.org/strings" target="_blank"];
+"sync" [label="sync" color="palegreen" URL="https://godoc.org/sync" target="_blank"];
+"sync/atomic" [label="sync/atomic" color="palegreen" URL="https://godoc.org/sync/atomic" target="_blank"];
+"syscall" [label="syscall" color="palegreen" URL="https://godoc.org/syscall" target="_blank"];
+"time" [label="time" color="palegreen" URL="https://godoc.org/time" target="_blank"];
+"unicode" [label="unicode" color="palegreen" URL="https://godoc.org/unicode" target="_blank"];
+"unicode/utf8" [label="unicode/utf8" color="palegreen" URL="https://godoc.org/unicode/utf8" target="_blank"];
+"unsafe" [label="unsafe" color="palegreen" URL="https://godoc.org/unsafe" target="_blank"];
+}
diff --git a/images/dot/ggcr.dot b/images/dot/ggcr.dot
new file mode 100644
index 0000000..459ba6d
--- /dev/null
+++ b/images/dot/ggcr.dot
@@ -0,0 +1,130 @@
+digraph godep {
+nodesep=0.4
+ranksep=0.8
+node [shape="box",style="rounded,filled"]
+edge [arrowsize="0.5"]
+"bufio" [label="bufio" color="palegreen" URL="https://godoc.org/bufio" target="_blank"];
+"bytes" [label="bytes" color="palegreen" URL="https://godoc.org/bytes" target="_blank"];
+"context" [label="context" color="palegreen" URL="https://godoc.org/context" target="_blank"];
+"encoding/base64" [label="encoding/base64" color="palegreen" URL="https://godoc.org/encoding/base64" target="_blank"];
+"encoding/json" [label="encoding/json" color="palegreen" URL="https://godoc.org/encoding/json" target="_blank"];
+"errors" [label="errors" color="palegreen" URL="https://godoc.org/errors" target="_blank"];
+"fmt" [label="fmt" color="palegreen" URL="https://godoc.org/fmt" target="_blank"];
+"github.com/docker/cli/cli/config" [label="github.com/docker/cli/cli/config" color="paleturquoise" URL="https://godoc.org/github.com/docker/cli/cli/config" target="_blank"];
+"github.com/docker/cli/cli/config" -> "fmt";
+"github.com/docker/cli/cli/config" -> "github.com/docker/cli/cli/config/configfile";
+"github.com/docker/cli/cli/config" -> "github.com/docker/cli/cli/config/credentials";
+"github.com/docker/cli/cli/config" -> "github.com/docker/cli/cli/config/types";
+"github.com/docker/cli/cli/config" -> "github.com/docker/docker/pkg/homedir";
+"github.com/docker/cli/cli/config" -> "github.com/pkg/errors";
+"github.com/docker/cli/cli/config" -> "io";
+"github.com/docker/cli/cli/config" -> "os";
+"github.com/docker/cli/cli/config" -> "path/filepath";
+"github.com/docker/cli/cli/config" -> "strings";
+"github.com/docker/cli/cli/config/configfile" [label="github.com/docker/cli/cli/config/configfile" color="paleturquoise" URL="https://godoc.org/github.com/docker/cli/cli/config/configfile" target="_blank"];
+"github.com/docker/cli/cli/config/configfile" -> "encoding/base64";
+"github.com/docker/cli/cli/config/configfile" -> "encoding/json";
+"github.com/docker/cli/cli/config/configfile" -> "fmt";
+"github.com/docker/cli/cli/config/configfile" -> "github.com/docker/cli/cli/config/credentials";
+"github.com/docker/cli/cli/config/configfile" -> "github.com/docker/cli/cli/config/types";
+"github.com/docker/cli/cli/config/configfile" -> "github.com/pkg/errors";
+"github.com/docker/cli/cli/config/configfile" -> "io";
+"github.com/docker/cli/cli/config/configfile" -> "io/ioutil";
+"github.com/docker/cli/cli/config/configfile" -> "os";
+"github.com/docker/cli/cli/config/configfile" -> "path/filepath";
+"github.com/docker/cli/cli/config/configfile" -> "strings";
+"github.com/docker/cli/cli/config/credentials" [label="github.com/docker/cli/cli/config/credentials" color="paleturquoise" URL="https://godoc.org/github.com/docker/cli/cli/config/credentials" target="_blank"];
+"github.com/docker/cli/cli/config/credentials" -> "github.com/docker/cli/cli/config/types";
+"github.com/docker/cli/cli/config/credentials" -> "github.com/docker/docker-credential-helpers/client";
+"github.com/docker/cli/cli/config/credentials" -> "github.com/docker/docker-credential-helpers/credentials";
+"github.com/docker/cli/cli/config/credentials" -> "os/exec";
+"github.com/docker/cli/cli/config/credentials" -> "strings";
+"github.com/docker/cli/cli/config/types" [label="github.com/docker/cli/cli/config/types" color="paleturquoise" URL="https://godoc.org/github.com/docker/cli/cli/config/types" target="_blank"];
+"github.com/docker/docker-credential-helpers/client" [label="github.com/docker/docker-credential-helpers/client" color="palegoldenrod" URL="https://godoc.org/github.com/docker/docker-credential-helpers/client" target="_blank"];
+"github.com/docker/docker-credential-helpers/client" -> "bytes";
+"github.com/docker/docker-credential-helpers/client" -> "encoding/json";
+"github.com/docker/docker-credential-helpers/client" -> "fmt";
+"github.com/docker/docker-credential-helpers/client" -> "github.com/docker/docker-credential-helpers/credentials";
+"github.com/docker/docker-credential-helpers/client" -> "io";
+"github.com/docker/docker-credential-helpers/client" -> "os";
+"github.com/docker/docker-credential-helpers/client" -> "os/exec";
+"github.com/docker/docker-credential-helpers/client" -> "strings";
+"github.com/docker/docker-credential-helpers/credentials" [label="github.com/docker/docker-credential-helpers/credentials" color="palegoldenrod" URL="https://godoc.org/github.com/docker/docker-credential-helpers/credentials" target="_blank"];
+"github.com/docker/docker-credential-helpers/credentials" -> "bufio";
+"github.com/docker/docker-credential-helpers/credentials" -> "bytes";
+"github.com/docker/docker-credential-helpers/credentials" -> "encoding/json";
+"github.com/docker/docker-credential-helpers/credentials" -> "fmt";
+"github.com/docker/docker-credential-helpers/credentials" -> "io";
+"github.com/docker/docker-credential-helpers/credentials" -> "os";
+"github.com/docker/docker-credential-helpers/credentials" -> "strings";
+"github.com/docker/docker/pkg/homedir" [label="github.com/docker/docker/pkg/homedir" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/homedir" target="_blank"];
+"github.com/docker/docker/pkg/homedir" -> "errors";
+"github.com/docker/docker/pkg/homedir" -> "os";
+"github.com/docker/docker/pkg/homedir" -> "os/user";
+"github.com/docker/docker/pkg/homedir" -> "path/filepath";
+"github.com/docker/docker/pkg/homedir" -> "strings";
+"github.com/google/go-containerregistry/pkg/authn" [label="github.com/google/go-containerregistry/pkg/authn" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/authn" target="_blank"];
+"github.com/google/go-containerregistry/pkg/authn" -> "encoding/json";
+"github.com/google/go-containerregistry/pkg/authn" -> "github.com/docker/cli/cli/config";
+"github.com/google/go-containerregistry/pkg/authn" -> "github.com/docker/cli/cli/config/types";
+"github.com/google/go-containerregistry/pkg/authn" -> "github.com/google/go-containerregistry/pkg/logs";
+"github.com/google/go-containerregistry/pkg/authn" -> "github.com/google/go-containerregistry/pkg/name";
+"github.com/google/go-containerregistry/pkg/authn" -> "os";
+"github.com/google/go-containerregistry/pkg/internal/retry" [label="github.com/google/go-containerregistry/pkg/internal/retry" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/internal/retry" target="_blank"];
+"github.com/google/go-containerregistry/pkg/internal/retry" -> "context";
+"github.com/google/go-containerregistry/pkg/internal/retry" -> "fmt";
+"github.com/google/go-containerregistry/pkg/internal/retry" -> "github.com/google/go-containerregistry/pkg/internal/retry/wait";
+"github.com/google/go-containerregistry/pkg/internal/retry/wait" [label="github.com/google/go-containerregistry/pkg/internal/retry/wait" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/internal/retry/wait" target="_blank"];
+"github.com/google/go-containerregistry/pkg/internal/retry/wait" -> "errors";
+"github.com/google/go-containerregistry/pkg/internal/retry/wait" -> "math/rand";
+"github.com/google/go-containerregistry/pkg/internal/retry/wait" -> "time";
+"github.com/google/go-containerregistry/pkg/logs" [label="github.com/google/go-containerregistry/pkg/logs" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/logs" target="_blank"];
+"github.com/google/go-containerregistry/pkg/logs" -> "io/ioutil";
+"github.com/google/go-containerregistry/pkg/logs" -> "log";
+"github.com/google/go-containerregistry/pkg/name" [label="github.com/google/go-containerregistry/pkg/name" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/name" target="_blank"];
+"github.com/google/go-containerregistry/pkg/name" -> "fmt";
+"github.com/google/go-containerregistry/pkg/name" -> "net";
+"github.com/google/go-containerregistry/pkg/name" -> "net/url";
+"github.com/google/go-containerregistry/pkg/name" -> "regexp";
+"github.com/google/go-containerregistry/pkg/name" -> "strings";
+"github.com/google/go-containerregistry/pkg/name" -> "unicode/utf8";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" [label="github.com/google/go-containerregistry/pkg/v1/remote/transport" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport" target="_blank"];
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "encoding/base64";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "encoding/json";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "fmt";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "github.com/google/go-containerregistry/pkg/authn";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "github.com/google/go-containerregistry/pkg/internal/retry";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "github.com/google/go-containerregistry/pkg/logs";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "github.com/google/go-containerregistry/pkg/name";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "io/ioutil";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "net";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "net/http";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "net/http/httputil";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "net/url";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "strings";
+"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "time";
+"github.com/pkg/errors" [label="github.com/pkg/errors" color="palegoldenrod" URL="https://godoc.org/github.com/pkg/errors" target="_blank"];
+"github.com/pkg/errors" -> "fmt";
+"github.com/pkg/errors" -> "io";
+"github.com/pkg/errors" -> "path";
+"github.com/pkg/errors" -> "runtime";
+"github.com/pkg/errors" -> "strings";
+"io" [label="io" color="palegreen" URL="https://godoc.org/io" target="_blank"];
+"io/ioutil" [label="io/ioutil" color="palegreen" URL="https://godoc.org/io/ioutil" target="_blank"];
+"log" [label="log" color="palegreen" URL="https://godoc.org/log" target="_blank"];
+"math/rand" [label="math/rand" color="palegreen" URL="https://godoc.org/math/rand" target="_blank"];
+"net" [label="net" color="palegreen" URL="https://godoc.org/net" target="_blank"];
+"net/http" [label="net/http" color="palegreen" URL="https://godoc.org/net/http" target="_blank"];
+"net/http/httputil" [label="net/http/httputil" color="palegreen" URL="https://godoc.org/net/http/httputil" target="_blank"];
+"net/url" [label="net/url" color="palegreen" URL="https://godoc.org/net/url" target="_blank"];
+"os" [label="os" color="palegreen" URL="https://godoc.org/os" target="_blank"];
+"os/exec" [label="os/exec" color="palegreen" URL="https://godoc.org/os/exec" target="_blank"];
+"os/user" [label="os/user" color="palegreen" URL="https://godoc.org/os/user" target="_blank"];
+"path" [label="path" color="palegreen" URL="https://godoc.org/path" target="_blank"];
+"path/filepath" [label="path/filepath" color="palegreen" URL="https://godoc.org/path/filepath" target="_blank"];
+"regexp" [label="regexp" color="palegreen" URL="https://godoc.org/regexp" target="_blank"];
+"runtime" [label="runtime" color="palegreen" URL="https://godoc.org/runtime" target="_blank"];
+"strings" [label="strings" color="palegreen" URL="https://godoc.org/strings" target="_blank"];
+"time" [label="time" color="palegreen" URL="https://godoc.org/time" target="_blank"];
+"unicode/utf8" [label="unicode/utf8" color="palegreen" URL="https://godoc.org/unicode/utf8" target="_blank"];
+}
diff --git a/images/dot/image-anatomy.dot b/images/dot/image-anatomy.dot
new file mode 100644
index 0000000..179e311
--- /dev/null
+++ b/images/dot/image-anatomy.dot
@@ -0,0 +1,26 @@
+digraph {
+ compound=true;
+ rankdir="LR";
+
+ tag [label="", shape="circle", width=0.1, style="filled", color="black"];
+ manifest [shape="note"];
+ config [shape="note"];
+
+ tag -> manifest [label="digest", taillabel="tag", tailport=head, labeldistance=2.1, labelangle=108];
+ manifest -> config [label="(image id)"];
+ config -> l1 [label="diffid"];
+ config -> l2 [label="diffid"];
+ manifest -> l1 [lhead=cluster_layer1, label="layer digest"];
+ manifest -> l2 [lhead=cluster_layer2, label="layer digest"];
+
+ subgraph cluster_layer1 {
+ label = "layer.tar.gz";
+ margin = 20.0;
+ l1 [label="layer.tar", shape="folder"];
+ }
+ subgraph cluster_layer2 {
+ label = "layer.tar.gz";
+ margin = 20.0;
+ l2 [label="layer.tar", shape="folder"];
+ }
+}
diff --git a/images/dot/index-anatomy-strange.dot b/images/dot/index-anatomy-strange.dot
new file mode 100644
index 0000000..2bccba3
--- /dev/null
+++ b/images/dot/index-anatomy-strange.dot
@@ -0,0 +1,24 @@
+digraph {
+ ordering = out;
+ compound=true;
+ rankdir="LR";
+
+ tag [label="", shape="circle", width=0.1, style="filled", color="black"];
+ tag2 [label="", shape="circle", width=0.1, style="filled", color="black"];
+ tag3 [label="", shape="circle", width=0.1, style="filled", color="black"];
+ index [shape="note"];
+ index2 [label="index", shape="note"];
+ image [shape="note"];
+ image2 [label="image", shape="note"];
+ image3 [label="image", shape="note"];
+ xml;
+
+ tag -> index [taillabel="r124356", tailport=head, labeldistance=2.1, labelangle=108];
+ tag2 -> index2 [taillabel="stable-release", tailport=head, labeldistance=2.1, labelangle=108];
+ tag3 -> image [taillabel="v1.0", tailport=head, labeldistance=2.1, labelangle=108];
+ index -> image;
+ index -> xml;
+ index -> index2;
+ index2 -> image2;
+ index2 -> image3;
+}
diff --git a/images/dot/index-anatomy.dot b/images/dot/index-anatomy.dot
new file mode 100644
index 0000000..9155af0
--- /dev/null
+++ b/images/dot/index-anatomy.dot
@@ -0,0 +1,18 @@
+digraph {
+ ordering = out;
+ compound=true;
+ rankdir="LR";
+
+ tag [label="", shape="circle", width=0.1, style="filled", color="black"];
+ tag2 [label="", shape="circle", width=0.1, style="filled", color="black"];
+ tag3 [label="", shape="circle", width=0.1, style="filled", color="black"];
+ index [shape="note"];
+ image [shape="note"];
+ image2 [label="image", shape="note"];
+
+ tag -> index [taillabel="latest", tailport=head, labeldistance=2.1, labelangle=108];
+ tag2 -> image [taillabel="amd64", tailport=head, labeldistance=2.1, labelangle=108];
+ tag3 -> image2 [taillabel="ppc64le", tailport=head, labeldistance=2.1, labelangle=252];
+ index -> image;
+ index -> image2;
+}
diff --git a/images/dot/mutate.dot b/images/dot/mutate.dot
new file mode 100644
index 0000000..228f8b6
--- /dev/null
+++ b/images/dot/mutate.dot
@@ -0,0 +1,59 @@
+digraph {
+ input [label="v1.Image", shape=box];
+ output [label="v1.Image", shape=box];
+
+ ordering = "out";
+
+ subgraph cluster_source {
+ label = "Sources";
+ "remotesource" [label="remote"];
+ "tarballsource" [label="tarball"];
+ "randomsource" [label="random"];
+ "layoutsource" [label="layout"];
+ "daemonsource" [label="daemon"];
+ }
+
+ subgraph cluster_mutate {
+ label = "mutate";
+ "mutateconfig" [label="Config"];
+ "mutatetime" [label="Time"];
+ "mutatemediatype" [label="MediaType"];
+ "mutateappend" [label="Append"];
+ "mutaterebase" [label="Rebase"];
+ }
+
+ subgraph cluster_sinks {
+ label = "Sinks";
+ labelloc = "b";
+
+ "remotesink" [label="remote"];
+ "tarballsink" [label="tarball"];
+ "legacy/tarballsink" [label="legacy/tarball"];
+ "layoutsink" [label="layout"];
+ "daemonsink" [label="daemon"];
+ }
+
+ "randomsource" -> input;
+ "layoutsource" -> input;
+ "daemonsource" -> input;
+ "tarballsource" -> input;
+ "remotesource" -> input;
+
+ input -> "mutateconfig";
+ input -> "mutatetime";
+ input -> "mutatemediatype";
+ input -> "mutateappend";
+ input -> "mutaterebase";
+
+ "mutateconfig" -> output;
+ "mutatetime" -> output;
+ "mutatemediatype" -> output;
+ "mutateappend" -> output;
+ "mutaterebase" -> output;
+
+ output -> "legacy/tarballsink";
+ output -> "layoutsink";
+ output -> "daemonsink";
+ output -> "tarballsink";
+ output -> "remotesink";
+}
diff --git a/images/dot/remote.dot b/images/dot/remote.dot
new file mode 100644
index 0000000..9b5e08c
--- /dev/null
+++ b/images/dot/remote.dot
@@ -0,0 +1,66 @@
+digraph {
+ compound=true;
+ rankdir="LR";
+ ordering = in;
+
+ subgraph cluster_registry {
+ label = "registry";
+
+ subgraph cluster_tags {
+ label = "/v2/.../tags/list";
+
+ tag [label="tag", shape="rect"];
+ tag2 [label="tag", shape="rect"];
+ }
+
+ subgraph cluster_manifests {
+ label = "/v2/.../manifests/<ref>";
+
+ subgraph cluster_manifest {
+ label = "manifest";
+
+ mconfig [label="config", shape="rect"];
+ layers [label="layers", shape="rect"];
+ }
+
+ subgraph cluster_manifest2 {
+ label = "manifest";
+
+ mconfig2 [label="config", shape="rect"];
+ layers2 [label="layers", shape="rect"];
+ }
+
+ subgraph cluster_index {
+ label = "index";
+
+ imanifest [label="manifests", shape="rect"];
+ }
+
+ imanifest -> mconfig [lhead=cluster_manifest];
+ imanifest -> mconfig2 [lhead=cluster_manifest2];
+ }
+
+ subgraph cluster_blobs {
+ label = "/v2/.../blobs/<sha256>";
+
+ bconfig [label="config", shape="hexagon"];
+ bconfig2 [label="config", shape="hexagon"];
+
+ l1 [label="layer", shape="folder"];
+ l2 [label="layer", shape="folder"];
+ l3 [label="layer", shape="folder"];
+ }
+
+ layers -> l1;
+ layers -> l2;
+
+ layers2 -> l2;
+ layers2 -> l3;
+
+ mconfig -> bconfig;
+ mconfig2 -> bconfig2;
+
+ tag -> mconfig [style="dashed", lhead=cluster_manifest];
+ tag2 -> imanifest [style="dashed", lhead=cluster_index];
+ }
+}
diff --git a/images/dot/stream.dot b/images/dot/stream.dot
new file mode 100644
index 0000000..0987be7
--- /dev/null
+++ b/images/dot/stream.dot
@@ -0,0 +1,47 @@
+digraph G {
+ ordering=out;
+
+ fs [label="input", shape="folder"];
+ pr [label="io.PipeReader"];
+ compressed [label="Compressed()", shape="rect"];
+ rc2 [label="io.ReadCloser"];
+ output [label="output", shape="cylinder"];
+
+ subgraph cluster_goroutine {
+ label = "goroutine";
+
+ rc [label="io.ReadCloser"];
+ copy [label="io.Copy"];
+ pw [label="io.PipeWriter"];
+ mw [label="io.MultiWriter"];
+ h1 [label="sha256.New"];
+ gzip [label="gzip.Writer"];
+ mw2 [label="io.MultiWriter"];
+ h2 [label="sha256.New"];
+ count [label="countWriter"];
+
+ size [label="Size()", shape="rect"];
+ diffid [label="DiffID()", shape="rect"];
+ digest [label="Digest()", shape="rect"];
+
+
+ rc -> copy [style="bold"];
+ copy -> mw [style="bold"];
+ mw -> h1;
+ h1 -> diffid [style="dashed"];
+ mw -> gzip [style="bold"];
+ gzip -> mw2 [style="bold"];
+ mw2 -> h2;
+ h2 -> digest [style="dashed"];
+ mw2 -> count;
+ count -> size [style="dotted"];
+ mw2 -> pw [style="bold"];
+ };
+
+ fs -> rc [style="bold"];
+
+ pw -> pr [style="bold"];
+ pr -> compressed [style="bold"];
+ compressed -> rc2 [style="bold"];
+ rc2 -> output [style="bold"];
+}
diff --git a/images/dot/tarball.dot b/images/dot/tarball.dot
new file mode 100644
index 0000000..595283f
--- /dev/null
+++ b/images/dot/tarball.dot
@@ -0,0 +1,43 @@
+digraph {
+ compound=true;
+ rankdir="LR";
+ ordering = out;
+
+ subgraph cluster_tarball {
+ label = "image.tar";
+
+ subgraph cluster_manifest {
+ label = "manifest.json";
+
+ mconfig [label="Config", shape="rect"];
+ layers [label="Layers", shape="rect"];
+ sources [label="LayerSources", shape="rect"];
+ tags [label="RepoTags", shape="rect"];
+ }
+
+ config [shape="note"];
+
+ mconfig -> config [label="image id"];
+
+ layers -> l1 [lhead=cluster_layer1, label="layer digest"];
+ layers -> l2 [lhead=cluster_layer2, label="layer digest"];
+
+ config -> l1 [label="diffid"];
+ config -> l2 [label="diffid"];
+
+ sources -> l1 [label="diffid"];
+ sources -> l2 [label="diffid"];
+
+ subgraph cluster_layer1 {
+ label = "layer.tar.gz";
+ margin = 20.0;
+ l1 [label="layer.tar", shape="folder"];
+ }
+
+ subgraph cluster_layer2 {
+ label = "layer.tar.gz";
+ margin = 20.0;
+ l2 [label="layer.tar", shape="folder"];
+ }
+ }
+}
diff --git a/images/dot/upload.dot b/images/dot/upload.dot
new file mode 100644
index 0000000..2cb3e26
--- /dev/null
+++ b/images/dot/upload.dot
@@ -0,0 +1,67 @@
+digraph G {
+ ordering=out;
+
+ fs [label="filesystem\nchangeset", shape=folder, href="https://github.com/opencontainers/image-spec/blob/master/layer.md"];
+ configuration [label="image\nconfig", shape=hexagon, href="https://github.com/opencontainers/image-spec/blob/master/config.md#properties"];
+
+ tar [shape=rect];
+ gzip [shape=rect];
+ tee [shape=rect];
+ tee2 [label=tee, shape=rect];
+ tee3 [label=tee, shape=rect];
+ sha256sum [shape=rect];
+ sha256sum2 [label=sha256sum, shape=rect];
+ sha256sum3 [label=sha256sum, shape=rect];
+ curl [shape=rect];
+ curl2 [label=curl, shape=rect];
+ curl3 [label=curl, shape=rect];
+ wc [label="wc -c", shape=rect];
+ wc2 [label="wc -c", shape=rect];
+
+ config [label="config file", shape=note, href="https://github.com/opencontainers/image-spec/blob/master/config.md"];
+ layer [shape=note, href="https://github.com/opencontainers/image-spec/blob/master/layer.md"];
+ manifest [shape=note, href="https://github.com/opencontainers/image-spec/blob/master/manifest.md"];
+
+ registry [shape=cylinder, href="https://github.com/opencontainers/distribution-spec/blob/master/spec.md"];
+
+ config_size [label="config size"];
+ layer_size [label="layer size"];
+ config_digest [label="config digest\n(image id)", href="https://github.com/opencontainers/image-spec/blob/master/config.md#imageid"];
+ layer_digest [label="layer digest"];
+
+ diffid [href="https://github.com/opencontainers/image-spec/blob/master/config.md#layer-diffid"];
+
+ configuration -> config;
+ fs -> tar;
+
+ tar -> tee;
+ tee -> sha256sum;
+ sha256sum -> diffid [style=dashed];
+ tee -> gzip;
+ gzip -> layer;
+ layer -> tee2;
+ tee2 -> sha256sum2;
+ sha256sum2 -> layer_digest [style=dashed];
+ tee2 -> wc;
+ wc -> layer_size [style=dotted];
+ layer_size -> manifest [style=dotted];
+ tee2 -> curl;
+
+ curl -> registry;
+
+ diffid -> config [style=dashed];
+ config -> tee3;
+ tee3 -> curl2;
+ curl2 -> registry;
+
+ tee3 -> wc2;
+ tee3 -> sha256sum3;
+ wc2 -> config_size [style=dotted];
+ sha256sum3 -> config_digest [style=dashed];
+
+ config_digest -> manifest [style=dashed];
+ config_size -> manifest [style=dotted];
+ layer_digest -> manifest [style=dashed];
+ manifest -> curl3;
+ curl3 -> registry;
+}
diff --git a/images/gcrane.png b/images/gcrane.png
new file mode 100644
index 0000000..461fbfe
--- /dev/null
+++ b/images/gcrane.png
Binary files differ
diff --git a/images/ggcr.dot.svg b/images/ggcr.dot.svg
new file mode 100644
index 0000000..3dbeccd
--- /dev/null
+++ b/images/ggcr.dot.svg
@@ -0,0 +1,874 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: godep Pages: 1 -->
+<svg width="3212pt" height="702pt"
+ viewBox="0.00 0.00 3211.50 702.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 698)">
+<title>godep</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-698 3207.5,-698 3207.5,4 -4,4"/>
+<!-- bufio -->
+<g id="node1" class="node">
+<title>bufio</title>
+<g id="a_node1"><a xlink:href="https://godoc.org/bufio" xlink:title="bufio" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M828,-36C828,-36 798,-36 798,-36 792,-36 786,-30 786,-24 786,-24 786,-12 786,-12 786,-6 792,0 798,0 798,0 828,0 828,0 834,0 840,-6 840,-12 840,-12 840,-24 840,-24 840,-30 834,-36 828,-36"/>
+<text text-anchor="middle" x="813" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bufio</text>
+</a>
+</g>
+</g>
+<!-- bytes -->
+<g id="node2" class="node">
+<title>bytes</title>
+<g id="a_node2"><a xlink:href="https://godoc.org/bytes" xlink:title="bytes" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M683,-36C683,-36 653,-36 653,-36 647,-36 641,-30 641,-24 641,-24 641,-12 641,-12 641,-6 647,0 653,0 653,0 683,0 683,0 689,0 695,-6 695,-12 695,-12 695,-24 695,-24 695,-30 689,-36 683,-36"/>
+<text text-anchor="middle" x="668" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bytes</text>
+</a>
+</g>
+</g>
+<!-- context -->
+<g id="node3" class="node">
+<title>context</title>
+<g id="a_node3"><a xlink:href="https://godoc.org/context" xlink:title="context" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2528,-506C2528,-506 2496,-506 2496,-506 2490,-506 2484,-500 2484,-494 2484,-494 2484,-482 2484,-482 2484,-476 2490,-470 2496,-470 2496,-470 2528,-470 2528,-470 2534,-470 2540,-476 2540,-482 2540,-482 2540,-494 2540,-494 2540,-500 2534,-506 2528,-506"/>
+<text text-anchor="middle" x="2512" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">context</text>
+</a>
+</g>
+</g>
+<!-- encoding/base64 -->
+<g id="node4" class="node">
+<title>encoding/base64</title>
+<g id="a_node4"><a xlink:href="https://godoc.org/encoding/base64" xlink:title="encoding/base64" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M257.5,-318C257.5,-318 174.5,-318 174.5,-318 168.5,-318 162.5,-312 162.5,-306 162.5,-306 162.5,-294 162.5,-294 162.5,-288 168.5,-282 174.5,-282 174.5,-282 257.5,-282 257.5,-282 263.5,-282 269.5,-288 269.5,-294 269.5,-294 269.5,-306 269.5,-306 269.5,-312 263.5,-318 257.5,-318"/>
+<text text-anchor="middle" x="216" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/base64</text>
+</a>
+</g>
+</g>
+<!-- encoding/json -->
+<g id="node5" class="node">
+<title>encoding/json</title>
+<g id="a_node5"><a xlink:href="https://godoc.org/encoding/json" xlink:title="encoding/json" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M228,-36C228,-36 160,-36 160,-36 154,-36 148,-30 148,-24 148,-24 148,-12 148,-12 148,-6 154,0 160,0 160,0 228,0 228,0 234,0 240,-6 240,-12 240,-12 240,-24 240,-24 240,-30 234,-36 228,-36"/>
+<text text-anchor="middle" x="194" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/json</text>
+</a>
+</g>
+</g>
+<!-- errors -->
+<g id="node6" class="node">
+<title>errors</title>
+<g id="a_node6"><a xlink:href="https://godoc.org/errors" xlink:title="errors" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2236,-318C2236,-318 2206,-318 2206,-318 2200,-318 2194,-312 2194,-306 2194,-306 2194,-294 2194,-294 2194,-288 2200,-282 2206,-282 2206,-282 2236,-282 2236,-282 2242,-282 2248,-288 2248,-294 2248,-294 2248,-306 2248,-306 2248,-312 2242,-318 2236,-318"/>
+<text text-anchor="middle" x="2221" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">errors</text>
+</a>
+</g>
+</g>
+<!-- fmt -->
+<g id="node7" class="node">
+<title>fmt</title>
+<g id="a_node7"><a xlink:href="https://godoc.org/fmt" xlink:title="fmt" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1986,-36C1986,-36 1956,-36 1956,-36 1950,-36 1944,-30 1944,-24 1944,-24 1944,-12 1944,-12 1944,-6 1950,0 1956,0 1956,0 1986,0 1986,0 1992,0 1998,-6 1998,-12 1998,-12 1998,-24 1998,-24 1998,-30 1992,-36 1986,-36"/>
+<text text-anchor="middle" x="1971" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">fmt</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config -->
+<g id="node8" class="node">
+<title>github.com/docker/cli/cli/config</title>
+<g id="a_node8"><a xlink:href="https://godoc.org/github.com/docker/cli/cli/config" xlink:title="github.com/docker/cli/cli/config" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1254,-506C1254,-506 1086,-506 1086,-506 1080,-506 1074,-500 1074,-494 1074,-494 1074,-482 1074,-482 1074,-476 1080,-470 1086,-470 1086,-470 1254,-470 1254,-470 1260,-470 1266,-476 1266,-482 1266,-482 1266,-494 1266,-494 1266,-500 1260,-506 1254,-506"/>
+<text text-anchor="middle" x="1170" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/cli/cli/config</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;fmt -->
+<g id="edge1" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1266.2661,-477.1263C1373.9923,-463.8751 1540.9149,-439.7838 1598,-412 1777.8388,-324.4709 1915.961,-111.0793 1957.8445,-40.8353"/>
+<polygon fill="#000000" stroke="#000000" points="1959.4186,-41.6117 1960.4635,-36.4184 1956.4081,-39.8266 1959.4186,-41.6117"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile -->
+<g id="node9" class="node">
+<title>github.com/docker/cli/cli/config/configfile</title>
+<g id="a_node9"><a xlink:href="https://godoc.org/github.com/docker/cli/cli/config/configfile" xlink:title="github.com/docker/cli/cli/config/configfile" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1084,-412C1084,-412 860,-412 860,-412 854,-412 848,-406 848,-400 848,-400 848,-388 848,-388 848,-382 854,-376 860,-376 860,-376 1084,-376 1084,-376 1090,-376 1096,-382 1096,-388 1096,-388 1096,-400 1096,-400 1096,-406 1090,-412 1084,-412"/>
+<text text-anchor="middle" x="972" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/cli/cli/config/configfile</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;github.com/docker/cli/cli/config/configfile -->
+<g id="edge2" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;github.com/docker/cli/cli/config/configfile</title>
+<path fill="none" stroke="#000000" d="M1131.8236,-469.8759C1098.2688,-453.9458 1049.4939,-430.79 1014.6636,-414.2545"/>
+<polygon fill="#000000" stroke="#000000" points="1015.3266,-412.632 1010.0592,-412.0685 1013.8255,-415.7938 1015.3266,-412.632"/>
+</g>
+<!-- github.com/docker/cli/cli/config/credentials -->
+<g id="node10" class="node">
+<title>github.com/docker/cli/cli/config/credentials</title>
+<g id="a_node10"><a xlink:href="https://godoc.org/github.com/docker/cli/cli/config/credentials" xlink:title="github.com/docker/cli/cli/config/credentials" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M925,-318C925,-318 695,-318 695,-318 689,-318 683,-312 683,-306 683,-306 683,-294 683,-294 683,-288 689,-282 695,-282 695,-282 925,-282 925,-282 931,-282 937,-288 937,-294 937,-294 937,-306 937,-306 937,-312 931,-318 925,-318"/>
+<text text-anchor="middle" x="810" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/cli/cli/config/credentials</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;github.com/docker/cli/cli/config/credentials -->
+<g id="edge3" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;github.com/docker/cli/cli/config/credentials</title>
+<path fill="none" stroke="#000000" d="M1073.825,-477.9324C986.3917,-466.7805 866.1268,-445.7713 833,-412 810.0302,-388.5833 807.1309,-348.7312 807.962,-323.4184"/>
+<polygon fill="#000000" stroke="#000000" points="809.7179,-323.3211 808.19,-318.2488 806.2213,-323.1667 809.7179,-323.3211"/>
+</g>
+<!-- github.com/docker/cli/cli/config/types -->
+<g id="node11" class="node">
+<title>github.com/docker/cli/cli/config/types</title>
+<g id="a_node11"><a xlink:href="https://godoc.org/github.com/docker/cli/cli/config/types" xlink:title="github.com/docker/cli/cli/config/types" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1247,-224C1247,-224 1047,-224 1047,-224 1041,-224 1035,-218 1035,-212 1035,-212 1035,-200 1035,-200 1035,-194 1041,-188 1047,-188 1047,-188 1247,-188 1247,-188 1253,-188 1259,-194 1259,-200 1259,-200 1259,-212 1259,-212 1259,-218 1253,-224 1247,-224"/>
+<text text-anchor="middle" x="1147" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/cli/cli/config/types</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;github.com/docker/cli/cli/config/types -->
+<g id="edge4" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;github.com/docker/cli/cli/config/types</title>
+<path fill="none" stroke="#000000" d="M1168.5263,-469.9306C1164.5114,-420.7051 1153.3823,-284.252 1148.9034,-229.3379"/>
+<polygon fill="#000000" stroke="#000000" points="1150.642,-229.125 1148.4912,-224.2838 1147.1536,-229.4096 1150.642,-229.125"/>
+</g>
+<!-- github.com/docker/docker/pkg/homedir -->
+<g id="node12" class="node">
+<title>github.com/docker/docker/pkg/homedir</title>
+<g id="a_node12"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/homedir" xlink:title="github.com/docker/docker/pkg/homedir" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1464.5,-412C1464.5,-412 1255.5,-412 1255.5,-412 1249.5,-412 1243.5,-406 1243.5,-400 1243.5,-400 1243.5,-388 1243.5,-388 1243.5,-382 1249.5,-376 1255.5,-376 1255.5,-376 1464.5,-376 1464.5,-376 1470.5,-376 1476.5,-382 1476.5,-388 1476.5,-388 1476.5,-400 1476.5,-400 1476.5,-406 1470.5,-412 1464.5,-412"/>
+<text text-anchor="middle" x="1360" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/homedir</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;github.com/docker/docker/pkg/homedir -->
+<g id="edge5" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;github.com/docker/docker/pkg/homedir</title>
+<path fill="none" stroke="#000000" d="M1206.6339,-469.8759C1238.7015,-454.0108 1285.2554,-430.9789 1318.6502,-414.4573"/>
+<polygon fill="#000000" stroke="#000000" points="1319.773,-415.8543 1323.4786,-412.0685 1318.221,-412.7172 1319.773,-415.8543"/>
+</g>
+<!-- github.com/pkg/errors -->
+<g id="node13" class="node">
+<title>github.com/pkg/errors</title>
+<g id="a_node13"><a xlink:href="https://godoc.org/github.com/pkg/errors" xlink:title="github.com/pkg/errors" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M1709,-130C1709,-130 1595,-130 1595,-130 1589,-130 1583,-124 1583,-118 1583,-118 1583,-106 1583,-106 1583,-100 1589,-94 1595,-94 1595,-94 1709,-94 1709,-94 1715,-94 1721,-100 1721,-106 1721,-106 1721,-118 1721,-118 1721,-124 1715,-130 1709,-130"/>
+<text text-anchor="middle" x="1652" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/pkg/errors</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;github.com/pkg/errors -->
+<g id="edge6" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M1266.0328,-479.5512C1343.3902,-470.1597 1453.5964,-450.8479 1543,-412 1576.0277,-397.6487 1658.4991,-350.008 1675,-318 1706.1362,-257.6029 1678.8276,-174.2295 1662.3518,-134.633"/>
+<polygon fill="#000000" stroke="#000000" points="1663.962,-133.9476 1660.4009,-130.0257 1660.739,-135.3123 1663.962,-133.9476"/>
+</g>
+<!-- io -->
+<g id="node14" class="node">
+<title>io</title>
+<g id="a_node14"><a xlink:href="https://godoc.org/io" xlink:title="io" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1034,-36C1034,-36 1004,-36 1004,-36 998,-36 992,-30 992,-24 992,-24 992,-12 992,-12 992,-6 998,0 1004,0 1004,0 1034,0 1034,0 1040,0 1046,-6 1046,-12 1046,-12 1046,-24 1046,-24 1046,-30 1040,-36 1034,-36"/>
+<text text-anchor="middle" x="1019" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;io -->
+<g id="edge7" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M1073.6611,-480.3086C896.553,-463.8416 530.9911,-418.2188 462,-318 390.1382,-213.6112 368.5322,-243.7443 637,-94 697.5302,-60.2378 906.0734,-31.813 986.764,-21.8273"/>
+<polygon fill="#000000" stroke="#000000" points="987.1423,-23.5441 991.8911,-21.1964 986.7147,-20.0703 987.1423,-23.5441"/>
+</g>
+<!-- os -->
+<g id="node15" class="node">
+<title>os</title>
+<g id="a_node15"><a xlink:href="https://godoc.org/os" xlink:title="os" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M533,-36C533,-36 503,-36 503,-36 497,-36 491,-30 491,-24 491,-24 491,-12 491,-12 491,-6 497,0 503,0 503,0 533,0 533,0 539,0 545,-6 545,-12 545,-12 545,-24 545,-24 545,-30 539,-36 533,-36"/>
+<text text-anchor="middle" x="518" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">os</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;os -->
+<g id="edge8" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M1073.7305,-482.9164C922.5411,-473.8315 637.0683,-451.9178 544,-412 438.4175,-366.7147 384.0948,-298.6494 415,-188 431.3471,-129.4728 473.9404,-71.0262 498.9374,-40.242"/>
+<polygon fill="#000000" stroke="#000000" points="500.38,-41.2425 502.194,-36.2654 497.6721,-39.025 500.38,-41.2425"/>
+</g>
+<!-- path/filepath -->
+<g id="node16" class="node">
+<title>path/filepath</title>
+<g id="a_node16"><a xlink:href="https://godoc.org/path/filepath" xlink:title="path/filepath" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1393.5,-318C1393.5,-318 1332.5,-318 1332.5,-318 1326.5,-318 1320.5,-312 1320.5,-306 1320.5,-306 1320.5,-294 1320.5,-294 1320.5,-288 1326.5,-282 1332.5,-282 1332.5,-282 1393.5,-282 1393.5,-282 1399.5,-282 1405.5,-288 1405.5,-294 1405.5,-294 1405.5,-306 1405.5,-306 1405.5,-312 1399.5,-318 1393.5,-318"/>
+<text text-anchor="middle" x="1363" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">path/filepath</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;path/filepath -->
+<g id="edge9" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M1175.9422,-469.7514C1184.489,-445.8127 1202.2302,-403.615 1229,-376 1253.2845,-350.9488 1287.6155,-331.6936 1315.4478,-318.8326"/>
+<polygon fill="#000000" stroke="#000000" points="1316.3842,-320.3292 1320.2098,-316.6648 1314.934,-317.1437 1316.3842,-320.3292"/>
+</g>
+<!-- strings -->
+<g id="node17" class="node">
+<title>strings</title>
+<g id="a_node17"><a xlink:href="https://godoc.org/strings" xlink:title="strings" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1495,-36C1495,-36 1465,-36 1465,-36 1459,-36 1453,-30 1453,-24 1453,-24 1453,-12 1453,-12 1453,-6 1459,0 1465,0 1465,0 1495,0 1495,0 1501,0 1507,-6 1507,-12 1507,-12 1507,-24 1507,-24 1507,-30 1501,-36 1495,-36"/>
+<text text-anchor="middle" x="1480" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strings</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config&#45;&gt;strings -->
+<g id="edge10" class="edge">
+<title>github.com/docker/cli/cli/config&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M1266.0054,-475.5835C1331.5889,-464.6593 1419.2633,-445.1657 1491,-412 1515.5289,-400.6597 1518.034,-391.7406 1540,-376 1576.5806,-349.7866 1602.2009,-357.9084 1623,-318 1650.6687,-264.9104 1539.5129,-100.7592 1496.4199,-40.4902"/>
+<polygon fill="#000000" stroke="#000000" points="1497.7743,-39.3759 1493.4377,-36.3334 1494.9304,-41.4161 1497.7743,-39.3759"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;encoding/base64 -->
+<g id="edge11" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;encoding/base64</title>
+<path fill="none" stroke="#000000" d="M847.5855,-378.5305C682.4277,-357.995 397.3991,-322.5549 274.8279,-307.3146"/>
+<polygon fill="#000000" stroke="#000000" points="275.0031,-305.573 269.8253,-306.6926 274.5711,-309.0462 275.0031,-305.573"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;encoding/json -->
+<g id="edge12" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M847.9491,-382.0576C774.096,-371.6871 679.9,-352.6298 602,-318 591.2618,-313.2264 313.7424,-107.0739 222.6591,-39.3267"/>
+<polygon fill="#000000" stroke="#000000" points="223.5208,-37.7867 218.4645,-36.2066 221.4319,-40.595 223.5208,-37.7867"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;fmt -->
+<g id="edge13" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1052.3679,-375.9376C1106.1723,-362.6836 1178.0334,-342.7178 1239,-318 1270.3273,-305.2989 1274.7303,-294.8422 1306,-282 1390.4286,-247.326 1415.6459,-250.4524 1503,-224 1637.5405,-183.2588 1678.2773,-190.8469 1805,-130 1857.9384,-104.5812 1913.2882,-63.9447 1944.8193,-39.2393"/>
+<polygon fill="#000000" stroke="#000000" points="1946.074,-40.4788 1948.9199,-36.0108 1943.9088,-37.7288 1946.074,-40.4788"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;github.com/docker/cli/cli/config/credentials -->
+<g id="edge14" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;github.com/docker/cli/cli/config/credentials</title>
+<path fill="none" stroke="#000000" d="M940.7648,-375.8759C913.535,-360.0759 874.0548,-337.1676 845.6068,-320.6607"/>
+<polygon fill="#000000" stroke="#000000" points="846.3423,-319.0643 841.1393,-318.0685 844.5857,-322.0916 846.3423,-319.0643"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;github.com/docker/cli/cli/config/types -->
+<g id="edge15" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;github.com/docker/cli/cli/config/types</title>
+<path fill="none" stroke="#000000" d="M975.6658,-375.8069C981.259,-351.9297 993.9088,-309.7997 1018,-282 1039.1613,-257.5811 1070.1883,-238.957 1096.5102,-226.2577"/>
+<polygon fill="#000000" stroke="#000000" points="1097.2618,-227.8381 1101.0284,-224.1132 1095.761,-224.6762 1097.2618,-227.8381"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;github.com/pkg/errors -->
+<g id="edge16" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;github.com/pkg/errors</title>
+<path fill="none" stroke="#000000" d="M1026.0251,-375.9647C1068.3593,-361.4247 1128.5104,-339.8634 1180,-318 1213.6651,-303.7052 1219.9709,-295.4052 1254,-282 1335.7517,-249.7952 1361.8945,-257.7994 1443,-224 1507.3434,-197.1859 1578.0071,-156.8428 1618.4792,-132.5995"/>
+<polygon fill="#000000" stroke="#000000" points="1619.3964,-134.0901 1622.7812,-130.015 1617.594,-131.0898 1619.3964,-134.0901"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;io -->
+<g id="edge17" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M995.2265,-375.9408C1026.1785,-352.399 1083.1956,-310.8353 1136,-282 1194.0319,-250.3101 1236.4794,-279.1199 1273,-224 1337.6707,-126.3937 1133.3242,-52.3646 1051.4109,-27.2658"/>
+<polygon fill="#000000" stroke="#000000" points="1051.4894,-25.4609 1046.1967,-25.6836 1050.4731,-28.8101 1051.4894,-25.4609"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;os -->
+<g id="edge19" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M847.6594,-376.8602C790.5991,-365.5592 723.4612,-347.243 668,-318 612.3013,-288.6318 593.7166,-278.3958 562,-224 527.3587,-164.5882 519.8684,-81.2996 518.3325,-41.2744"/>
+<polygon fill="#000000" stroke="#000000" points="520.0774,-41.0891 518.1597,-36.1509 516.5793,-41.2071 520.0774,-41.0891"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;path/filepath -->
+<g id="edge20" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M1053.6144,-375.9512C1119.6914,-361.1035 1215.0776,-339.1379 1298,-318 1303.6733,-316.5538 1309.61,-314.9859 1315.4908,-313.399"/>
+<polygon fill="#000000" stroke="#000000" points="1316.0435,-315.0624 1320.4106,-312.0637 1315.1267,-311.6846 1316.0435,-315.0624"/>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;strings -->
+<g id="edge21" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M1015.4995,-375.9692C1047.9599,-361.8316 1092.9141,-340.7881 1130,-318 1152.4184,-304.2246 1154.4863,-295.6191 1177,-282 1229.4026,-250.3002 1253.1077,-260.8843 1302,-224 1374.0841,-169.6198 1437.7085,-81.6836 1465.3568,-40.557"/>
+<polygon fill="#000000" stroke="#000000" points="1466.9108,-41.3811 1468.2344,-36.2517 1464.001,-39.4361 1466.9108,-41.3811"/>
+</g>
+<!-- io/ioutil -->
+<g id="node18" class="node">
+<title>io/ioutil</title>
+<g id="a_node18"><a xlink:href="https://godoc.org/io/ioutil" xlink:title="io/ioutil" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M523.5,-318C523.5,-318 488.5,-318 488.5,-318 482.5,-318 476.5,-312 476.5,-306 476.5,-306 476.5,-294 476.5,-294 476.5,-288 482.5,-282 488.5,-282 488.5,-282 523.5,-282 523.5,-282 529.5,-282 535.5,-288 535.5,-294 535.5,-294 535.5,-306 535.5,-306 535.5,-312 529.5,-318 523.5,-318"/>
+<text text-anchor="middle" x="506" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">io/ioutil</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config/configfile&#45;&gt;io/ioutil -->
+<g id="edge18" class="edge">
+<title>github.com/docker/cli/cli/config/configfile&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M847.7343,-376.2349C764.9442,-363.1676 654.1093,-343.2996 558,-318 552.298,-316.499 546.323,-314.6756 540.538,-312.7723"/>
+<polygon fill="#000000" stroke="#000000" points="541.0304,-311.0917 535.7334,-311.1602 539.917,-314.4099 541.0304,-311.0917"/>
+</g>
+<!-- github.com/docker/cli/cli/config/credentials&#45;&gt;github.com/docker/cli/cli/config/types -->
+<g id="edge22" class="edge">
+<title>github.com/docker/cli/cli/config/credentials&#45;&gt;github.com/docker/cli/cli/config/types</title>
+<path fill="none" stroke="#000000" d="M874.5782,-281.9871C932.6702,-265.7834 1017.7991,-242.0382 1077.3897,-225.4165"/>
+<polygon fill="#000000" stroke="#000000" points="1078.0077,-227.061 1082.3537,-224.0319 1077.0673,-223.6897 1078.0077,-227.061"/>
+</g>
+<!-- github.com/docker/cli/cli/config/credentials&#45;&gt;strings -->
+<g id="edge26" class="edge">
+<title>github.com/docker/cli/cli/config/credentials&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M849.5573,-281.9865C881.0458,-267.2893 925.9846,-245.5089 964,-224 989.752,-209.4297 993.3131,-200.7777 1020,-188 1093.5234,-152.797 1117.356,-157.7604 1194,-130 1287.0119,-96.3112 1395.41,-52.6325 1448.0494,-31.1342"/>
+<polygon fill="#000000" stroke="#000000" points="1449.001,-32.6358 1452.9671,-29.124 1447.6767,-29.396 1449.001,-32.6358"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client -->
+<g id="node19" class="node">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client</title>
+<g id="a_node19"><a xlink:href="https://godoc.org/github.com/docker/docker-credential-helpers/client" xlink:title="github.com/docker/docker&#45;credential&#45;helpers/client" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M875.5,-224C875.5,-224 604.5,-224 604.5,-224 598.5,-224 592.5,-218 592.5,-212 592.5,-212 592.5,-200 592.5,-200 592.5,-194 598.5,-188 604.5,-188 604.5,-188 875.5,-188 875.5,-188 881.5,-188 887.5,-194 887.5,-200 887.5,-200 887.5,-212 887.5,-212 887.5,-218 881.5,-224 875.5,-224"/>
+<text text-anchor="middle" x="740" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker&#45;credential&#45;helpers/client</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config/credentials&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/client -->
+<g id="edge23" class="edge">
+<title>github.com/docker/cli/cli/config/credentials&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/client</title>
+<path fill="none" stroke="#000000" d="M796.5033,-281.8759C785.0763,-266.531 768.6564,-244.4815 756.4596,-228.1029"/>
+<polygon fill="#000000" stroke="#000000" points="757.8452,-227.0335 753.4553,-224.0685 755.0381,-229.124 757.8452,-227.0335"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials -->
+<g id="node20" class="node">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials</title>
+<g id="a_node20"><a xlink:href="https://godoc.org/github.com/docker/docker-credential-helpers/credentials" xlink:title="github.com/docker/docker&#45;credential&#45;helpers/credentials" target="_blank">
+<path fill="#eee8aa" stroke="#eee8aa" d="M962.5,-130C962.5,-130 663.5,-130 663.5,-130 657.5,-130 651.5,-124 651.5,-118 651.5,-118 651.5,-106 651.5,-106 651.5,-100 657.5,-94 663.5,-94 663.5,-94 962.5,-94 962.5,-94 968.5,-94 974.5,-100 974.5,-106 974.5,-106 974.5,-118 974.5,-118 974.5,-124 968.5,-130 962.5,-130"/>
+<text text-anchor="middle" x="813" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker&#45;credential&#45;helpers/credentials</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config/credentials&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/credentials -->
+<g id="edge24" class="edge">
+<title>github.com/docker/cli/cli/config/credentials&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/credentials</title>
+<path fill="none" stroke="#000000" d="M682.8452,-282.0149C610.9277,-266.1393 541.95,-237.5471 578,-188 595.0194,-164.6085 659.3798,-144.7622 716.4501,-131.222"/>
+<polygon fill="#000000" stroke="#000000" points="717.1287,-132.8603 721.596,-130.0133 716.3283,-129.4531 717.1287,-132.8603"/>
+</g>
+<!-- os/exec -->
+<g id="node21" class="node">
+<title>os/exec</title>
+<g id="a_node21"><a xlink:href="https://godoc.org/os/exec" xlink:title="os/exec" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1167.5,-130C1167.5,-130 1134.5,-130 1134.5,-130 1128.5,-130 1122.5,-124 1122.5,-118 1122.5,-118 1122.5,-106 1122.5,-106 1122.5,-100 1128.5,-94 1134.5,-94 1134.5,-94 1167.5,-94 1167.5,-94 1173.5,-94 1179.5,-100 1179.5,-106 1179.5,-106 1179.5,-118 1179.5,-118 1179.5,-124 1173.5,-130 1167.5,-130"/>
+<text text-anchor="middle" x="1151" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">os/exec</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/cli/cli/config/credentials&#45;&gt;os/exec -->
+<g id="edge25" class="edge">
+<title>github.com/docker/cli/cli/config/credentials&#45;&gt;os/exec</title>
+<path fill="none" stroke="#000000" d="M832.4024,-281.8347C862.2838,-258.1769 917.424,-216.4865 969,-188 995.6062,-173.3049 1072.8374,-142.4417 1117.6832,-124.9021"/>
+<polygon fill="#000000" stroke="#000000" points="1118.4318,-126.4885 1122.4524,-123.0393 1117.1583,-123.2284 1118.4318,-126.4885"/>
+</g>
+<!-- github.com/docker/docker/pkg/homedir&#45;&gt;errors -->
+<g id="edge42" class="edge">
+<title>github.com/docker/docker/pkg/homedir&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M1476.566,-381.2739C1675.9979,-359.5008 2072.4064,-316.2228 2188.4022,-303.5589"/>
+<polygon fill="#000000" stroke="#000000" points="2188.9314,-305.2616 2193.7119,-302.9792 2188.5515,-301.7823 2188.9314,-305.2616"/>
+</g>
+<!-- github.com/docker/docker/pkg/homedir&#45;&gt;os -->
+<g id="edge43" class="edge">
+<title>github.com/docker/docker/pkg/homedir&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M1381.5627,-375.6808C1404.609,-353.7518 1435.827,-315.753 1420,-282 1364.5306,-163.7047 1314.4518,-144.6161 1194,-94 960.0587,4.3064 877.5008,-69.7646 626,-36 600.3612,-32.5579 571.4609,-27.6887 550.0122,-23.8824"/>
+<polygon fill="#000000" stroke="#000000" points="550.2325,-22.1441 545.0028,-22.9887 549.6177,-25.5897 550.2325,-22.1441"/>
+</g>
+<!-- github.com/docker/docker/pkg/homedir&#45;&gt;path/filepath -->
+<g id="edge45" class="edge">
+<title>github.com/docker/docker/pkg/homedir&#45;&gt;path/filepath</title>
+<path fill="none" stroke="#000000" d="M1360.5784,-375.8759C1361.0557,-360.9211 1361.7362,-339.5983 1362.2544,-323.3629"/>
+<polygon fill="#000000" stroke="#000000" points="1364.0129,-323.1218 1362.4233,-318.0685 1360.5147,-323.0101 1364.0129,-323.1218"/>
+</g>
+<!-- github.com/docker/docker/pkg/homedir&#45;&gt;strings -->
+<g id="edge46" class="edge">
+<title>github.com/docker/docker/pkg/homedir&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M1404.8867,-375.9352C1429.3442,-363.5353 1457.5068,-344.49 1472,-318 1522.168,-226.3051 1498.622,-94.096 1486.0842,-41.1775"/>
+<polygon fill="#000000" stroke="#000000" points="1487.7111,-40.4593 1484.8349,-36.0107 1484.3091,-41.282 1487.7111,-40.4593"/>
+</g>
+<!-- os/user -->
+<g id="node22" class="node">
+<title>os/user</title>
+<g id="a_node22"><a xlink:href="https://godoc.org/os/user" xlink:title="os/user" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1596.5,-318C1596.5,-318 1565.5,-318 1565.5,-318 1559.5,-318 1553.5,-312 1553.5,-306 1553.5,-306 1553.5,-294 1553.5,-294 1553.5,-288 1559.5,-282 1565.5,-282 1565.5,-282 1596.5,-282 1596.5,-282 1602.5,-282 1608.5,-288 1608.5,-294 1608.5,-294 1608.5,-306 1608.5,-306 1608.5,-312 1602.5,-318 1596.5,-318"/>
+<text text-anchor="middle" x="1581" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">os/user</text>
+</a>
+</g>
+</g>
+<!-- github.com/docker/docker/pkg/homedir&#45;&gt;os/user -->
+<g id="edge44" class="edge">
+<title>github.com/docker/docker/pkg/homedir&#45;&gt;os/user</title>
+<path fill="none" stroke="#000000" d="M1402.3495,-375.9871C1444.9313,-357.8754 1509.6649,-330.3416 1548.4022,-313.8651"/>
+<polygon fill="#000000" stroke="#000000" points="1549.5422,-315.282 1553.4583,-311.7145 1548.1723,-312.0612 1549.5422,-315.282"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;fmt -->
+<g id="edge81" class="edge">
+<title>github.com/pkg/errors&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1713.129,-93.9871C1780.4283,-74.1559 1886.0616,-43.0289 1938.798,-27.489"/>
+<polygon fill="#000000" stroke="#000000" points="1939.4319,-29.1267 1943.7333,-26.0347 1938.4425,-25.7694 1939.4319,-29.1267"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;io -->
+<g id="edge82" class="edge">
+<title>github.com/pkg/errors&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M1582.7503,-101.7165C1446.7594,-81.5219 1149.4983,-37.3789 1051.2413,-22.7878"/>
+<polygon fill="#000000" stroke="#000000" points="1051.3631,-21.0368 1046.1602,-22.0333 1050.8489,-24.4988 1051.3631,-21.0368"/>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;strings -->
+<g id="edge85" class="edge">
+<title>github.com/pkg/errors&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M1618.8367,-93.8759C1587.9801,-77.0124 1542.3072,-52.0516 1511.8726,-35.4187"/>
+<polygon fill="#000000" stroke="#000000" points="1512.3502,-33.6855 1507.1234,-32.8233 1510.6717,-36.7568 1512.3502,-33.6855"/>
+</g>
+<!-- path -->
+<g id="node38" class="node">
+<title>path</title>
+<g id="a_node38"><a xlink:href="https://godoc.org/path" xlink:title="path" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1624,-36C1624,-36 1594,-36 1594,-36 1588,-36 1582,-30 1582,-24 1582,-24 1582,-12 1582,-12 1582,-6 1588,0 1594,0 1594,0 1624,0 1624,0 1630,0 1636,-6 1636,-12 1636,-12 1636,-24 1636,-24 1636,-30 1630,-36 1624,-36"/>
+<text text-anchor="middle" x="1609" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">path</text>
+</a>
+</g>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;path -->
+<g id="edge83" class="edge">
+<title>github.com/pkg/errors&#45;&gt;path</title>
+<path fill="none" stroke="#000000" d="M1643.7092,-93.8759C1636.8087,-78.7911 1626.9443,-57.227 1619.4941,-40.9405"/>
+<polygon fill="#000000" stroke="#000000" points="1620.9368,-39.8874 1617.2654,-36.0685 1617.754,-41.3434 1620.9368,-39.8874"/>
+</g>
+<!-- runtime -->
+<g id="node39" class="node">
+<title>runtime</title>
+<g id="a_node39"><a xlink:href="https://godoc.org/runtime" xlink:title="runtime" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1712.5,-36C1712.5,-36 1677.5,-36 1677.5,-36 1671.5,-36 1665.5,-30 1665.5,-24 1665.5,-24 1665.5,-12 1665.5,-12 1665.5,-6 1671.5,0 1677.5,0 1677.5,0 1712.5,0 1712.5,0 1718.5,0 1724.5,-6 1724.5,-12 1724.5,-12 1724.5,-24 1724.5,-24 1724.5,-30 1718.5,-36 1712.5,-36"/>
+<text text-anchor="middle" x="1695" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">runtime</text>
+</a>
+</g>
+</g>
+<!-- github.com/pkg/errors&#45;&gt;runtime -->
+<g id="edge84" class="edge">
+<title>github.com/pkg/errors&#45;&gt;runtime</title>
+<path fill="none" stroke="#000000" d="M1660.2908,-93.8759C1667.1913,-78.7911 1677.0557,-57.227 1684.5059,-40.9405"/>
+<polygon fill="#000000" stroke="#000000" points="1686.246,-41.3434 1686.7346,-36.0685 1683.0632,-39.8874 1686.246,-41.3434"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;bytes -->
+<g id="edge27" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M699.695,-187.9234C676.7845,-175.2795 650.1209,-155.9875 637,-130 622.3372,-100.9586 638.4454,-64.0663 652.3779,-40.7356"/>
+<polygon fill="#000000" stroke="#000000" points="654.0027,-41.4337 655.1256,-36.2567 651.0193,-39.6035 654.0027,-41.4337"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;encoding/json -->
+<g id="edge28" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M687.6472,-187.9738C582.9444,-151.9222 348.9088,-71.3386 245.18,-35.6224"/>
+<polygon fill="#000000" stroke="#000000" points="245.6374,-33.9291 240.34,-33.9559 244.4979,-37.2385 245.6374,-33.9291"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;fmt -->
+<g id="edge29" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M887.6731,-196.1443C1154.8686,-178.1123 1694.8018,-140.6793 1735,-130 1812.5486,-109.3979 1895.6292,-63.7777 1939.5557,-37.56"/>
+<polygon fill="#000000" stroke="#000000" points="1940.5536,-39.0022 1943.9411,-34.9293 1938.7531,-36.0008 1940.5536,-39.0022"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;io -->
+<g id="edge31" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M830.8914,-187.9399C892.5304,-173.939 966.7036,-153.0798 989,-130 1012.1116,-106.0764 1017.8863,-66.3718 1019.0761,-41.2273"/>
+<polygon fill="#000000" stroke="#000000" points="1020.8298,-41.1546 1019.2649,-36.0936 1017.3322,-41.0259 1020.8298,-41.1546"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;os -->
+<g id="edge32" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M708.4095,-187.7767C684.5249,-173.3822 651.4436,-152.0975 625,-130 591.2531,-101.7995 556.9566,-63.9225 536.5846,-40.2407"/>
+<polygon fill="#000000" stroke="#000000" points="537.7728,-38.9377 533.1916,-36.2778 535.1142,-41.2141 537.7728,-38.9377"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;strings -->
+<g id="edge34" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M818.0386,-187.936C875.1239,-174.007 954.12,-153.2389 1022,-130 1061.2022,-116.579 1068.4023,-106.205 1108,-94 1230.8475,-56.1355 1382.4067,-31.8561 1447.6423,-22.4374"/>
+<polygon fill="#000000" stroke="#000000" points="1448.2359,-24.1202 1452.9369,-21.6783 1447.7391,-20.6557 1448.2359,-24.1202"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/credentials -->
+<g id="edge30" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;github.com/docker/docker&#45;credential&#45;helpers/credentials</title>
+<path fill="none" stroke="#000000" d="M754.0751,-187.8759C765.9919,-172.531 783.1154,-150.4815 795.835,-134.1029"/>
+<polygon fill="#000000" stroke="#000000" points="797.2834,-135.0909 798.9681,-130.0685 794.5191,-132.9442 797.2834,-135.0909"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;os/exec -->
+<g id="edge33" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/client&#45;&gt;os/exec</title>
+<path fill="none" stroke="#000000" d="M842.8954,-187.9971C915.3721,-174.4141 1014.3608,-154.0405 1100,-130 1105.7206,-128.3941 1111.7294,-126.4823 1117.5343,-124.5139"/>
+<polygon fill="#000000" stroke="#000000" points="1118.1953,-126.1371 1122.3516,-122.8526 1117.0542,-122.8283 1118.1953,-126.1371"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;bufio -->
+<g id="edge35" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;bufio</title>
+<path fill="none" stroke="#000000" d="M813,-93.8759C813,-78.9211 813,-57.5983 813,-41.3629"/>
+<polygon fill="#000000" stroke="#000000" points="814.7501,-41.0685 813,-36.0685 811.2501,-41.0685 814.7501,-41.0685"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;bytes -->
+<g id="edge36" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;bytes</title>
+<path fill="none" stroke="#000000" d="M785.0425,-93.8759C760.6149,-78.04 725.1726,-55.0636 699.6971,-38.5485"/>
+<polygon fill="#000000" stroke="#000000" points="700.5274,-37.0012 695.3799,-35.7497 698.6235,-39.9381 700.5274,-37.0012"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;encoding/json -->
+<g id="edge37" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M694.383,-93.9871C558.8459,-73.4047 343.1816,-40.6544 245.3082,-25.7915"/>
+<polygon fill="#000000" stroke="#000000" points="245.3749,-24.0317 240.1688,-25.0111 244.8494,-27.492 245.3749,-24.0317"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;fmt -->
+<g id="edge38" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M974.6242,-98.8803C1251.5666,-76.3996 1800.4046,-31.848 1938.7538,-20.6176"/>
+<polygon fill="#000000" stroke="#000000" points="1939.0437,-22.3499 1943.8857,-20.201 1938.7605,-18.8614 1939.0437,-22.3499"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;io -->
+<g id="edge39" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;io</title>
+<path fill="none" stroke="#000000" d="M852.7189,-93.8759C891.8813,-76.0056 950.9711,-49.0423 987.1527,-32.5322"/>
+<polygon fill="#000000" stroke="#000000" points="988.0615,-34.0412 991.8838,-30.3734 986.6085,-30.8571 988.0615,-34.0412"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;os -->
+<g id="edge40" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M756.4701,-93.9871C695.1682,-74.4536 599.4727,-43.9608 550.0082,-28.1992"/>
+<polygon fill="#000000" stroke="#000000" points="550.3741,-26.4792 545.0787,-26.6285 549.3114,-29.814 550.3741,-26.4792"/>
+</g>
+<!-- github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;strings -->
+<g id="edge41" class="edge">
+<title>github.com/docker/docker&#45;credential&#45;helpers/credentials&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M940.8151,-93.9871C1098.6257,-71.7469 1357.2455,-35.2997 1447.6149,-22.564"/>
+<polygon fill="#000000" stroke="#000000" points="1448.1062,-24.2622 1452.8131,-21.8314 1447.6177,-20.7964 1448.1062,-24.2622"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/authn -->
+<g id="node23" class="node">
+<title>github.com/google/go&#45;containerregistry/pkg/authn</title>
+<g id="a_node23"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/authn" xlink:title="github.com/google/go&#45;containerregistry/pkg/authn" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1303,-600C1303,-600 1037,-600 1037,-600 1031,-600 1025,-594 1025,-588 1025,-588 1025,-576 1025,-576 1025,-570 1031,-564 1037,-564 1037,-564 1303,-564 1303,-564 1309,-564 1315,-570 1315,-576 1315,-576 1315,-588 1315,-588 1315,-594 1309,-600 1303,-600"/>
+<text text-anchor="middle" x="1170" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go&#45;containerregistry/pkg/authn</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;encoding/json -->
+<g id="edge47" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M1024.9508,-574.5193C829.7314,-563.3542 493.2775,-539.9443 375,-506 213.8533,-459.7527 59,-467.6516 59,-300 59,-300 59,-300 59,-206 59,-134.2762 124.6313,-71.1736 164.3711,-39.539"/>
+<polygon fill="#000000" stroke="#000000" points="165.759,-40.6738 168.6075,-36.2074 163.5955,-37.9226 165.759,-40.6738"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;github.com/docker/cli/cli/config -->
+<g id="edge48" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;github.com/docker/cli/cli/config</title>
+<path fill="none" stroke="#000000" d="M1170,-563.8759C1170,-548.9211 1170,-527.5983 1170,-511.3629"/>
+<polygon fill="#000000" stroke="#000000" points="1171.7501,-511.0685 1170,-506.0685 1168.2501,-511.0685 1171.7501,-511.0685"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;github.com/docker/cli/cli/config/types -->
+<g id="edge49" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;github.com/docker/cli/cli/config/types</title>
+<path fill="none" stroke="#000000" d="M1024.8472,-570.6895C771.6299,-545.7577 291.601,-472.3501 462,-282 480.6886,-261.1233 838.8562,-230.1019 1029.6383,-214.9633"/>
+<polygon fill="#000000" stroke="#000000" points="1029.8273,-216.7039 1034.6735,-214.5644 1029.5508,-213.2148 1029.8273,-216.7039"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;os -->
+<g id="edge52" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;os</title>
+<path fill="none" stroke="#000000" d="M1024.6507,-570.094C867.0858,-556.2002 626.3053,-531.8169 538,-506 353.3099,-452.0042 250.3428,-480.9476 148,-318 53.2875,-167.2011 379.6896,-57.8331 485.7838,-26.8904"/>
+<polygon fill="#000000" stroke="#000000" points="486.351,-28.5482 490.6669,-25.4764 485.3774,-25.1863 486.351,-28.5482"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/logs -->
+<g id="node24" class="node">
+<title>github.com/google/go&#45;containerregistry/pkg/logs</title>
+<g id="a_node24"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/logs" xlink:title="github.com/google/go&#45;containerregistry/pkg/logs" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M1630.5,-506C1630.5,-506 1371.5,-506 1371.5,-506 1365.5,-506 1359.5,-500 1359.5,-494 1359.5,-494 1359.5,-482 1359.5,-482 1359.5,-476 1365.5,-470 1371.5,-470 1371.5,-470 1630.5,-470 1630.5,-470 1636.5,-470 1642.5,-476 1642.5,-482 1642.5,-482 1642.5,-494 1642.5,-494 1642.5,-500 1636.5,-506 1630.5,-506"/>
+<text text-anchor="middle" x="1501" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go&#45;containerregistry/pkg/logs</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;github.com/google/go&#45;containerregistry/pkg/logs -->
+<g id="edge50" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;github.com/google/go&#45;containerregistry/pkg/logs</title>
+<path fill="none" stroke="#000000" d="M1233.4285,-563.9871C1290.4862,-547.7834 1374.0994,-524.0382 1432.629,-507.4165"/>
+<polygon fill="#000000" stroke="#000000" points="1433.1729,-509.0813 1437.5046,-506.0319 1432.2167,-505.7145 1433.1729,-509.0813"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/name -->
+<g id="node25" class="node">
+<title>github.com/google/go&#45;containerregistry/pkg/name</title>
+<g id="a_node25"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/name" xlink:title="github.com/google/go&#45;containerregistry/pkg/name" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2106,-506C2106,-506 1840,-506 1840,-506 1834,-506 1828,-500 1828,-494 1828,-494 1828,-482 1828,-482 1828,-476 1834,-470 1840,-470 1840,-470 2106,-470 2106,-470 2112,-470 2118,-476 2118,-482 2118,-482 2118,-494 2118,-494 2118,-500 2112,-506 2106,-506"/>
+<text text-anchor="middle" x="1973" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go&#45;containerregistry/pkg/name</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;github.com/google/go&#45;containerregistry/pkg/name -->
+<g id="edge51" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/authn&#45;&gt;github.com/google/go&#45;containerregistry/pkg/name</title>
+<path fill="none" stroke="#000000" d="M1315.4172,-564.9773C1459.019,-548.1671 1677.6313,-522.5762 1822.8531,-505.5764"/>
+<polygon fill="#000000" stroke="#000000" points="1823.2253,-507.2948 1827.9879,-504.9753 1822.8183,-503.8186 1823.2253,-507.2948"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/logs&#45;&gt;io/ioutil -->
+<g id="edge59" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/logs&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M1359.3559,-475.5064C1193.7666,-460.2638 930.0058,-433.8275 833,-412 723.3831,-387.3348 599.1886,-338.8368 540.3782,-314.5625"/>
+<polygon fill="#000000" stroke="#000000" points="540.8642,-312.8698 535.575,-312.5738 539.5252,-316.1035 540.8642,-312.8698"/>
+</g>
+<!-- log -->
+<g id="node30" class="node">
+<title>log</title>
+<g id="a_node30"><a xlink:href="https://godoc.org/log" xlink:title="log" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1725,-412C1725,-412 1695,-412 1695,-412 1689,-412 1683,-406 1683,-400 1683,-400 1683,-388 1683,-388 1683,-382 1689,-376 1695,-376 1695,-376 1725,-376 1725,-376 1731,-376 1737,-382 1737,-388 1737,-388 1737,-400 1737,-400 1737,-406 1731,-412 1725,-412"/>
+<text text-anchor="middle" x="1710" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">log</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/logs&#45;&gt;log -->
+<g id="edge60" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/logs&#45;&gt;log</title>
+<path fill="none" stroke="#000000" d="M1541.2973,-469.8759C1581.1436,-451.9546 1641.3231,-424.8882 1678.003,-408.391"/>
+<polygon fill="#000000" stroke="#000000" points="1678.9553,-409.8816 1682.7975,-406.2346 1677.5196,-406.6896 1678.9553,-409.8816"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;fmt -->
+<g id="edge61" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M1958.49,-469.9227C1947.7181,-455.2755 1933.9131,-433.5925 1928,-412 1923.774,-396.5682 1925.0684,-391.7291 1928,-376 1938.7373,-318.3907 2012.2627,-187.6093 2023,-130 2025.9316,-114.2709 2027.7287,-109.2853 2023,-94 2016.7808,-73.8967 2003.2102,-54.3746 1991.4918,-40.1506"/>
+<polygon fill="#000000" stroke="#000000" points="1992.7993,-38.9863 1988.2434,-36.2831 1990.1192,-41.2374 1992.7993,-38.9863"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;strings -->
+<g id="edge65" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M1958.2351,-469.7202C1946.6186,-454.7857 1930.5239,-432.8163 1919,-412 1844.8842,-278.1204 1903.0477,-193.7487 1787,-94 1776.4275,-84.9124 1589.3534,-42.3984 1512.5268,-25.2242"/>
+<polygon fill="#000000" stroke="#000000" points="1512.451,-23.4142 1507.1897,-24.0322 1511.688,-26.83 1512.451,-23.4142"/>
+</g>
+<!-- net -->
+<g id="node31" class="node">
+<title>net</title>
+<g id="a_node31"><a xlink:href="https://godoc.org/net" xlink:title="net" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2183,-412C2183,-412 2153,-412 2153,-412 2147,-412 2141,-406 2141,-400 2141,-400 2141,-388 2141,-388 2141,-382 2147,-376 2153,-376 2153,-376 2183,-376 2183,-376 2189,-376 2195,-382 2195,-388 2195,-388 2195,-400 2195,-400 2195,-406 2189,-412 2183,-412"/>
+<text text-anchor="middle" x="2168" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">net</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;net -->
+<g id="edge62" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M2010.598,-469.8759C2046.9905,-452.3328 2101.5618,-426.0266 2135.9505,-409.4495"/>
+<polygon fill="#000000" stroke="#000000" points="2137.1292,-410.8241 2140.8733,-407.0765 2135.6093,-407.6713 2137.1292,-410.8241"/>
+</g>
+<!-- net/url -->
+<g id="node32" class="node">
+<title>net/url</title>
+<g id="a_node32"><a xlink:href="https://godoc.org/net/url" xlink:title="net/url" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2266,-412C2266,-412 2236,-412 2236,-412 2230,-412 2224,-406 2224,-400 2224,-400 2224,-388 2224,-388 2224,-382 2230,-376 2236,-376 2236,-376 2266,-376 2266,-376 2272,-376 2278,-382 2278,-388 2278,-388 2278,-400 2278,-400 2278,-406 2272,-412 2266,-412"/>
+<text text-anchor="middle" x="2251" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/url</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;net/url -->
+<g id="edge63" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M2034.6626,-469.868C2082.5982,-455.3586 2150.5476,-433.8829 2209,-412 2212.1204,-410.8318 2215.3474,-409.5624 2218.5621,-408.2565"/>
+<polygon fill="#000000" stroke="#000000" points="2219.6318,-409.7083 2223.5864,-406.1836 2218.2969,-406.4729 2219.6318,-409.7083"/>
+</g>
+<!-- regexp -->
+<g id="node33" class="node">
+<title>regexp</title>
+<g id="a_node33"><a xlink:href="https://godoc.org/regexp" xlink:title="regexp" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M1985,-412C1985,-412 1955,-412 1955,-412 1949,-412 1943,-406 1943,-400 1943,-400 1943,-388 1943,-388 1943,-382 1949,-376 1955,-376 1955,-376 1985,-376 1985,-376 1991,-376 1997,-382 1997,-388 1997,-388 1997,-400 1997,-400 1997,-406 1991,-412 1985,-412"/>
+<text text-anchor="middle" x="1970" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">regexp</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;regexp -->
+<g id="edge64" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;regexp</title>
+<path fill="none" stroke="#000000" d="M1972.4216,-469.8759C1971.9443,-454.9211 1971.2638,-433.5983 1970.7456,-417.3629"/>
+<polygon fill="#000000" stroke="#000000" points="1972.4853,-417.0101 1970.5767,-412.0685 1968.9871,-417.1218 1972.4853,-417.0101"/>
+</g>
+<!-- unicode/utf8 -->
+<g id="node34" class="node">
+<title>unicode/utf8</title>
+<g id="a_node34"><a xlink:href="https://godoc.org/unicode/utf8" xlink:title="unicode/utf8" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2099.5,-412C2099.5,-412 2038.5,-412 2038.5,-412 2032.5,-412 2026.5,-406 2026.5,-400 2026.5,-400 2026.5,-388 2026.5,-388 2026.5,-382 2032.5,-376 2038.5,-376 2038.5,-376 2099.5,-376 2099.5,-376 2105.5,-376 2111.5,-382 2111.5,-388 2111.5,-388 2111.5,-400 2111.5,-400 2111.5,-406 2105.5,-412 2099.5,-412"/>
+<text text-anchor="middle" x="2069" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">unicode/utf8</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;unicode/utf8 -->
+<g id="edge66" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/name&#45;&gt;unicode/utf8</title>
+<path fill="none" stroke="#000000" d="M1991.5098,-469.8759C2007.3139,-454.401 2030.0819,-432.1073 2046.8509,-415.6877"/>
+<polygon fill="#000000" stroke="#000000" points="2048.1988,-416.8171 2050.5471,-412.0685 2045.7501,-414.3163 2048.1988,-416.8171"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/internal/retry -->
+<g id="node26" class="node">
+<title>github.com/google/go&#45;containerregistry/pkg/internal/retry</title>
+<g id="a_node26"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/internal/retry" xlink:title="github.com/google/go&#45;containerregistry/pkg/internal/retry" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2744.5,-600C2744.5,-600 2437.5,-600 2437.5,-600 2431.5,-600 2425.5,-594 2425.5,-588 2425.5,-588 2425.5,-576 2425.5,-576 2425.5,-570 2431.5,-564 2437.5,-564 2437.5,-564 2744.5,-564 2744.5,-564 2750.5,-564 2756.5,-570 2756.5,-576 2756.5,-576 2756.5,-588 2756.5,-588 2756.5,-594 2750.5,-600 2744.5,-600"/>
+<text text-anchor="middle" x="2591" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go&#45;containerregistry/pkg/internal/retry</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/internal/retry&#45;&gt;context -->
+<g id="edge53" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/internal/retry&#45;&gt;context</title>
+<path fill="none" stroke="#000000" d="M2575.768,-563.8759C2562.8718,-548.531 2544.3408,-526.4815 2530.5758,-510.1029"/>
+<polygon fill="#000000" stroke="#000000" points="2531.7419,-508.7703 2527.1852,-506.0685 2529.0625,-511.0222 2531.7419,-508.7703"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/internal/retry&#45;&gt;fmt -->
+<g id="edge54" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/internal/retry&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2546.1332,-563.8924C2520.2926,-551.2872 2489.2249,-532.0465 2470,-506 2439.3971,-464.5384 2440,-445.5326 2440,-394 2440,-394 2440,-394 2440,-206 2440,-153.5763 2442.6557,-128.2881 2403,-94 2342.8493,-41.9909 2093.343,-24.26 2003.4565,-19.4922"/>
+<polygon fill="#000000" stroke="#000000" points="2003.3619,-17.735 1998.2776,-19.2226 2003.1798,-21.2303 2003.3619,-17.735"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/internal/retry/wait -->
+<g id="node27" class="node">
+<title>github.com/google/go&#45;containerregistry/pkg/internal/retry/wait</title>
+<g id="a_node27"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/internal/retry/wait" xlink:title="github.com/google/go&#45;containerregistry/pkg/internal/retry/wait" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2915,-506C2915,-506 2581,-506 2581,-506 2575,-506 2569,-500 2569,-494 2569,-494 2569,-482 2569,-482 2569,-476 2575,-470 2581,-470 2581,-470 2915,-470 2915,-470 2921,-470 2927,-476 2927,-482 2927,-482 2927,-494 2927,-494 2927,-500 2921,-506 2915,-506"/>
+<text text-anchor="middle" x="2748" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go&#45;containerregistry/pkg/internal/retry/wait</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/internal/retry&#45;&gt;github.com/google/go&#45;containerregistry/pkg/internal/retry/wait -->
+<g id="edge55" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/internal/retry&#45;&gt;github.com/google/go&#45;containerregistry/pkg/internal/retry/wait</title>
+<path fill="none" stroke="#000000" d="M2621.2712,-563.8759C2647.6605,-548.0759 2685.9222,-525.1676 2713.4922,-508.6607"/>
+<polygon fill="#000000" stroke="#000000" points="2714.4308,-510.1385 2717.8218,-506.0685 2712.6329,-507.1356 2714.4308,-510.1385"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/internal/retry/wait&#45;&gt;errors -->
+<g id="edge56" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/internal/retry/wait&#45;&gt;errors</title>
+<path fill="none" stroke="#000000" d="M2697.469,-469.9738C2590.0101,-431.6393 2341.4084,-342.954 2252.9554,-311.3997"/>
+<polygon fill="#000000" stroke="#000000" points="2253.4098,-309.7038 2248.1125,-309.672 2252.2338,-313.0003 2253.4098,-309.7038"/>
+</g>
+<!-- math/rand -->
+<g id="node28" class="node">
+<title>math/rand</title>
+<g id="a_node28"><a xlink:href="https://godoc.org/math/rand" xlink:title="math/rand" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2772,-412C2772,-412 2724,-412 2724,-412 2718,-412 2712,-406 2712,-400 2712,-400 2712,-388 2712,-388 2712,-382 2718,-376 2724,-376 2724,-376 2772,-376 2772,-376 2778,-376 2784,-382 2784,-388 2784,-388 2784,-400 2784,-400 2784,-406 2778,-412 2772,-412"/>
+<text text-anchor="middle" x="2748" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">math/rand</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/internal/retry/wait&#45;&gt;math/rand -->
+<g id="edge57" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/internal/retry/wait&#45;&gt;math/rand</title>
+<path fill="none" stroke="#000000" d="M2748,-469.8759C2748,-454.9211 2748,-433.5983 2748,-417.3629"/>
+<polygon fill="#000000" stroke="#000000" points="2749.7501,-417.0685 2748,-412.0685 2746.2501,-417.0685 2749.7501,-417.0685"/>
+</g>
+<!-- time -->
+<g id="node29" class="node">
+<title>time</title>
+<g id="a_node29"><a xlink:href="https://godoc.org/time" xlink:title="time" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M2921,-412C2921,-412 2891,-412 2891,-412 2885,-412 2879,-406 2879,-400 2879,-400 2879,-388 2879,-388 2879,-382 2885,-376 2891,-376 2891,-376 2921,-376 2921,-376 2927,-376 2933,-382 2933,-388 2933,-388 2933,-400 2933,-400 2933,-406 2927,-412 2921,-412"/>
+<text text-anchor="middle" x="2906" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">time</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/internal/retry/wait&#45;&gt;time -->
+<g id="edge58" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/internal/retry/wait&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2778.464,-469.8759C2806.0429,-453.4682 2846.5064,-429.3949 2874.4248,-412.7853"/>
+<polygon fill="#000000" stroke="#000000" points="2875.3916,-414.2464 2878.7939,-410.1859 2873.6021,-411.2385 2875.3916,-414.2464"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport -->
+<g id="node35" class="node">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport</title>
+<g id="a_node35"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport" xlink:title="github.com/google/go&#45;containerregistry/pkg/v1/remote/transport" target="_blank">
+<path fill="#afeeee" stroke="#afeeee" d="M2288,-694C2288,-694 1944,-694 1944,-694 1938,-694 1932,-688 1932,-682 1932,-682 1932,-670 1932,-670 1932,-664 1938,-658 1944,-658 1944,-658 2288,-658 2288,-658 2294,-658 2300,-664 2300,-670 2300,-670 2300,-682 2300,-682 2300,-688 2294,-694 2288,-694"/>
+<text text-anchor="middle" x="2116" y="-672.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go&#45;containerregistry/pkg/v1/remote/transport</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;encoding/base64 -->
+<g id="edge67" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;encoding/base64</title>
+<path fill="none" stroke="#000000" d="M1931.9625,-668.5155C1644.7457,-656.1342 1103.0357,-629.7791 912,-600 742.6532,-573.6018 695.3448,-573.9546 538,-506 419.5411,-454.8395 295.2762,-362.5606 242.3584,-321.1352"/>
+<polygon fill="#000000" stroke="#000000" points="243.3825,-319.7143 238.3691,-318.0027 241.2209,-322.4671 243.3825,-319.7143"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;encoding/json -->
+<g id="edge68" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;encoding/json</title>
+<path fill="none" stroke="#000000" d="M1931.9205,-669.2048C1417.1107,-648.9922 0,-584.5816 0,-488 0,-488 0,-488 0,-206 0,-124.3433 89.7319,-66.7993 147.1424,-38.3391"/>
+<polygon fill="#000000" stroke="#000000" points="148.1834,-39.778 151.9049,-36.0079 146.6446,-36.6343 148.1834,-39.778"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;fmt -->
+<g id="edge69" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;fmt</title>
+<path fill="none" stroke="#000000" d="M2189.4654,-657.8977C2267.8907,-633.6575 2381,-582.3435 2381,-488 2381,-488 2381,-488 2381,-206 2381,-123.0113 2099.9384,-48.5681 2003.2301,-25.4098"/>
+<polygon fill="#000000" stroke="#000000" points="2003.4882,-23.6724 1998.2188,-24.2162 2002.6772,-27.0771 2003.4882,-23.6724"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;strings -->
+<g id="edge79" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;strings</title>
+<path fill="none" stroke="#000000" d="M2051.971,-657.9892C1981.8298,-634.7862 1871.5349,-587.4217 1813,-506 1704.2154,-354.6813 1859.8518,-232.36 1735,-94 1719.7682,-77.1202 1577.1558,-41.2558 1512.0894,-25.5889"/>
+<polygon fill="#000000" stroke="#000000" points="1512.436,-23.8725 1507.1656,-24.4063 1511.6186,-27.2757 1512.436,-23.8725"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;io/ioutil -->
+<g id="edge74" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;io/ioutil</title>
+<path fill="none" stroke="#000000" d="M1931.874,-673.6681C1662.9766,-668.5124 1176.7194,-652.0769 1010,-600 808.4444,-537.0416 602.7197,-379.233 531.8324,-321.5489"/>
+<polygon fill="#000000" stroke="#000000" points="532.7377,-320.0288 527.7578,-318.2224 530.5243,-322.74 532.7377,-320.0288"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;github.com/google/go&#45;containerregistry/pkg/authn -->
+<g id="edge70" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;github.com/google/go&#45;containerregistry/pkg/authn</title>
+<path fill="none" stroke="#000000" d="M1934.721,-657.9871C1756.3941,-640.2675 1487.3067,-613.5294 1320.4603,-596.9506"/>
+<polygon fill="#000000" stroke="#000000" points="1320.21,-595.1672 1315.0614,-596.4141 1319.8639,-598.65 1320.21,-595.1672"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;github.com/google/go&#45;containerregistry/pkg/logs -->
+<g id="edge72" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;github.com/google/go&#45;containerregistry/pkg/logs</title>
+<path fill="none" stroke="#000000" d="M2003.1445,-657.9396C1932.1439,-645.1194 1839.1201,-625.6344 1759,-600 1679.4792,-574.5574 1591.0895,-533.1381 1541.1524,-508.434"/>
+<polygon fill="#000000" stroke="#000000" points="1541.6983,-506.7513 1536.4414,-506.0966 1540.1426,-509.8866 1541.6983,-506.7513"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;github.com/google/go&#45;containerregistry/pkg/name -->
+<g id="edge73" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;github.com/google/go&#45;containerregistry/pkg/name</title>
+<path fill="none" stroke="#000000" d="M2102.2886,-657.9738C2076.0903,-623.5313 2018.9753,-548.443 1990.2397,-510.6648"/>
+<polygon fill="#000000" stroke="#000000" points="1991.3,-509.168 1986.88,-506.2479 1988.5142,-511.287 1991.3,-509.168"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;github.com/google/go&#45;containerregistry/pkg/internal/retry -->
+<g id="edge71" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;github.com/google/go&#45;containerregistry/pkg/internal/retry</title>
+<path fill="none" stroke="#000000" d="M2207.0228,-657.9871C2289.5608,-641.6532 2410.8219,-617.6563 2494.9015,-601.0174"/>
+<polygon fill="#000000" stroke="#000000" points="2495.3162,-602.7194 2499.8813,-600.0319 2494.6366,-599.2859 2495.3162,-602.7194"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;time -->
+<g id="edge80" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;time</title>
+<path fill="none" stroke="#000000" d="M2300.3792,-662.575C2462.7573,-649.2977 2686.2105,-627.0731 2771,-600 2853.6167,-573.6206 2897.8516,-580.6478 2942,-506 2958.933,-477.369 2939.7507,-439.8373 2923.5517,-416.3178"/>
+<polygon fill="#000000" stroke="#000000" points="2924.884,-415.1705 2920.5711,-412.0945 2922.0245,-417.1887 2924.884,-415.1705"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;net -->
+<g id="edge75" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;net</title>
+<path fill="none" stroke="#000000" d="M2119.3319,-657.9306C2128.409,-608.7051 2153.5706,-472.252 2163.6966,-417.3379"/>
+<polygon fill="#000000" stroke="#000000" points="2165.4427,-417.5183 2164.6285,-412.2838 2162.0007,-416.8836 2165.4427,-417.5183"/>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;net/url -->
+<g id="edge78" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;net/url</title>
+<path fill="none" stroke="#000000" d="M2125.971,-657.7097C2134.2333,-642.401 2146.1711,-619.9239 2156,-600 2188.4333,-534.2549 2223.9276,-455.2061 2240.8839,-416.96"/>
+<polygon fill="#000000" stroke="#000000" points="2242.6293,-417.3406 2243.0537,-412.0601 2239.429,-415.9233 2242.6293,-417.3406"/>
+</g>
+<!-- net/http -->
+<g id="node36" class="node">
+<title>net/http</title>
+<g id="a_node36"><a xlink:href="https://godoc.org/net/http" xlink:title="net/http" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3061,-600C3061,-600 3027,-600 3027,-600 3021,-600 3015,-594 3015,-588 3015,-588 3015,-576 3015,-576 3015,-570 3021,-564 3027,-564 3027,-564 3061,-564 3061,-564 3067,-564 3073,-570 3073,-576 3073,-576 3073,-588 3073,-588 3073,-594 3067,-600 3061,-600"/>
+<text text-anchor="middle" x="3044" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;net/http -->
+<g id="edge76" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;net/http</title>
+<path fill="none" stroke="#000000" d="M2300.0852,-665.5289C2517.3945,-652.2055 2865.3751,-627.5852 2993,-600 2998.551,-598.8002 3004.3258,-597.1625 3009.909,-595.3609"/>
+<polygon fill="#000000" stroke="#000000" points="3010.7837,-596.9137 3014.9705,-593.6681 3009.6736,-593.5944 3010.7837,-596.9137"/>
+</g>
+<!-- net/http/httputil -->
+<g id="node37" class="node">
+<title>net/http/httputil</title>
+<g id="a_node37"><a xlink:href="https://godoc.org/net/http/httputil" xlink:title="net/http/httputil" target="_blank">
+<path fill="#98fb98" stroke="#98fb98" d="M3191.5,-600C3191.5,-600 3114.5,-600 3114.5,-600 3108.5,-600 3102.5,-594 3102.5,-588 3102.5,-588 3102.5,-576 3102.5,-576 3102.5,-570 3108.5,-564 3114.5,-564 3114.5,-564 3191.5,-564 3191.5,-564 3197.5,-564 3203.5,-570 3203.5,-576 3203.5,-576 3203.5,-588 3203.5,-588 3203.5,-594 3197.5,-600 3191.5,-600"/>
+<text text-anchor="middle" x="3153" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http/httputil</text>
+</a>
+</g>
+</g>
+<!-- github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;net/http/httputil -->
+<g id="edge77" class="edge">
+<title>github.com/google/go&#45;containerregistry/pkg/v1/remote/transport&#45;&gt;net/http/httputil</title>
+<path fill="none" stroke="#000000" d="M2300.0481,-672.1185C2496.5108,-665.5337 2816.2684,-647.79 3088,-600 3091.1029,-599.4543 3094.2717,-598.8244 3097.458,-598.1342"/>
+<polygon fill="#000000" stroke="#000000" points="3097.9804,-599.8104 3102.4728,-597.0032 3097.2103,-596.3962 3097.9804,-599.8104"/>
+</g>
+</g>
+</svg>
diff --git a/images/image-anatomy.dot.svg b/images/image-anatomy.dot.svg
new file mode 100644
index 0000000..d9fdaa5
--- /dev/null
+++ b/images/image-anatomy.dot.svg
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: %3 Pages: 1 -->
+<svg width="481pt" height="230pt"
+ viewBox="0.00 0.00 480.52 230.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 226)">
+<title>%3</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-226 476.5204,-226 476.5204,4 -4,4"/>
+<g id="clust1" class="cluster">
+<title>cluster_layer1</title>
+<polygon fill="none" stroke="#000000" points="362.5204,-115 362.5204,-214 464.5204,-214 464.5204,-115 362.5204,-115"/>
+<text text-anchor="middle" x="413.5204" y="-198.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar.gz</text>
+</g>
+<g id="clust2" class="cluster">
+<title>cluster_layer2</title>
+<polygon fill="none" stroke="#000000" points="362.5204,-8 362.5204,-107 464.5204,-107 464.5204,-8 362.5204,-8"/>
+<text text-anchor="middle" x="413.5204" y="-91.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar.gz</text>
+</g>
+<!-- tag -->
+<g id="node1" class="node">
+<title>tag</title>
+<ellipse fill="#000000" stroke="#000000" cx="11.0204" cy="-98" rx="3.5" ry="3.5"/>
+</g>
+<!-- manifest -->
+<g id="node2" class="node">
+<title>manifest</title>
+<polygon fill="none" stroke="#000000" points="141.5204,-116 83.5204,-116 83.5204,-80 147.5204,-80 147.5204,-110 141.5204,-116"/>
+<polyline fill="none" stroke="#000000" points="141.5204,-116 141.5204,-110 "/>
+<polyline fill="none" stroke="#000000" points="147.5204,-110 141.5204,-110 "/>
+<text text-anchor="middle" x="115.5204" y="-94.3" font-family="Times,serif" font-size="14.00" fill="#000000">manifest</text>
+</g>
+<!-- tag&#45;&gt;manifest -->
+<g id="edge1" class="edge">
+<title>tag:head&#45;&gt;manifest</title>
+<path fill="none" stroke="#000000" d="M14.9894,-98C24.6237,-98 50.1431,-98 73.0579,-98"/>
+<polygon fill="#000000" stroke="#000000" points="73.3366,-101.5001 83.3366,-98 73.3365,-94.5001 73.3366,-101.5001"/>
+<text text-anchor="middle" x="49.0204" y="-101.8" font-family="Times,serif" font-size="14.00" fill="#000000">digest</text>
+<text text-anchor="middle" x="8.5" y="-114.2722" font-family="Times,serif" font-size="14.00" fill="#000000">tag</text>
+</g>
+<!-- config -->
+<g id="node3" class="node">
+<title>config</title>
+<polygon fill="none" stroke="#000000" points="294.5204,-116 246.5204,-116 246.5204,-80 300.5204,-80 300.5204,-110 294.5204,-116"/>
+<polyline fill="none" stroke="#000000" points="294.5204,-116 294.5204,-110 "/>
+<polyline fill="none" stroke="#000000" points="300.5204,-110 294.5204,-110 "/>
+<text text-anchor="middle" x="273.5204" y="-94.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text>
+</g>
+<!-- manifest&#45;&gt;config -->
+<g id="edge2" class="edge">
+<title>manifest&#45;&gt;config</title>
+<path fill="none" stroke="#000000" d="M147.8754,-98C173.5398,-98 209.4367,-98 236.2927,-98"/>
+<polygon fill="#000000" stroke="#000000" points="236.4645,-101.5001 246.4645,-98 236.4645,-94.5001 236.4645,-101.5001"/>
+<text text-anchor="middle" x="194.5204" y="-101.8" font-family="Times,serif" font-size="14.00" fill="#000000">(image id)</text>
+</g>
+<!-- l1 -->
+<g id="node4" class="node">
+<title>l1</title>
+<polygon fill="none" stroke="#000000" points="444.5204,-171 441.5204,-175 420.5204,-175 417.5204,-171 382.5204,-171 382.5204,-135 444.5204,-135 444.5204,-171"/>
+<text text-anchor="middle" x="413.5204" y="-149.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar</text>
+</g>
+<!-- manifest&#45;&gt;l1 -->
+<g id="edge5" class="edge">
+<title>manifest&#45;&gt;l1</title>
+<path fill="none" stroke="#000000" d="M147.7297,-111.4179C153.5784,-113.5149 159.6776,-115.4853 165.5204,-117 228.1655,-133.2397 301.7402,-142.7616 352.393,-147.8895"/>
+<polygon fill="#000000" stroke="#000000" points="352.2296,-151.3903 362.5245,-148.8876 352.9159,-144.424 352.2296,-151.3903"/>
+<text text-anchor="middle" x="273.5204" y="-145.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text>
+</g>
+<!-- l2 -->
+<g id="node5" class="node">
+<title>l2</title>
+<polygon fill="none" stroke="#000000" points="444.5204,-64 441.5204,-68 420.5204,-68 417.5204,-64 382.5204,-64 382.5204,-28 444.5204,-28 444.5204,-64"/>
+<text text-anchor="middle" x="413.5204" y="-42.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar</text>
+</g>
+<!-- manifest&#45;&gt;l2 -->
+<g id="edge6" class="edge">
+<title>manifest&#45;&gt;l2</title>
+<path fill="none" stroke="#000000" d="M147.5251,-84.7188C172.6249,-74.9475 208.6871,-62.3115 241.5204,-56 277.7838,-49.0291 319.072,-46.456 352.1051,-45.6602"/>
+<polygon fill="#000000" stroke="#000000" points="352.592,-49.1516 362.5232,-45.4609 352.4581,-42.1529 352.592,-49.1516"/>
+<text text-anchor="middle" x="273.5204" y="-59.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text>
+</g>
+<!-- config&#45;&gt;l1 -->
+<g id="edge3" class="edge">
+<title>config&#45;&gt;l1</title>
+<path fill="none" stroke="#000000" d="M300.5542,-105.0907C316.4942,-109.5797 336.9243,-115.872 354.5204,-123 360.746,-125.5219 367.2045,-128.4562 373.4651,-131.4831"/>
+<polygon fill="#000000" stroke="#000000" points="371.9331,-134.6301 382.4458,-135.9469 375.0488,-128.3617 371.9331,-134.6301"/>
+<text text-anchor="middle" x="339.0204" y="-126.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text>
+</g>
+<!-- config&#45;&gt;l2 -->
+<g id="edge4" class="edge">
+<title>config&#45;&gt;l2</title>
+<path fill="none" stroke="#000000" d="M300.8462,-87.8504C321.3169,-80.247 349.6613,-69.7191 372.7994,-61.1249"/>
+<polygon fill="#000000" stroke="#000000" points="374.2033,-64.3372 382.3589,-57.5743 371.766,-57.7752 374.2033,-64.3372"/>
+<text text-anchor="middle" x="339.0204" y="-81.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text>
+</g>
+</g>
+</svg>
diff --git a/images/index-anatomy-strange.dot.svg b/images/index-anatomy-strange.dot.svg
new file mode 100644
index 0000000..f698139
--- /dev/null
+++ b/images/index-anatomy-strange.dot.svg
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: %3 Pages: 1 -->
+<svg width="307pt" height="188pt"
+ viewBox="0.00 0.00 307.41 188.47" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 184.4722)">
+<title>%3</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-184.4722 303.414,-184.4722 303.414,4 -4,4"/>
+<!-- tag -->
+<g id="node1" class="node">
+<title>tag</title>
+<ellipse fill="#000000" stroke="#000000" cx="25.914" cy="-99" rx="3.5" ry="3.5"/>
+</g>
+<!-- index -->
+<g id="node4" class="node">
+<title>index</title>
+<polygon fill="none" stroke="#000000" points="113.414,-117 65.414,-117 65.414,-81 119.414,-81 119.414,-111 113.414,-117"/>
+<polyline fill="none" stroke="#000000" points="113.414,-117 113.414,-111 "/>
+<polyline fill="none" stroke="#000000" points="119.414,-111 113.414,-111 "/>
+<text text-anchor="middle" x="92.414" y="-95.3" font-family="Times,serif" font-size="14.00" fill="#000000">index</text>
+</g>
+<!-- tag&#45;&gt;index -->
+<g id="edge1" class="edge">
+<title>tag:head&#45;&gt;index</title>
+<path fill="none" stroke="#000000" d="M29.4894,-99C34.6004,-99 44.5446,-99 55.0454,-99"/>
+<polygon fill="#000000" stroke="#000000" points="55.1441,-102.5001 65.1441,-99 55.144,-95.5001 55.1441,-102.5001"/>
+<text text-anchor="middle" x="23" y="-115.2722" font-family="Times,serif" font-size="14.00" fill="#000000">r124356</text>
+</g>
+<!-- tag2 -->
+<g id="node2" class="node">
+<title>tag2</title>
+<ellipse fill="#000000" stroke="#000000" cx="92.414" cy="-45" rx="3.5" ry="3.5"/>
+</g>
+<!-- index2 -->
+<g id="node5" class="node">
+<title>index2</title>
+<polygon fill="none" stroke="#000000" points="203.414,-63 155.414,-63 155.414,-27 209.414,-27 209.414,-57 203.414,-63"/>
+<polyline fill="none" stroke="#000000" points="203.414,-63 203.414,-57 "/>
+<polyline fill="none" stroke="#000000" points="209.414,-57 203.414,-57 "/>
+<text text-anchor="middle" x="182.414" y="-41.3" font-family="Times,serif" font-size="14.00" fill="#000000">index</text>
+</g>
+<!-- tag2&#45;&gt;index2 -->
+<g id="edge2" class="edge">
+<title>tag2:head&#45;&gt;index2</title>
+<path fill="none" stroke="#000000" d="M96.2812,-45C104.7802,-45 125.8583,-45 145.0347,-45"/>
+<polygon fill="#000000" stroke="#000000" points="145.1057,-48.5001 155.1057,-45 145.1056,-41.5001 145.1057,-48.5001"/>
+<text text-anchor="middle" x="89.7919" y="-61.2722" font-family="Times,serif" font-size="14.00" fill="#000000">stable&#45;release</text>
+</g>
+<!-- tag3 -->
+<g id="node3" class="node">
+<title>tag3</title>
+<ellipse fill="#000000" stroke="#000000" cx="92.414" cy="-153" rx="3.5" ry="3.5"/>
+</g>
+<!-- image -->
+<g id="node6" class="node">
+<title>image</title>
+<polygon fill="none" stroke="#000000" points="203.414,-171 155.414,-171 155.414,-135 209.414,-135 209.414,-165 203.414,-171"/>
+<polyline fill="none" stroke="#000000" points="203.414,-171 203.414,-165 "/>
+<polyline fill="none" stroke="#000000" points="209.414,-165 203.414,-165 "/>
+<text text-anchor="middle" x="182.414" y="-149.3" font-family="Times,serif" font-size="14.00" fill="#000000">image</text>
+</g>
+<!-- tag3&#45;&gt;image -->
+<g id="edge3" class="edge">
+<title>tag3:head&#45;&gt;image</title>
+<path fill="none" stroke="#000000" d="M96.2812,-153C104.7802,-153 125.8583,-153 145.0347,-153"/>
+<polygon fill="#000000" stroke="#000000" points="145.1057,-156.5001 155.1057,-153 145.1056,-149.5001 145.1057,-156.5001"/>
+<text text-anchor="middle" x="89.7919" y="-169.2722" font-family="Times,serif" font-size="14.00" fill="#000000">v1.0</text>
+</g>
+<!-- index&#45;&gt;index2 -->
+<g id="edge6" class="edge">
+<title>index&#45;&gt;index2</title>
+<path fill="none" stroke="#000000" d="M119.417,-82.7982C127.8826,-77.7188 137.3656,-72.029 146.353,-66.6366"/>
+<polygon fill="#000000" stroke="#000000" points="148.3449,-69.5232 155.1191,-61.377 144.7434,-63.5208 148.3449,-69.5232"/>
+</g>
+<!-- index&#45;&gt;image -->
+<g id="edge4" class="edge">
+<title>index&#45;&gt;image</title>
+<path fill="none" stroke="#000000" d="M119.417,-115.2018C127.8826,-120.2812 137.3656,-125.971 146.353,-131.3634"/>
+<polygon fill="#000000" stroke="#000000" points="144.7434,-134.4792 155.1191,-136.623 148.3449,-128.4768 144.7434,-134.4792"/>
+</g>
+<!-- xml -->
+<g id="node9" class="node">
+<title>xml</title>
+<ellipse fill="none" stroke="#000000" cx="182.414" cy="-99" rx="27" ry="18"/>
+<text text-anchor="middle" x="182.414" y="-95.3" font-family="Times,serif" font-size="14.00" fill="#000000">xml</text>
+</g>
+<!-- index&#45;&gt;xml -->
+<g id="edge5" class="edge">
+<title>index&#45;&gt;xml</title>
+<path fill="none" stroke="#000000" d="M119.417,-99C127.4417,-99 136.3806,-99 144.9449,-99"/>
+<polygon fill="#000000" stroke="#000000" points="145.1191,-102.5001 155.1191,-99 145.119,-95.5001 145.1191,-102.5001"/>
+</g>
+<!-- image2 -->
+<g id="node7" class="node">
+<title>image2</title>
+<polygon fill="none" stroke="#000000" points="293.414,-90 245.414,-90 245.414,-54 299.414,-54 299.414,-84 293.414,-90"/>
+<polyline fill="none" stroke="#000000" points="293.414,-90 293.414,-84 "/>
+<polyline fill="none" stroke="#000000" points="299.414,-84 293.414,-84 "/>
+<text text-anchor="middle" x="272.414" y="-68.3" font-family="Times,serif" font-size="14.00" fill="#000000">image</text>
+</g>
+<!-- index2&#45;&gt;image2 -->
+<g id="edge7" class="edge">
+<title>index2&#45;&gt;image2</title>
+<path fill="none" stroke="#000000" d="M209.417,-53.1009C217.6181,-55.5612 226.7739,-58.308 235.509,-60.9285"/>
+<polygon fill="#000000" stroke="#000000" points="234.535,-64.2904 245.1191,-63.8115 236.5465,-57.5856 234.535,-64.2904"/>
+</g>
+<!-- image3 -->
+<g id="node8" class="node">
+<title>image3</title>
+<polygon fill="none" stroke="#000000" points="293.414,-36 245.414,-36 245.414,0 299.414,0 299.414,-30 293.414,-36"/>
+<polyline fill="none" stroke="#000000" points="293.414,-36 293.414,-30 "/>
+<polyline fill="none" stroke="#000000" points="299.414,-30 293.414,-30 "/>
+<text text-anchor="middle" x="272.414" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">image</text>
+</g>
+<!-- index2&#45;&gt;image3 -->
+<g id="edge8" class="edge">
+<title>index2&#45;&gt;image3</title>
+<path fill="none" stroke="#000000" d="M209.417,-36.8991C217.6181,-34.4388 226.7739,-31.692 235.509,-29.0715"/>
+<polygon fill="#000000" stroke="#000000" points="236.5465,-32.4144 245.1191,-26.1885 234.535,-25.7096 236.5465,-32.4144"/>
+</g>
+</g>
+</svg>
diff --git a/images/index-anatomy.dot.svg b/images/index-anatomy.dot.svg
new file mode 100644
index 0000000..55e16a6
--- /dev/null
+++ b/images/index-anatomy.dot.svg
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: %3 Pages: 1 -->
+<svg width="209pt" height="143pt"
+ viewBox="0.00 0.00 208.91 143.25" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 139.2506)">
+<title>%3</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-139.2506 204.914,-139.2506 204.914,4 -4,4"/>
+<!-- tag -->
+<g id="node1" class="node">
+<title>tag</title>
+<ellipse fill="#000000" stroke="#000000" cx="17.414" cy="-67.6362" rx="3.5" ry="3.5"/>
+</g>
+<!-- index -->
+<g id="node4" class="node">
+<title>index</title>
+<polygon fill="none" stroke="#000000" points="104.914,-85.6362 56.914,-85.6362 56.914,-49.6362 110.914,-49.6362 110.914,-79.6362 104.914,-85.6362"/>
+<polyline fill="none" stroke="#000000" points="104.914,-85.6362 104.914,-79.6362 "/>
+<polyline fill="none" stroke="#000000" points="110.914,-79.6362 104.914,-79.6362 "/>
+<text text-anchor="middle" x="83.914" y="-63.9362" font-family="Times,serif" font-size="14.00" fill="#000000">index</text>
+</g>
+<!-- tag&#45;&gt;index -->
+<g id="edge1" class="edge">
+<title>tag:head&#45;&gt;index</title>
+<path fill="none" stroke="#000000" d="M20.9894,-67.6362C26.1004,-67.6362 36.0446,-67.6362 46.5454,-67.6362"/>
+<polygon fill="#000000" stroke="#000000" points="46.6441,-71.1363 56.6441,-67.6362 46.644,-64.1363 46.6441,-71.1363"/>
+<text text-anchor="middle" x="14.5" y="-83.9084" font-family="Times,serif" font-size="14.00" fill="#000000">latest</text>
+</g>
+<!-- tag2 -->
+<g id="node2" class="node">
+<title>tag2</title>
+<ellipse fill="#000000" stroke="#000000" cx="83.914" cy="-107.6362" rx="3.5" ry="3.5"/>
+</g>
+<!-- image -->
+<g id="node5" class="node">
+<title>image</title>
+<polygon fill="none" stroke="#000000" points="194.914,-118.6362 146.914,-118.6362 146.914,-82.6362 200.914,-82.6362 200.914,-112.6362 194.914,-118.6362"/>
+<polyline fill="none" stroke="#000000" points="194.914,-118.6362 194.914,-112.6362 "/>
+<polyline fill="none" stroke="#000000" points="200.914,-112.6362 194.914,-112.6362 "/>
+<text text-anchor="middle" x="173.914" y="-96.9362" font-family="Times,serif" font-size="14.00" fill="#000000">image</text>
+</g>
+<!-- tag2&#45;&gt;image -->
+<g id="edge2" class="edge">
+<title>tag2:head&#45;&gt;image</title>
+<path fill="none" stroke="#000000" d="M87.7812,-107.3354C96.2802,-106.6744 117.3583,-105.035 136.5347,-103.5435"/>
+<polygon fill="#000000" stroke="#000000" points="136.9072,-107.0251 146.6057,-102.7602 136.3644,-100.0462 136.9072,-107.0251"/>
+<text text-anchor="middle" x="82.8601" y="-124.0506" font-family="Times,serif" font-size="14.00" fill="#000000">amd64</text>
+</g>
+<!-- tag3 -->
+<g id="node3" class="node">
+<title>tag3</title>
+<ellipse fill="#000000" stroke="#000000" cx="83.914" cy="-27.6362" rx="3.5" ry="3.5"/>
+</g>
+<!-- image2 -->
+<g id="node6" class="node">
+<title>image2</title>
+<polygon fill="none" stroke="#000000" points="194.914,-58.6362 146.914,-58.6362 146.914,-22.6362 200.914,-22.6362 200.914,-52.6362 194.914,-58.6362"/>
+<polyline fill="none" stroke="#000000" points="194.914,-58.6362 194.914,-52.6362 "/>
+<polyline fill="none" stroke="#000000" points="200.914,-52.6362 194.914,-52.6362 "/>
+<text text-anchor="middle" x="173.914" y="-36.9362" font-family="Times,serif" font-size="14.00" fill="#000000">image</text>
+</g>
+<!-- tag3&#45;&gt;image2 -->
+<g id="edge3" class="edge">
+<title>tag3:head&#45;&gt;image2</title>
+<path fill="none" stroke="#000000" d="M87.7812,-28.1948C96.2802,-29.4224 117.3583,-32.467 136.5347,-35.2369"/>
+<polygon fill="#000000" stroke="#000000" points="136.208,-38.726 146.6057,-36.6916 137.2088,-31.7979 136.208,-38.726"/>
+<text text-anchor="middle" x="84.2138" y="-3.8" font-family="Times,serif" font-size="14.00" fill="#000000">ppc64le</text>
+</g>
+<!-- index&#45;&gt;image -->
+<g id="edge4" class="edge">
+<title>index&#45;&gt;image</title>
+<path fill="none" stroke="#000000" d="M110.917,-77.5373C119.1181,-80.5443 128.2739,-83.9015 137.009,-87.1043"/>
+<polygon fill="#000000" stroke="#000000" points="136.0254,-90.4715 146.6191,-90.628 138.4352,-83.8993 136.0254,-90.4715"/>
+</g>
+<!-- index&#45;&gt;image2 -->
+<g id="edge5" class="edge">
+<title>index&#45;&gt;image2</title>
+<path fill="none" stroke="#000000" d="M110.917,-59.5353C119.1181,-57.075 128.2739,-54.3282 137.009,-51.7077"/>
+<polygon fill="#000000" stroke="#000000" points="138.0465,-55.0506 146.6191,-48.8247 136.035,-48.3458 138.0465,-55.0506"/>
+</g>
+</g>
+</svg>
diff --git a/images/mutate.dot.svg b/images/mutate.dot.svg
new file mode 100644
index 0000000..e493588
--- /dev/null
+++ b/images/mutate.dot.svg
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: %3 Pages: 1 -->
+<svg width="510pt" height="413pt"
+ viewBox="0.00 0.00 510.00 413.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 409)">
+<title>%3</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-409 506,-409 506,4 -4,4"/>
+<g id="clust1" class="cluster">
+<title>cluster_source</title>
+<polygon fill="none" stroke="#000000" points="28,-322 28,-397 473,-397 473,-322 28,-322"/>
+<text text-anchor="middle" x="250.5" y="-381.8" font-family="Times,serif" font-size="14.00" fill="#000000">Sources</text>
+</g>
+<g id="clust2" class="cluster">
+<title>cluster_mutate</title>
+<polygon fill="none" stroke="#000000" points="8,-175 8,-250 479,-250 479,-175 8,-175"/>
+<text text-anchor="middle" x="243.5" y="-234.8" font-family="Times,serif" font-size="14.00" fill="#000000">mutate</text>
+</g>
+<g id="clust3" class="cluster">
+<title>cluster_sinks</title>
+<polygon fill="none" stroke="#000000" points="8,-8 8,-83 494,-83 494,-8 8,-8"/>
+<text text-anchor="middle" x="251" y="-15.8" font-family="Times,serif" font-size="14.00" fill="#000000">Sinks</text>
+</g>
+<!-- input -->
+<g id="node1" class="node">
+<title>input</title>
+<polygon fill="none" stroke="#000000" points="287,-294 219,-294 219,-258 287,-258 287,-294"/>
+<text text-anchor="middle" x="253" y="-272.3" font-family="Times,serif" font-size="14.00" fill="#000000">v1.Image</text>
+</g>
+<!-- mutateconfig -->
+<g id="node8" class="node">
+<title>mutateconfig</title>
+<ellipse fill="none" stroke="#000000" cx="436" cy="-201" rx="35.194" ry="18"/>
+<text text-anchor="middle" x="436" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">Config</text>
+</g>
+<!-- input&#45;&gt;mutateconfig -->
+<g id="edge6" class="edge">
+<title>input&#45;&gt;mutateconfig</title>
+<path fill="none" stroke="#000000" d="M287.1531,-272.1612C322.3405,-267.7595 374.4502,-259.863 392,-250 402.3783,-244.1674 411.5932,-234.9731 418.8743,-226.1167"/>
+<polygon fill="#000000" stroke="#000000" points="421.657,-228.2396 424.9952,-218.1844 416.1151,-223.9632 421.657,-228.2396"/>
+</g>
+<!-- mutatetime -->
+<g id="node9" class="node">
+<title>mutatetime</title>
+<ellipse fill="none" stroke="#000000" cx="353" cy="-201" rx="29.795" ry="18"/>
+<text text-anchor="middle" x="353" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">Time</text>
+</g>
+<!-- input&#45;&gt;mutatetime -->
+<g id="edge7" class="edge">
+<title>input&#45;&gt;mutatetime</title>
+<path fill="none" stroke="#000000" d="M287.2346,-264.8998C296.4336,-261.0133 306.0312,-256.0671 314,-250 322.4749,-243.5475 330.1749,-234.8575 336.452,-226.5875"/>
+<polygon fill="#000000" stroke="#000000" points="339.4615,-228.397 342.451,-218.2327 333.7754,-224.3142 339.4615,-228.397"/>
+</g>
+<!-- mutatemediatype -->
+<g id="node10" class="node">
+<title>mutatemediatype</title>
+<ellipse fill="none" stroke="#000000" cx="253" cy="-201" rx="51.9908" ry="18"/>
+<text text-anchor="middle" x="253" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">MediaType</text>
+</g>
+<!-- input&#45;&gt;mutatemediatype -->
+<g id="edge8" class="edge">
+<title>input&#45;&gt;mutatemediatype</title>
+<path fill="none" stroke="#000000" d="M253,-257.8446C253,-249.3401 253,-239.0076 253,-229.4964"/>
+<polygon fill="#000000" stroke="#000000" points="256.5001,-229.2481 253,-219.2482 249.5001,-229.2482 256.5001,-229.2481"/>
+</g>
+<!-- mutateappend -->
+<g id="node11" class="node">
+<title>mutateappend</title>
+<ellipse fill="none" stroke="#000000" cx="145" cy="-201" rx="38.1938" ry="18"/>
+<text text-anchor="middle" x="145" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">Append</text>
+</g>
+<!-- input&#45;&gt;mutateappend -->
+<g id="edge9" class="edge">
+<title>input&#45;&gt;mutateappend</title>
+<path fill="none" stroke="#000000" d="M218.8031,-264.276C209.7024,-260.4082 200.1455,-255.6275 192,-250 182.1987,-243.2286 172.8112,-234.167 165.0383,-225.6788"/>
+<polygon fill="#000000" stroke="#000000" points="167.6237,-223.3188 158.3882,-218.1268 162.3702,-227.9449 167.6237,-223.3188"/>
+</g>
+<!-- mutaterebase -->
+<g id="node12" class="node">
+<title>mutaterebase</title>
+<ellipse fill="none" stroke="#000000" cx="52" cy="-201" rx="36.2938" ry="18"/>
+<text text-anchor="middle" x="52" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">Rebase</text>
+</g>
+<!-- input&#45;&gt;mutaterebase -->
+<g id="edge10" class="edge">
+<title>input&#45;&gt;mutaterebase</title>
+<path fill="none" stroke="#000000" d="M218.8523,-272.8393C179.727,-268.7745 118.3864,-260.9153 98,-250 87.2892,-244.2652 77.6813,-235.0892 70.0511,-226.22"/>
+<polygon fill="#000000" stroke="#000000" points="72.6328,-223.8461 63.6249,-218.2686 67.1885,-228.2462 72.6328,-223.8461"/>
+</g>
+<!-- output -->
+<g id="node2" class="node">
+<title>output</title>
+<polygon fill="none" stroke="#000000" points="287,-147 219,-147 219,-111 287,-111 287,-147"/>
+<text text-anchor="middle" x="253" y="-125.3" font-family="Times,serif" font-size="14.00" fill="#000000">v1.Image</text>
+</g>
+<!-- remotesink -->
+<g id="node13" class="node">
+<title>remotesink</title>
+<ellipse fill="none" stroke="#000000" cx="450" cy="-57" rx="35.9954" ry="18"/>
+<text text-anchor="middle" x="450" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">remote</text>
+</g>
+<!-- output&#45;&gt;remotesink -->
+<g id="edge20" class="edge">
+<title>output&#45;&gt;remotesink</title>
+<path fill="none" stroke="#000000" d="M287.303,-121.0277C318.6434,-113.1846 365.9147,-99.9363 405,-83 409.5224,-81.0404 414.1637,-78.7332 418.6672,-76.3161"/>
+<polygon fill="#000000" stroke="#000000" points="420.6325,-79.2265 427.6357,-71.2763 417.2032,-73.1241 420.6325,-79.2265"/>
+</g>
+<!-- tarballsink -->
+<g id="node14" class="node">
+<title>tarballsink</title>
+<ellipse fill="none" stroke="#000000" cx="363" cy="-57" rx="33.2948" ry="18"/>
+<text text-anchor="middle" x="363" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">tarball</text>
+</g>
+<!-- output&#45;&gt;tarballsink -->
+<g id="edge19" class="edge">
+<title>output&#45;&gt;tarballsink</title>
+<path fill="none" stroke="#000000" d="M280.7576,-110.8314C296.6532,-100.427 316.6101,-87.3643 332.8841,-76.7122"/>
+<polygon fill="#000000" stroke="#000000" points="335.0468,-79.4798 341.497,-71.0747 331.2132,-73.6228 335.0468,-79.4798"/>
+</g>
+<!-- legacy/tarballsink -->
+<g id="node15" class="node">
+<title>legacy/tarballsink</title>
+<ellipse fill="none" stroke="#000000" cx="253" cy="-57" rx="58.4896" ry="18"/>
+<text text-anchor="middle" x="253" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">legacy/tarball</text>
+</g>
+<!-- output&#45;&gt;legacy/tarballsink -->
+<g id="edge16" class="edge">
+<title>output&#45;&gt;legacy/tarballsink</title>
+<path fill="none" stroke="#000000" d="M253,-110.8314C253,-103.131 253,-93.9743 253,-85.4166"/>
+<polygon fill="#000000" stroke="#000000" points="256.5001,-85.4132 253,-75.4133 249.5001,-85.4133 256.5001,-85.4132"/>
+</g>
+<!-- layoutsink -->
+<g id="node16" class="node">
+<title>layoutsink</title>
+<ellipse fill="none" stroke="#000000" cx="144" cy="-57" rx="32.4942" ry="18"/>
+<text text-anchor="middle" x="144" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">layout</text>
+</g>
+<!-- output&#45;&gt;layoutsink -->
+<g id="edge17" class="edge">
+<title>output&#45;&gt;layoutsink</title>
+<path fill="none" stroke="#000000" d="M225.4947,-110.8314C209.6373,-100.3567 189.7007,-87.1876 173.5158,-76.4967"/>
+<polygon fill="#000000" stroke="#000000" points="175.2329,-73.4363 164.9598,-70.845 171.3748,-79.2771 175.2329,-73.4363"/>
+</g>
+<!-- daemonsink -->
+<g id="node17" class="node">
+<title>daemonsink</title>
+<ellipse fill="none" stroke="#000000" cx="55" cy="-57" rx="38.9931" ry="18"/>
+<text text-anchor="middle" x="55" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">daemon</text>
+</g>
+<!-- output&#45;&gt;daemonsink -->
+<g id="edge18" class="edge">
+<title>output&#45;&gt;daemonsink</title>
+<path fill="none" stroke="#000000" d="M218.9614,-120.6358C188.1089,-112.5708 141.6852,-99.2189 103,-83 98.2065,-80.9903 93.2639,-78.6484 88.456,-76.2089"/>
+<polygon fill="#000000" stroke="#000000" points="89.9289,-73.0288 79.451,-71.4589 86.663,-79.2202 89.9289,-73.0288"/>
+</g>
+<!-- remotesource -->
+<g id="node3" class="node">
+<title>remotesource</title>
+<ellipse fill="none" stroke="#000000" cx="429" cy="-348" rx="35.9954" ry="18"/>
+<text text-anchor="middle" x="429" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">remote</text>
+</g>
+<!-- remotesource&#45;&gt;input -->
+<g id="edge5" class="edge">
+<title>remotesource&#45;&gt;input</title>
+<path fill="none" stroke="#000000" d="M406.5444,-333.9282C399.4756,-329.7994 391.5425,-325.4665 384,-322 355.7704,-309.0257 322.7595,-297.4554 296.9378,-289.1642"/>
+<polygon fill="#000000" stroke="#000000" points="297.6528,-285.7195 287.0628,-286.0401 295.5414,-292.3935 297.6528,-285.7195"/>
+</g>
+<!-- tarballsource -->
+<g id="node4" class="node">
+<title>tarballsource</title>
+<ellipse fill="none" stroke="#000000" cx="342" cy="-348" rx="33.2948" ry="18"/>
+<text text-anchor="middle" x="342" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">tarball</text>
+</g>
+<!-- tarballsource&#45;&gt;input -->
+<g id="edge4" class="edge">
+<title>tarballsource&#45;&gt;input</title>
+<path fill="none" stroke="#000000" d="M323.1254,-332.7307C311.5473,-323.3641 296.4585,-311.1575 283.2447,-300.4676"/>
+<polygon fill="#000000" stroke="#000000" points="285.2835,-297.6151 275.3077,-294.0467 280.8809,-303.0573 285.2835,-297.6151"/>
+</g>
+<!-- randomsource -->
+<g id="node5" class="node">
+<title>randomsource</title>
+<ellipse fill="none" stroke="#000000" cx="253" cy="-348" rx="38.1938" ry="18"/>
+<text text-anchor="middle" x="253" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">random</text>
+</g>
+<!-- randomsource&#45;&gt;input -->
+<g id="edge1" class="edge">
+<title>randomsource&#45;&gt;input</title>
+<path fill="none" stroke="#000000" d="M253,-329.8314C253,-322.131 253,-312.9743 253,-304.4166"/>
+<polygon fill="#000000" stroke="#000000" points="256.5001,-304.4132 253,-294.4133 249.5001,-304.4133 256.5001,-304.4132"/>
+</g>
+<!-- layoutsource -->
+<g id="node6" class="node">
+<title>layoutsource</title>
+<ellipse fill="none" stroke="#000000" cx="164" cy="-348" rx="32.4942" ry="18"/>
+<text text-anchor="middle" x="164" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">layout</text>
+</g>
+<!-- layoutsource&#45;&gt;input -->
+<g id="edge2" class="edge">
+<title>layoutsource&#45;&gt;input</title>
+<path fill="none" stroke="#000000" d="M182.4409,-333.0816C193.9939,-323.7353 209.165,-311.462 222.4783,-300.6917"/>
+<polygon fill="#000000" stroke="#000000" points="224.9055,-303.2301 230.4786,-294.2195 220.5028,-297.788 224.9055,-303.2301"/>
+</g>
+<!-- daemonsource -->
+<g id="node7" class="node">
+<title>daemonsource</title>
+<ellipse fill="none" stroke="#000000" cx="75" cy="-348" rx="38.9931" ry="18"/>
+<text text-anchor="middle" x="75" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">daemon</text>
+</g>
+<!-- daemonsource&#45;&gt;input -->
+<g id="edge3" class="edge">
+<title>daemonsource&#45;&gt;input</title>
+<path fill="none" stroke="#000000" d="M99.5417,-333.7511C106.9591,-329.7065 115.2057,-325.4645 123,-322 151.0042,-309.5525 183.5065,-298.0518 209.0065,-289.6634"/>
+<polygon fill="#000000" stroke="#000000" points="210.3357,-292.9117 218.7644,-286.4925 208.1722,-286.2544 210.3357,-292.9117"/>
+</g>
+<!-- mutateconfig&#45;&gt;output -->
+<g id="edge11" class="edge">
+<title>mutateconfig&#45;&gt;output</title>
+<path fill="none" stroke="#000000" d="M414.1129,-186.8193C407.2039,-182.686 399.4313,-178.3781 392,-175 361.1889,-160.9941 324.8265,-149.1138 297.0223,-140.9367"/>
+<polygon fill="#000000" stroke="#000000" points="297.7707,-137.5098 287.192,-138.0946 295.8264,-144.2344 297.7707,-137.5098"/>
+</g>
+<!-- mutatetime&#45;&gt;output -->
+<g id="edge12" class="edge">
+<title>mutatetime&#45;&gt;output</title>
+<path fill="none" stroke="#000000" d="M333.719,-187.1177C320.4063,-177.5325 302.3499,-164.5319 286.6904,-153.2571"/>
+<polygon fill="#000000" stroke="#000000" points="288.4431,-150.2062 278.2827,-147.2035 284.353,-155.887 288.4431,-150.2062"/>
+</g>
+<!-- mutatemediatype&#45;&gt;output -->
+<g id="edge13" class="edge">
+<title>mutatemediatype&#45;&gt;output</title>
+<path fill="none" stroke="#000000" d="M253,-182.8314C253,-175.131 253,-165.9743 253,-157.4166"/>
+<polygon fill="#000000" stroke="#000000" points="256.5001,-157.4132 253,-147.4133 249.5001,-157.4133 256.5001,-157.4132"/>
+</g>
+<!-- mutateappend&#45;&gt;output -->
+<g id="edge14" class="edge">
+<title>mutateappend&#45;&gt;output</title>
+<path fill="none" stroke="#000000" d="M167.116,-186.256C181.6204,-176.5864 200.9061,-163.7292 217.5271,-152.6486"/>
+<polygon fill="#000000" stroke="#000000" points="219.5574,-155.5016 225.9365,-147.0423 215.6745,-149.6772 219.5574,-155.5016"/>
+</g>
+<!-- mutaterebase&#45;&gt;output -->
+<g id="edge15" class="edge">
+<title>mutaterebase&#45;&gt;output</title>
+<path fill="none" stroke="#000000" d="M74.8826,-186.7207C82.1056,-182.5835 90.2314,-178.298 98,-175 134.3708,-159.5595 177.6081,-147.1912 209.1638,-139.185"/>
+<polygon fill="#000000" stroke="#000000" points="210.118,-142.5543 218.9752,-136.7405 208.4256,-135.7619 210.118,-142.5543"/>
+</g>
+</g>
+</svg>
diff --git a/images/ociimage.gv b/images/ociimage.gv
new file mode 100644
index 0000000..5fbe947
--- /dev/null
+++ b/images/ociimage.gv
@@ -0,0 +1,97 @@
+digraph ociimage {
+ rankdir=LR;
+ node [shape=box];
+ edge [splines=polyline];
+ lrank [style=invisible][color=white];
+
+ "manifest A"[label=<<table border="0">
+ <tr><td align="center">image manifest (platform A)</td></tr>
+ <tr><td align="center"></td></tr>
+ <tr><td align="left">- schema version</td></tr>
+ <tr><td align="left">- media type</td></tr>
+ <tr><td align="left">- config : descriptor</td></tr>
+ <tr><td align="left">- layers : array of descriptors</td></tr>
+ <tr><td align="left">- (annotations)</td></tr>
+ </table>>];
+
+ "image index"[label=<<table border="0">
+ <tr><td align="center">image index</td></tr>
+ <tr><td></td></tr>
+ <tr><td align="left">- schema version</td></tr>
+ <tr><td align="left">- media type</td></tr>
+ <tr><td align="left">- manifests : array of descriptors</td></tr>
+ <tr><td align="left">- (annotations)</td></tr>
+ </table>>];
+
+ // references
+ edge [color=red][style=dashed];
+ client [style=invisible][color=white];
+ client -> "image index"[label="image reference"];
+ client -> "manifest A"[label="image reference"];
+
+ // descriptors
+ edge [color=brown][style=solid];
+ "image index" -> "manifest A";
+ "image index" -> "image manifest (platform B)";
+ "configuration"[label=<<table border="0">
+ <tr><td align="center">configuration</td></tr>
+ <tr><td></td></tr>
+ <tr><td align="left">- rootfs/diff_ids : array of layer ids</td></tr>
+ <tr><td align="left">- container config</td></tr>
+ <tr><td align="left">- history</td></tr>
+ </table>>];
+ "manifest A" -> "configuration";
+ "layer 0"[label=<<table border="0">
+ <tr><td align="center">layer</td></tr>
+ <tr><td></td></tr>
+ <tr><td align="left">file system additions, overwrites, and deletions</td></tr>
+ </table>>];
+ "layer 1"[label=layer];
+ "layer 2"[label=layer];
+ "manifest A" -> "layer 0"[label=0];
+ "manifest A" -> "layer 1"[label=1];
+ "manifest A" -> "layer 2"[label=2];
+
+ // ids
+ edge [color=blue][style=dotted];
+ "client" -> "configuration"[label="image id"];
+ "configuration" -> "layer 0"[label=0];
+ "configuration" -> "layer 1"[label=1];
+ "configuration" -> "layer 2"[label=2];
+
+ // key
+ subgraph cluster {
+ k1 [label="Key:"][peripheries="0"];
+ node [style=invisible][color=white];
+ k2;
+ k3;
+ k4;
+ node [style=solid][color=black];
+ k1 -> k2[color=red][style=dashed][label=<<table border="0">
+ <tr><td align="center">image reference</td></tr>
+ <tr><td></td></tr>
+ <tr><td align="left">- hostname</td></tr>
+ <tr><td align="left">- path</td></tr>
+ <tr><td align="left">- (tag)</td></tr>
+ <tr><td align="left">- (SHA-256 digest of compressed content)</td></tr>
+ </table>>];
+ k2 -> k3[color=brown][style=solid][label=<<table border="0">
+ <tr><td align="center">descriptor</td></tr>
+ <tr><td></td></tr>
+ <tr><td align="left">targets content with the following properties:</td></tr>
+ <tr><td align="left">- media type</td></tr>
+ <tr><td align="left">- SHA-256 digest of compressed content</td></tr>
+ <tr><td align="left">- size</td></tr>
+ <tr><td align="left">- (urls)</td></tr>
+ <tr><td align="left">- (annotations)</td></tr>
+ </table>>];
+ k3 -> k4[color=blue][style=dotted][label=<<table border="0">
+ <tr><td align="center">id</td></tr>
+ <tr><td></td></tr>
+ <tr><td align="left">- SHA-256 digest of uncompressed content</td></tr>
+ </table>>];
+ }
+
+ { rank=same; lrank -> "layer 2" -> "layer 1" -> "layer 0" [style=invis] }
+ { rank=same; "manifest A", "image manifest (platform B)" }
+} \ No newline at end of file
diff --git a/images/ociimage.jpeg b/images/ociimage.jpeg
new file mode 100644
index 0000000..b1e0ca5
--- /dev/null
+++ b/images/ociimage.jpeg
Binary files differ
diff --git a/images/remote.dot.svg b/images/remote.dot.svg
new file mode 100644
index 0000000..a4b5fae
--- /dev/null
+++ b/images/remote.dot.svg
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: %3 Pages: 1 -->
+<svg width="490pt" height="368pt"
+ viewBox="0.00 0.00 489.55 368.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 364)">
+<title>%3</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-364 485.5499,-364 485.5499,4 -4,4"/>
+<g id="clust1" class="cluster">
+<title>cluster_registry</title>
+<polygon fill="none" stroke="#000000" points="0,-8 0,-352 481.5499,-352 481.5499,-8 0,-8"/>
+<text text-anchor="middle" x="240.7749" y="-336.8" font-family="Times,serif" font-size="14.00" fill="#000000">registry</text>
+</g>
+<g id="clust2" class="cluster">
+<title>cluster_tags</title>
+<polygon fill="none" stroke="#000000" points="8,-24 8,-153 103,-153 103,-24 8,-24"/>
+<text text-anchor="middle" x="55.5" y="-137.8" font-family="Times,serif" font-size="14.00" fill="#000000">/v2/.../tags/list</text>
+</g>
+<g id="clust3" class="cluster">
+<title>cluster_manifests</title>
+<polygon fill="none" stroke="#000000" points="123,-16 123,-321 314,-321 314,-16 123,-16"/>
+<text text-anchor="middle" x="218.5" y="-305.8" font-family="Times,serif" font-size="14.00" fill="#000000">/v2/.../manifests/&lt;ref&gt;</text>
+</g>
+<g id="clust4" class="cluster">
+<title>cluster_manifest</title>
+<polygon fill="none" stroke="#000000" points="236,-24 236,-153 306,-153 306,-24 236,-24"/>
+<text text-anchor="middle" x="271" y="-137.8" font-family="Times,serif" font-size="14.00" fill="#000000">manifest</text>
+</g>
+<g id="clust5" class="cluster">
+<title>cluster_manifest2</title>
+<polygon fill="none" stroke="#000000" points="236,-161 236,-290 306,-290 306,-161 236,-161"/>
+<text text-anchor="middle" x="271" y="-274.8" font-family="Times,serif" font-size="14.00" fill="#000000">manifest</text>
+</g>
+<g id="clust6" class="cluster">
+<title>cluster_index</title>
+<polygon fill="none" stroke="#000000" points="131,-78 131,-153 216,-153 216,-78 131,-78"/>
+<text text-anchor="middle" x="173.5" y="-137.8" font-family="Times,serif" font-size="14.00" fill="#000000">index</text>
+</g>
+<g id="clust7" class="cluster">
+<title>cluster_blobs</title>
+<polygon fill="none" stroke="#000000" points="334,-20 334,-311 473.5499,-311 473.5499,-20 334,-20"/>
+<text text-anchor="middle" x="403.7749" y="-295.8" font-family="Times,serif" font-size="14.00" fill="#000000">/v2/.../blobs/&lt;sha256&gt;</text>
+</g>
+<!-- tag -->
+<g id="node1" class="node">
+<title>tag</title>
+<polygon fill="none" stroke="#000000" points="82,-68 28,-68 28,-32 82,-32 82,-68"/>
+<text text-anchor="middle" x="55" y="-46.3" font-family="Times,serif" font-size="14.00" fill="#000000">tag</text>
+</g>
+<!-- mconfig -->
+<g id="node3" class="node">
+<title>mconfig</title>
+<polygon fill="none" stroke="#000000" points="298,-68 244,-68 244,-32 298,-32 298,-68"/>
+<text text-anchor="middle" x="271" y="-46.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text>
+</g>
+<!-- tag&#45;&gt;mconfig -->
+<g id="edge9" class="edge">
+<title>tag&#45;&gt;mconfig</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M82.3565,-50C120.7403,-50 190.6983,-50 233.7878,-50"/>
+<polygon fill="#000000" stroke="#000000" points="226.0002,-53.5005 236,-50 225.9998,-46.5005 226.0002,-53.5005"/>
+</g>
+<!-- tag2 -->
+<g id="node2" class="node">
+<title>tag2</title>
+<polygon fill="none" stroke="#000000" points="82,-122 28,-122 28,-86 82,-86 82,-122"/>
+<text text-anchor="middle" x="55" y="-100.3" font-family="Times,serif" font-size="14.00" fill="#000000">tag</text>
+</g>
+<!-- imanifest -->
+<g id="node7" class="node">
+<title>imanifest</title>
+<polygon fill="none" stroke="#000000" points="208,-122 139,-122 139,-86 208,-86 208,-122"/>
+<text text-anchor="middle" x="173.5" y="-100.3" font-family="Times,serif" font-size="14.00" fill="#000000">manifests</text>
+</g>
+<!-- tag2&#45;&gt;imanifest -->
+<g id="edge10" class="edge">
+<title>tag2&#45;&gt;imanifest</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M82.1863,-104C95.9752,-104 113.0796,-104 128.744,-104"/>
+<polygon fill="#000000" stroke="#000000" points="121.0002,-107.5004 131,-104 120.9998,-100.5004 121.0002,-107.5004"/>
+</g>
+<!-- bconfig -->
+<g id="node8" class="node">
+<title>bconfig</title>
+<polygon fill="none" stroke="#000000" points="441.3261,-46 422.3005,-64 384.2493,-64 365.2238,-46 384.2493,-28 422.3005,-28 441.3261,-46"/>
+<text text-anchor="middle" x="403.2749" y="-42.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text>
+</g>
+<!-- mconfig&#45;&gt;bconfig -->
+<g id="edge7" class="edge">
+<title>mconfig&#45;&gt;bconfig</title>
+<path fill="none" stroke="#000000" d="M298.087,-49.1809C314.7449,-48.6772 336.552,-48.0177 355.9889,-47.4299"/>
+<polygon fill="#000000" stroke="#000000" points="356.3057,-50.922 366.1953,-47.1213 356.0941,-43.9252 356.3057,-50.922"/>
+</g>
+<!-- layers -->
+<g id="node4" class="node">
+<title>layers</title>
+<polygon fill="none" stroke="#000000" points="298,-122 244,-122 244,-86 298,-86 298,-122"/>
+<text text-anchor="middle" x="271" y="-100.3" font-family="Times,serif" font-size="14.00" fill="#000000">layers</text>
+</g>
+<!-- l1 -->
+<g id="node10" class="node">
+<title>l1</title>
+<polygon fill="none" stroke="#000000" points="430.2749,-118 427.2749,-122 406.2749,-122 403.2749,-118 376.2749,-118 376.2749,-82 430.2749,-82 430.2749,-118"/>
+<text text-anchor="middle" x="403.2749" y="-96.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer</text>
+</g>
+<!-- layers&#45;&gt;l1 -->
+<g id="edge3" class="edge">
+<title>layers&#45;&gt;l1</title>
+<path fill="none" stroke="#000000" d="M298.087,-103.1809C317.7033,-102.5877 344.4602,-101.7786 366.0782,-101.1248"/>
+<polygon fill="#000000" stroke="#000000" points="366.3345,-104.6188 376.2242,-100.818 366.1229,-97.622 366.3345,-104.6188"/>
+</g>
+<!-- l2 -->
+<g id="node11" class="node">
+<title>l2</title>
+<polygon fill="none" stroke="#000000" points="430.2749,-172 427.2749,-176 406.2749,-176 403.2749,-172 376.2749,-172 376.2749,-136 430.2749,-136 430.2749,-172"/>
+<text text-anchor="middle" x="403.2749" y="-150.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer</text>
+</g>
+<!-- layers&#45;&gt;l2 -->
+<g id="edge4" class="edge">
+<title>layers&#45;&gt;l2</title>
+<path fill="none" stroke="#000000" d="M298.087,-114.2389C317.8816,-121.7213 344.9472,-131.9521 366.6665,-140.162"/>
+<polygon fill="#000000" stroke="#000000" points="365.6326,-143.5128 376.2242,-143.7748 368.1077,-136.965 365.6326,-143.5128"/>
+</g>
+<!-- mconfig2 -->
+<g id="node5" class="node">
+<title>mconfig2</title>
+<polygon fill="none" stroke="#000000" points="298,-259 244,-259 244,-223 298,-223 298,-259"/>
+<text text-anchor="middle" x="271" y="-237.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text>
+</g>
+<!-- bconfig2 -->
+<g id="node9" class="node">
+<title>bconfig2</title>
+<polygon fill="none" stroke="#000000" points="441.3261,-262 422.3005,-280 384.2493,-280 365.2238,-262 384.2493,-244 422.3005,-244 441.3261,-262"/>
+<text text-anchor="middle" x="403.2749" y="-258.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text>
+</g>
+<!-- mconfig2&#45;&gt;bconfig2 -->
+<g id="edge8" class="edge">
+<title>mconfig2&#45;&gt;bconfig2</title>
+<path fill="none" stroke="#000000" d="M298.087,-245.3003C316.0623,-248.1541 340.0335,-251.9598 360.5522,-255.2173"/>
+<polygon fill="#000000" stroke="#000000" points="360.1858,-258.7029 370.6109,-256.8143 361.2834,-251.7895 360.1858,-258.7029"/>
+</g>
+<!-- layers2 -->
+<g id="node6" class="node">
+<title>layers2</title>
+<polygon fill="none" stroke="#000000" points="298,-205 244,-205 244,-169 298,-169 298,-205"/>
+<text text-anchor="middle" x="271" y="-183.3" font-family="Times,serif" font-size="14.00" fill="#000000">layers</text>
+</g>
+<!-- layers2&#45;&gt;l2 -->
+<g id="edge5" class="edge">
+<title>layers2&#45;&gt;l2</title>
+<path fill="none" stroke="#000000" d="M298.087,-180.2423C317.7924,-175.3262 344.7036,-168.6124 366.3727,-163.2064"/>
+<polygon fill="#000000" stroke="#000000" points="367.3688,-166.5652 376.2242,-160.7486 365.6743,-159.7734 367.3688,-166.5652"/>
+</g>
+<!-- l3 -->
+<g id="node12" class="node">
+<title>l3</title>
+<polygon fill="none" stroke="#000000" points="430.2749,-226 427.2749,-230 406.2749,-230 403.2749,-226 376.2749,-226 376.2749,-190 430.2749,-190 430.2749,-226"/>
+<text text-anchor="middle" x="403.2749" y="-204.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer</text>
+</g>
+<!-- layers2&#45;&gt;l3 -->
+<g id="edge6" class="edge">
+<title>layers2&#45;&gt;l3</title>
+<path fill="none" stroke="#000000" d="M298.087,-191.3003C317.7033,-194.4146 344.4602,-198.6626 366.0782,-202.0946"/>
+<polygon fill="#000000" stroke="#000000" points="365.799,-205.5941 376.2242,-203.7054 366.8967,-198.6807 365.799,-205.5941"/>
+</g>
+<!-- imanifest&#45;&gt;mconfig -->
+<g id="edge1" class="edge">
+<title>imanifest&#45;&gt;mconfig</title>
+<path fill="none" stroke="#000000" d="M206.2373,-85.8686C215.4605,-80.7603 225.5439,-75.1757 234.9494,-69.9665"/>
+<polygon fill="#000000" stroke="#000000" points="230.4352,-78.131 236,-69.1149 226.0273,-72.6929 230.4352,-78.131"/>
+</g>
+<!-- imanifest&#45;&gt;mconfig2 -->
+<g id="edge2" class="edge">
+<title>imanifest&#45;&gt;mconfig2</title>
+<path fill="none" stroke="#000000" d="M181.0913,-122.1444C190.3822,-143.0951 207.6041,-178.2366 229.4113,-206.011"/>
+<polygon fill="#000000" stroke="#000000" points="226.9372,-208.5121 236,-214 232.3376,-204.0583 226.9372,-208.5121"/>
+</g>
+</g>
+</svg>
diff --git a/images/stream.dot.svg b/images/stream.dot.svg
new file mode 100644
index 0000000..3f3f04e
--- /dev/null
+++ b/images/stream.dot.svg
@@ -0,0 +1,217 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="533pt" height="767pt"
+ viewBox="0.00 0.00 533.09 767.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 763)">
+<title>G</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-763 529.0946,-763 529.0946,4 -4,4"/>
+<g id="clust1" class="cluster">
+<title>cluster_goroutine</title>
+<polygon fill="none" stroke="#000000" points="8,-208 8,-715 396,-715 396,-208 8,-208"/>
+<text text-anchor="middle" x="202" y="-699.8" font-family="Times,serif" font-size="14.00" fill="#000000">goroutine</text>
+</g>
+<!-- fs -->
+<g id="node1" class="node">
+<title>fs</title>
+<polygon fill="none" stroke="#000000" points="166,-759 163,-763 142,-763 139,-759 112,-759 112,-723 166,-723 166,-759"/>
+<text text-anchor="middle" x="139" y="-737.3" font-family="Times,serif" font-size="14.00" fill="#000000">input</text>
+</g>
+<!-- rc -->
+<g id="node6" class="node">
+<title>rc</title>
+<ellipse fill="none" stroke="#000000" cx="139" cy="-666" rx="61.1893" ry="18"/>
+<text text-anchor="middle" x="139" y="-662.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.ReadCloser</text>
+</g>
+<!-- fs&#45;&gt;rc -->
+<g id="edge12" class="edge">
+<title>fs&#45;&gt;rc</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M139,-722.8446C139,-714.3401 139,-704.0076 139,-694.4964"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="142.5001,-694.2481 139,-684.2482 135.5001,-694.2482 142.5001,-694.2481"/>
+</g>
+<!-- pr -->
+<g id="node2" class="node">
+<title>pr</title>
+<ellipse fill="none" stroke="#000000" cx="464" cy="-234" rx="60.3893" ry="18"/>
+<text text-anchor="middle" x="464" y="-230.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.PipeReader</text>
+</g>
+<!-- compressed -->
+<g id="node3" class="node">
+<title>compressed</title>
+<polygon fill="none" stroke="#000000" points="510.5,-180 417.5,-180 417.5,-144 510.5,-144 510.5,-180"/>
+<text text-anchor="middle" x="464" y="-158.3" font-family="Times,serif" font-size="14.00" fill="#000000">Compressed()</text>
+</g>
+<!-- pr&#45;&gt;compressed -->
+<g id="edge14" class="edge">
+<title>pr&#45;&gt;compressed</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M464,-215.8314C464,-208.131 464,-198.9743 464,-190.4166"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="467.5001,-190.4132 464,-180.4133 460.5001,-190.4133 467.5001,-190.4132"/>
+</g>
+<!-- rc2 -->
+<g id="node4" class="node">
+<title>rc2</title>
+<ellipse fill="none" stroke="#000000" cx="464" cy="-90" rx="61.1893" ry="18"/>
+<text text-anchor="middle" x="464" y="-86.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.ReadCloser</text>
+</g>
+<!-- compressed&#45;&gt;rc2 -->
+<g id="edge15" class="edge">
+<title>compressed&#45;&gt;rc2</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M464,-143.8314C464,-136.131 464,-126.9743 464,-118.4166"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="467.5001,-118.4132 464,-108.4133 460.5001,-118.4133 467.5001,-118.4132"/>
+</g>
+<!-- output -->
+<g id="node5" class="node">
+<title>output</title>
+<path fill="none" stroke="#000000" d="M491,-32.7273C491,-34.5331 478.8982,-36 464,-36 449.1018,-36 437,-34.5331 437,-32.7273 437,-32.7273 437,-3.2727 437,-3.2727 437,-1.4669 449.1018,0 464,0 478.8982,0 491,-1.4669 491,-3.2727 491,-3.2727 491,-32.7273 491,-32.7273"/>
+<path fill="none" stroke="#000000" d="M491,-32.7273C491,-30.9214 478.8982,-29.4545 464,-29.4545 449.1018,-29.4545 437,-30.9214 437,-32.7273"/>
+<text text-anchor="middle" x="464" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">output</text>
+</g>
+<!-- rc2&#45;&gt;output -->
+<g id="edge16" class="edge">
+<title>rc2&#45;&gt;output</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M464,-71.8314C464,-64.131 464,-54.9743 464,-46.4166"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="467.5001,-46.4132 464,-36.4133 460.5001,-46.4133 467.5001,-46.4132"/>
+</g>
+<!-- copy -->
+<g id="node7" class="node">
+<title>copy</title>
+<ellipse fill="none" stroke="#000000" cx="139" cy="-594" rx="38.9931" ry="18"/>
+<text text-anchor="middle" x="139" y="-590.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.Copy</text>
+</g>
+<!-- rc&#45;&gt;copy -->
+<g id="edge1" class="edge">
+<title>rc&#45;&gt;copy</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M139,-647.8314C139,-640.131 139,-630.9743 139,-622.4166"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="142.5001,-622.4132 139,-612.4133 135.5001,-622.4133 142.5001,-622.4132"/>
+</g>
+<!-- mw -->
+<g id="node9" class="node">
+<title>mw</title>
+<ellipse fill="none" stroke="#000000" cx="139" cy="-522" rx="63.8893" ry="18"/>
+<text text-anchor="middle" x="139" y="-518.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.MultiWriter</text>
+</g>
+<!-- copy&#45;&gt;mw -->
+<g id="edge2" class="edge">
+<title>copy&#45;&gt;mw</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M139,-575.8314C139,-568.131 139,-558.9743 139,-550.4166"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="142.5001,-550.4132 139,-540.4133 135.5001,-550.4133 142.5001,-550.4132"/>
+</g>
+<!-- pw -->
+<g id="node8" class="node">
+<title>pw</title>
+<ellipse fill="none" stroke="#000000" cx="329" cy="-306" rx="59.2899" ry="18"/>
+<text text-anchor="middle" x="329" y="-302.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.PipeWriter</text>
+</g>
+<!-- pw&#45;&gt;pr -->
+<g id="edge13" class="edge">
+<title>pw&#45;&gt;pr</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M358.6263,-290.1993C378.2335,-279.7421 404.1444,-265.923 425.3655,-254.6051"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="427.2663,-257.558 434.4428,-249.7638 423.9722,-251.3815 427.2663,-257.558"/>
+</g>
+<!-- h1 -->
+<g id="node10" class="node">
+<title>h1</title>
+<ellipse fill="none" stroke="#000000" cx="73" cy="-450" rx="54.6905" ry="18"/>
+<text text-anchor="middle" x="73" y="-446.3" font-family="Times,serif" font-size="14.00" fill="#000000">sha256.New</text>
+</g>
+<!-- mw&#45;&gt;h1 -->
+<g id="edge3" class="edge">
+<title>mw&#45;&gt;h1</title>
+<path fill="none" stroke="#000000" d="M123.0232,-504.5708C114.8353,-495.6385 104.7218,-484.6056 95.736,-474.8029"/>
+<polygon fill="#000000" stroke="#000000" points="98.167,-472.2752 88.8296,-467.2687 93.0069,-477.0053 98.167,-472.2752"/>
+</g>
+<!-- gzip -->
+<g id="node11" class="node">
+<title>gzip</title>
+<ellipse fill="none" stroke="#000000" cx="198" cy="-450" rx="51.9908" ry="18"/>
+<text text-anchor="middle" x="198" y="-446.3" font-family="Times,serif" font-size="14.00" fill="#000000">gzip.Writer</text>
+</g>
+<!-- mw&#45;&gt;gzip -->
+<g id="edge5" class="edge">
+<title>mw&#45;&gt;gzip</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M153.5843,-504.2022C160.7104,-495.506 169.4123,-484.8867 177.2191,-475.3598"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="180.156,-477.2978 183.7871,-467.3446 174.7416,-472.861 180.156,-477.2978"/>
+</g>
+<!-- diffid -->
+<g id="node16" class="node">
+<title>diffid</title>
+<polygon fill="none" stroke="#000000" points="104,-396 42,-396 42,-360 104,-360 104,-396"/>
+<text text-anchor="middle" x="73" y="-374.3" font-family="Times,serif" font-size="14.00" fill="#000000">DiffID()</text>
+</g>
+<!-- h1&#45;&gt;diffid -->
+<g id="edge4" class="edge">
+<title>h1&#45;&gt;diffid</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M73,-431.8314C73,-424.131 73,-414.9743 73,-406.4166"/>
+<polygon fill="#000000" stroke="#000000" points="76.5001,-406.4132 73,-396.4133 69.5001,-406.4133 76.5001,-406.4132"/>
+</g>
+<!-- mw2 -->
+<g id="node12" class="node">
+<title>mw2</title>
+<ellipse fill="none" stroke="#000000" cx="198" cy="-378" rx="63.8893" ry="18"/>
+<text text-anchor="middle" x="198" y="-374.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.MultiWriter</text>
+</g>
+<!-- gzip&#45;&gt;mw2 -->
+<g id="edge6" class="edge">
+<title>gzip&#45;&gt;mw2</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M198,-431.8314C198,-424.131 198,-414.9743 198,-406.4166"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="201.5001,-406.4132 198,-396.4133 194.5001,-406.4133 201.5001,-406.4132"/>
+</g>
+<!-- mw2&#45;&gt;pw -->
+<g id="edge11" class="edge">
+<title>mw2&#45;&gt;pw</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M227.399,-361.8418C246.2391,-351.4869 270.8742,-337.947 291.1679,-326.7932"/>
+<polygon fill="#000000" stroke="#000000" stroke-width="2" points="293.0724,-329.7403 300.1502,-321.8564 289.7008,-323.6058 293.0724,-329.7403"/>
+</g>
+<!-- h2 -->
+<g id="node13" class="node">
+<title>h2</title>
+<ellipse fill="none" stroke="#000000" cx="71" cy="-306" rx="54.6905" ry="18"/>
+<text text-anchor="middle" x="71" y="-302.3" font-family="Times,serif" font-size="14.00" fill="#000000">sha256.New</text>
+</g>
+<!-- mw2&#45;&gt;h2 -->
+<g id="edge7" class="edge">
+<title>mw2&#45;&gt;h2</title>
+<path fill="none" stroke="#000000" d="M169.4987,-361.8418C151.107,-351.415 127.019,-337.7588 107.2676,-326.5612"/>
+<polygon fill="#000000" stroke="#000000" points="108.9593,-323.4969 98.5339,-321.6098 105.507,-329.5864 108.9593,-323.4969"/>
+</g>
+<!-- count -->
+<g id="node14" class="node">
+<title>count</title>
+<ellipse fill="none" stroke="#000000" cx="198" cy="-306" rx="53.8905" ry="18"/>
+<text text-anchor="middle" x="198" y="-302.3" font-family="Times,serif" font-size="14.00" fill="#000000">countWriter</text>
+</g>
+<!-- mw2&#45;&gt;count -->
+<g id="edge9" class="edge">
+<title>mw2&#45;&gt;count</title>
+<path fill="none" stroke="#000000" d="M198,-359.8314C198,-352.131 198,-342.9743 198,-334.4166"/>
+<polygon fill="#000000" stroke="#000000" points="201.5001,-334.4132 198,-324.4133 194.5001,-334.4133 201.5001,-334.4132"/>
+</g>
+<!-- digest -->
+<g id="node17" class="node">
+<title>digest</title>
+<polygon fill="none" stroke="#000000" points="101.5,-252 40.5,-252 40.5,-216 101.5,-216 101.5,-252"/>
+<text text-anchor="middle" x="71" y="-230.3" font-family="Times,serif" font-size="14.00" fill="#000000">Digest()</text>
+</g>
+<!-- h2&#45;&gt;digest -->
+<g id="edge8" class="edge">
+<title>h2&#45;&gt;digest</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M71,-287.8314C71,-280.131 71,-270.9743 71,-262.4166"/>
+<polygon fill="#000000" stroke="#000000" points="74.5001,-262.4132 71,-252.4133 67.5001,-262.4133 74.5001,-262.4132"/>
+</g>
+<!-- size -->
+<g id="node15" class="node">
+<title>size</title>
+<polygon fill="none" stroke="#000000" points="225,-252 171,-252 171,-216 225,-216 225,-252"/>
+<text text-anchor="middle" x="198" y="-230.3" font-family="Times,serif" font-size="14.00" fill="#000000">Size()</text>
+</g>
+<!-- count&#45;&gt;size -->
+<g id="edge10" class="edge">
+<title>count&#45;&gt;size</title>
+<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M198,-287.8314C198,-280.131 198,-270.9743 198,-262.4166"/>
+<polygon fill="#000000" stroke="#000000" points="201.5001,-262.4132 198,-252.4133 194.5001,-262.4133 201.5001,-262.4132"/>
+</g>
+</g>
+</svg>
diff --git a/images/tarball.dot.svg b/images/tarball.dot.svg
new file mode 100644
index 0000000..4c6edc0
--- /dev/null
+++ b/images/tarball.dot.svg
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: %3 Pages: 1 -->
+<svg width="523pt" height="303pt"
+ viewBox="0.00 0.00 523.00 303.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 299)">
+<title>%3</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-299 519,-299 519,4 -4,4"/>
+<g id="clust1" class="cluster">
+<title>cluster_tarball</title>
+<polygon fill="none" stroke="#000000" points="8,-8 8,-287 507,-287 507,-8 8,-8"/>
+<text text-anchor="middle" x="257.5" y="-271.8" font-family="Times,serif" font-size="14.00" fill="#000000">image.tar</text>
+</g>
+<g id="clust2" class="cluster">
+<title>cluster_manifest</title>
+<polygon fill="none" stroke="#000000" points="16,-16 16,-253 123,-253 123,-16 16,-16"/>
+<text text-anchor="middle" x="69.5" y="-237.8" font-family="Times,serif" font-size="14.00" fill="#000000">manifest.json</text>
+</g>
+<g id="clust3" class="cluster">
+<title>cluster_layer1</title>
+<polygon fill="none" stroke="#000000" points="397,-34 397,-133 499,-133 499,-34 397,-34"/>
+<text text-anchor="middle" x="448" y="-117.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar.gz</text>
+</g>
+<g id="clust4" class="cluster">
+<title>cluster_layer2</title>
+<polygon fill="none" stroke="#000000" points="397,-141 397,-240 499,-240 499,-141 397,-141"/>
+<text text-anchor="middle" x="448" y="-224.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar.gz</text>
+</g>
+<!-- mconfig -->
+<g id="node1" class="node">
+<title>mconfig</title>
+<polygon fill="none" stroke="#000000" points="96.5,-168 42.5,-168 42.5,-132 96.5,-132 96.5,-168"/>
+<text text-anchor="middle" x="69.5" y="-146.3" font-family="Times,serif" font-size="14.00" fill="#000000">Config</text>
+</g>
+<!-- config -->
+<g id="node5" class="node">
+<title>config</title>
+<polygon fill="none" stroke="#000000" points="291,-154 243,-154 243,-118 297,-118 297,-148 291,-154"/>
+<polyline fill="none" stroke="#000000" points="291,-154 291,-148 "/>
+<polyline fill="none" stroke="#000000" points="297,-148 291,-148 "/>
+<text text-anchor="middle" x="270" y="-132.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text>
+</g>
+<!-- mconfig&#45;&gt;config -->
+<g id="edge1" class="edge">
+<title>mconfig&#45;&gt;config</title>
+<path fill="none" stroke="#000000" d="M96.5403,-148.1119C131.668,-145.6591 193.1644,-141.3651 232.7073,-138.604"/>
+<polygon fill="#000000" stroke="#000000" points="233.0159,-142.0911 242.7477,-137.9029 232.5282,-135.1081 233.0159,-142.0911"/>
+<text text-anchor="middle" x="176.5" y="-147.8" font-family="Times,serif" font-size="14.00" fill="#000000">image id</text>
+</g>
+<!-- layers -->
+<g id="node2" class="node">
+<title>layers</title>
+<polygon fill="none" stroke="#000000" points="96.5,-222 42.5,-222 42.5,-186 96.5,-186 96.5,-222"/>
+<text text-anchor="middle" x="69.5" y="-200.3" font-family="Times,serif" font-size="14.00" fill="#000000">Layers</text>
+</g>
+<!-- l1 -->
+<g id="node6" class="node">
+<title>l1</title>
+<polygon fill="none" stroke="#000000" points="479,-90 476,-94 455,-94 452,-90 417,-90 417,-54 479,-54 479,-90"/>
+<text text-anchor="middle" x="448" y="-68.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar</text>
+</g>
+<!-- layers&#45;&gt;l1 -->
+<g id="edge2" class="edge">
+<title>layers&#45;&gt;l1</title>
+<path fill="none" stroke="#000000" d="M96.5198,-204.19C165.9738,-204.4755 346.9131,-203.8632 370,-190 390.008,-177.9856 385.5908,-163.8126 394.84,-142.0701"/>
+<polygon fill="#000000" stroke="#000000" points="398.031,-143.513 399.3348,-132.9987 391.7588,-140.4051 398.031,-143.513"/>
+<text text-anchor="middle" x="270" y="-205.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text>
+</g>
+<!-- l2 -->
+<g id="node7" class="node">
+<title>l2</title>
+<polygon fill="none" stroke="#000000" points="479,-197 476,-201 455,-201 452,-197 417,-197 417,-161 479,-161 479,-197"/>
+<text text-anchor="middle" x="448" y="-175.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar</text>
+</g>
+<!-- layers&#45;&gt;l2 -->
+<g id="edge3" class="edge">
+<title>layers&#45;&gt;l2</title>
+<path fill="none" stroke="#000000" d="M96.7682,-219.0493C117.8784,-229.293 148.1958,-241 176.5,-241 176.5,-241 176.5,-241 354.5,-241 367.5727,-241 373.2434,-242.298 387.8589,-235.9183"/>
+<polygon fill="#000000" stroke="#000000" points="389.5034,-239.0113 397.0026,-231.5271 386.473,-232.7012 389.5034,-239.0113"/>
+<text text-anchor="middle" x="270" y="-244.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text>
+</g>
+<!-- sources -->
+<g id="node3" class="node">
+<title>sources</title>
+<polygon fill="none" stroke="#000000" points="115,-114 24,-114 24,-78 115,-78 115,-114"/>
+<text text-anchor="middle" x="69.5" y="-92.3" font-family="Times,serif" font-size="14.00" fill="#000000">LayerSources</text>
+</g>
+<!-- sources&#45;&gt;l1 -->
+<g id="edge6" class="edge">
+<title>sources&#45;&gt;l1</title>
+<path fill="none" stroke="#000000" d="M106.626,-77.9421C112.1689,-75.0511 117.7734,-72.0159 123,-69 147.6108,-54.799 148.086,-34 176.5,-34 176.5,-34 176.5,-34 354.5,-34 369.9935,-34 374.3272,-34.0242 389,-39 396.5806,-41.5707 404.3097,-45.114 411.5701,-48.9291"/>
+<polygon fill="#000000" stroke="#000000" points="410.0253,-52.0743 420.4696,-53.8542 413.4148,-45.9497 410.0253,-52.0743"/>
+<text text-anchor="middle" x="270" y="-37.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text>
+</g>
+<!-- sources&#45;&gt;l2 -->
+<g id="edge7" class="edge">
+<title>sources&#45;&gt;l2</title>
+<path fill="none" stroke="#000000" d="M115.0969,-83.9218C174.9057,-70.173 282.5191,-52.6874 370,-78 379.5941,-80.7761 383.4596,-81.69 389,-90 400.7544,-107.6302 386.3753,-118.6669 397,-137 400.6773,-143.3452 405.699,-149.1408 411.1641,-154.2655"/>
+<polygon fill="#000000" stroke="#000000" points="408.9395,-156.9685 418.807,-160.8264 413.4991,-151.6571 408.9395,-156.9685"/>
+<text text-anchor="middle" x="270" y="-69.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text>
+</g>
+<!-- tags -->
+<g id="node4" class="node">
+<title>tags</title>
+<polygon fill="none" stroke="#000000" points="105,-60 34,-60 34,-24 105,-24 105,-60"/>
+<text text-anchor="middle" x="69.5" y="-38.3" font-family="Times,serif" font-size="14.00" fill="#000000">RepoTags</text>
+</g>
+<!-- config&#45;&gt;l1 -->
+<g id="edge4" class="edge">
+<title>config&#45;&gt;l1</title>
+<path fill="none" stroke="#000000" d="M297.0344,-126.2798C326.3338,-115.7451 373.4204,-98.8151 407.2182,-86.6631"/>
+<polygon fill="#000000" stroke="#000000" points="408.5826,-89.892 416.8086,-83.2149 406.2141,-83.3048 408.5826,-89.892"/>
+<text text-anchor="middle" x="354.5" y="-113.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text>
+</g>
+<!-- config&#45;&gt;l2 -->
+<g id="edge5" class="edge">
+<title>config&#45;&gt;l2</title>
+<path fill="none" stroke="#000000" d="M297.0344,-142.5308C326.2081,-149.5784 373.0165,-160.886 406.7823,-169.0429"/>
+<polygon fill="#000000" stroke="#000000" points="406.2663,-172.5189 416.8086,-171.465 407.9101,-165.7146 406.2663,-172.5189"/>
+<text text-anchor="middle" x="354.5" y="-163.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text>
+</g>
+</g>
+</svg>
diff --git a/images/upload.dot.svg b/images/upload.dot.svg
new file mode 100644
index 0000000..16ba738
--- /dev/null
+++ b/images/upload.dot.svg
@@ -0,0 +1,359 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="505pt" height="882pt"
+ viewBox="0.00 0.00 504.57 881.79" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 877.7939)">
+<title>G</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-877.7939 500.5651,-877.7939 500.5651,4 -4,4"/>
+<!-- fs -->
+<g id="node1" class="node">
+<title>fs</title>
+<g id="a_node1"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/layer.md" xlink:title="filesystem\nchangeset">
+<polygon fill="none" stroke="#000000" points="291.9498,-873.7939 288.9498,-877.7939 267.9498,-877.7939 264.9498,-873.7939 218.9498,-873.7939 218.9498,-835.7939 291.9498,-835.7939 291.9498,-873.7939"/>
+<text text-anchor="middle" x="255.4498" y="-858.5939" font-family="Times,serif" font-size="14.00" fill="#000000">filesystem</text>
+<text text-anchor="middle" x="255.4498" y="-843.5939" font-family="Times,serif" font-size="14.00" fill="#000000">changeset</text>
+</a>
+</g>
+</g>
+<!-- tar -->
+<g id="node3" class="node">
+<title>tar</title>
+<polygon fill="none" stroke="#000000" points="282.4498,-799.7939 228.4498,-799.7939 228.4498,-763.7939 282.4498,-763.7939 282.4498,-799.7939"/>
+<text text-anchor="middle" x="255.4498" y="-778.0939" font-family="Times,serif" font-size="14.00" fill="#000000">tar</text>
+</g>
+<!-- fs&#45;&gt;tar -->
+<g id="edge2" class="edge">
+<title>fs&#45;&gt;tar</title>
+<path fill="none" stroke="#000000" d="M255.4498,-835.614C255.4498,-827.8913 255.4498,-818.8221 255.4498,-810.3524"/>
+<polygon fill="#000000" stroke="#000000" points="258.9499,-810.0912 255.4498,-800.0912 251.9499,-810.0912 258.9499,-810.0912"/>
+</g>
+<!-- configuration -->
+<g id="node2" class="node">
+<title>configuration</title>
+<g id="a_node2"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/config.md#properties" xlink:title="image\nconfig">
+<polygon fill="none" stroke="#000000" points="299.233,-552.767 278.3414,-583.8207 236.5583,-583.8207 215.6667,-552.767 236.5583,-521.7132 278.3414,-521.7132 299.233,-552.767"/>
+<text text-anchor="middle" x="257.4498" y="-556.567" font-family="Times,serif" font-size="14.00" fill="#000000">image</text>
+<text text-anchor="middle" x="257.4498" y="-541.567" font-family="Times,serif" font-size="14.00" fill="#000000">config</text>
+</a>
+</g>
+</g>
+<!-- config -->
+<g id="node16" class="node">
+<title>config</title>
+<g id="a_node16"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/config.md" xlink:title="config file">
+<polygon fill="none" stroke="#000000" points="197.9498,-485.7401 130.9498,-485.7401 130.9498,-449.7401 203.9498,-449.7401 203.9498,-479.7401 197.9498,-485.7401"/>
+<polyline fill="none" stroke="#000000" points="197.9498,-485.7401 197.9498,-479.7401 "/>
+<polyline fill="none" stroke="#000000" points="203.9498,-479.7401 197.9498,-479.7401 "/>
+<text text-anchor="middle" x="167.4498" y="-464.0401" font-family="Times,serif" font-size="14.00" fill="#000000">config file</text>
+</a>
+</g>
+</g>
+<!-- configuration&#45;&gt;config -->
+<g id="edge1" class="edge">
+<title>configuration&#45;&gt;config</title>
+<path fill="none" stroke="#000000" d="M231.9002,-528.6291C220.1342,-517.5133 206.1713,-504.322 194.2553,-493.0644"/>
+<polygon fill="#000000" stroke="#000000" points="196.4367,-490.3103 186.764,-485.987 191.6295,-495.3986 196.4367,-490.3103"/>
+</g>
+<!-- tee -->
+<g id="node5" class="node">
+<title>tee</title>
+<polygon fill="none" stroke="#000000" points="282.4498,-727.7939 228.4498,-727.7939 228.4498,-691.7939 282.4498,-691.7939 282.4498,-727.7939"/>
+<text text-anchor="middle" x="255.4498" y="-706.0939" font-family="Times,serif" font-size="14.00" fill="#000000">tee</text>
+</g>
+<!-- tar&#45;&gt;tee -->
+<g id="edge3" class="edge">
+<title>tar&#45;&gt;tee</title>
+<path fill="none" stroke="#000000" d="M255.4498,-763.6252C255.4498,-755.9248 255.4498,-746.7682 255.4498,-738.2105"/>
+<polygon fill="#000000" stroke="#000000" points="258.9499,-738.2071 255.4498,-728.2071 251.9499,-738.2072 258.9499,-738.2071"/>
+</g>
+<!-- gzip -->
+<g id="node4" class="node">
+<title>gzip</title>
+<polygon fill="none" stroke="#000000" points="347.4498,-655.7939 293.4498,-655.7939 293.4498,-619.7939 347.4498,-619.7939 347.4498,-655.7939"/>
+<text text-anchor="middle" x="320.4498" y="-634.0939" font-family="Times,serif" font-size="14.00" fill="#000000">gzip</text>
+</g>
+<!-- layer -->
+<g id="node17" class="node">
+<title>layer</title>
+<g id="a_node17"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/layer.md" xlink:title="layer">
+<polygon fill="none" stroke="#000000" points="375.4498,-570.767 327.4498,-570.767 327.4498,-534.767 381.4498,-534.767 381.4498,-564.767 375.4498,-570.767"/>
+<polyline fill="none" stroke="#000000" points="375.4498,-570.767 375.4498,-564.767 "/>
+<polyline fill="none" stroke="#000000" points="381.4498,-564.767 375.4498,-564.767 "/>
+<text text-anchor="middle" x="354.4498" y="-549.067" font-family="Times,serif" font-size="14.00" fill="#000000">layer</text>
+</a>
+</g>
+</g>
+<!-- gzip&#45;&gt;layer -->
+<g id="edge7" class="edge">
+<title>gzip&#45;&gt;layer</title>
+<path fill="none" stroke="#000000" d="M327.6604,-619.7618C332.2583,-608.2635 338.3055,-593.1407 343.4908,-580.1733"/>
+<polygon fill="#000000" stroke="#000000" points="346.7866,-581.3577 347.2497,-570.773 340.2869,-578.7586 346.7866,-581.3577"/>
+</g>
+<!-- tee&#45;&gt;gzip -->
+<g id="edge6" class="edge">
+<title>tee&#45;&gt;gzip</title>
+<path fill="none" stroke="#000000" d="M271.8521,-691.6252C279.4914,-683.1632 288.7182,-672.9428 297.0705,-663.6909"/>
+<polygon fill="#000000" stroke="#000000" points="299.7236,-665.9752 303.8267,-656.2071 294.5277,-661.2845 299.7236,-665.9752"/>
+</g>
+<!-- sha256sum -->
+<g id="node8" class="node">
+<title>sha256sum</title>
+<polygon fill="none" stroke="#000000" points="252.4498,-655.7939 174.4498,-655.7939 174.4498,-619.7939 252.4498,-619.7939 252.4498,-655.7939"/>
+<text text-anchor="middle" x="213.4498" y="-634.0939" font-family="Times,serif" font-size="14.00" fill="#000000">sha256sum</text>
+</g>
+<!-- tee&#45;&gt;sha256sum -->
+<g id="edge4" class="edge">
+<title>tee&#45;&gt;sha256sum</title>
+<path fill="none" stroke="#000000" d="M244.8515,-691.6252C240.1621,-683.5863 234.547,-673.9604 229.37,-665.0856"/>
+<polygon fill="#000000" stroke="#000000" points="232.2529,-663.0814 224.1909,-656.2071 226.2064,-666.6085 232.2529,-663.0814"/>
+</g>
+<!-- tee2 -->
+<g id="node6" class="node">
+<title>tee2</title>
+<polygon fill="none" stroke="#000000" points="407.4498,-413.7401 353.4498,-413.7401 353.4498,-377.7401 407.4498,-377.7401 407.4498,-413.7401"/>
+<text text-anchor="middle" x="380.4498" y="-392.0401" font-family="Times,serif" font-size="14.00" fill="#000000">tee</text>
+</g>
+<!-- sha256sum2 -->
+<g id="node9" class="node">
+<title>sha256sum2</title>
+<polygon fill="none" stroke="#000000" points="370.4498,-341.7401 292.4498,-341.7401 292.4498,-305.7401 370.4498,-305.7401 370.4498,-341.7401"/>
+<text text-anchor="middle" x="331.4498" y="-320.0401" font-family="Times,serif" font-size="14.00" fill="#000000">sha256sum</text>
+</g>
+<!-- tee2&#45;&gt;sha256sum2 -->
+<g id="edge9" class="edge">
+<title>tee2&#45;&gt;sha256sum2</title>
+<path fill="none" stroke="#000000" d="M368.0851,-377.5715C362.499,-369.3634 355.7869,-359.5007 349.6426,-350.4724"/>
+<polygon fill="#000000" stroke="#000000" points="352.5009,-348.4513 343.9811,-342.1534 346.7139,-352.3897 352.5009,-348.4513"/>
+</g>
+<!-- curl -->
+<g id="node11" class="node">
+<title>curl</title>
+<polygon fill="none" stroke="#000000" points="476.4498,-180 422.4498,-180 422.4498,-144 476.4498,-144 476.4498,-180"/>
+<text text-anchor="middle" x="449.4498" y="-158.3" font-family="Times,serif" font-size="14.00" fill="#000000">curl</text>
+</g>
+<!-- tee2&#45;&gt;curl -->
+<g id="edge14" class="edge">
+<title>tee2&#45;&gt;curl</title>
+<path fill="none" stroke="#000000" d="M407.4647,-382.2896C424.2217,-372.836 445.1987,-358.8424 459.4498,-341.7401 481.8672,-314.8378 484.1804,-303.9954 491.4498,-269.7401 496.408,-246.3759 499.2012,-238.5917 491.4498,-216 487.9808,-205.8894 481.7599,-196.2137 475.1508,-187.9166"/>
+<polygon fill="#000000" stroke="#000000" points="477.6506,-185.4539 468.4998,-180.1141 472.3234,-189.9949 477.6506,-185.4539"/>
+</g>
+<!-- wc -->
+<g id="node14" class="node">
+<title>wc</title>
+<polygon fill="none" stroke="#000000" points="450.4498,-341.7401 396.4498,-341.7401 396.4498,-305.7401 450.4498,-305.7401 450.4498,-341.7401"/>
+<text text-anchor="middle" x="423.4498" y="-320.0401" font-family="Times,serif" font-size="14.00" fill="#000000">wc &#45;c</text>
+</g>
+<!-- tee2&#45;&gt;wc -->
+<g id="edge11" class="edge">
+<title>tee2&#45;&gt;wc</title>
+<path fill="none" stroke="#000000" d="M391.3005,-377.5715C396.1521,-369.448 401.9715,-359.7038 407.3178,-350.7519"/>
+<polygon fill="#000000" stroke="#000000" points="410.3305,-352.5334 412.453,-342.1534 404.3207,-348.9442 410.3305,-352.5334"/>
+</g>
+<!-- tee3 -->
+<g id="node7" class="node">
+<title>tee3</title>
+<polygon fill="none" stroke="#000000" points="175.4498,-413.7401 121.4498,-413.7401 121.4498,-377.7401 175.4498,-377.7401 175.4498,-413.7401"/>
+<text text-anchor="middle" x="148.4498" y="-392.0401" font-family="Times,serif" font-size="14.00" fill="#000000">tee</text>
+</g>
+<!-- sha256sum3 -->
+<g id="node10" class="node">
+<title>sha256sum3</title>
+<polygon fill="none" stroke="#000000" points="220.4498,-341.7401 142.4498,-341.7401 142.4498,-305.7401 220.4498,-305.7401 220.4498,-341.7401"/>
+<text text-anchor="middle" x="181.4498" y="-320.0401" font-family="Times,serif" font-size="14.00" fill="#000000">sha256sum</text>
+</g>
+<!-- tee3&#45;&gt;sha256sum3 -->
+<g id="edge21" class="edge">
+<title>tee3&#45;&gt;sha256sum3</title>
+<path fill="none" stroke="#000000" d="M156.7771,-377.5715C160.4228,-369.6172 164.7807,-360.1091 168.8125,-351.3124"/>
+<polygon fill="#000000" stroke="#000000" points="172.0255,-352.7024 173.0104,-342.1534 165.6621,-349.7857 172.0255,-352.7024"/>
+</g>
+<!-- curl2 -->
+<g id="node12" class="node">
+<title>curl2</title>
+<polygon fill="none" stroke="#000000" points="140.4498,-108 86.4498,-108 86.4498,-72 140.4498,-72 140.4498,-108"/>
+<text text-anchor="middle" x="113.4498" y="-86.3" font-family="Times,serif" font-size="14.00" fill="#000000">curl</text>
+</g>
+<!-- tee3&#45;&gt;curl2 -->
+<g id="edge18" class="edge">
+<title>tee3&#45;&gt;curl2</title>
+<path fill="none" stroke="#000000" d="M121.176,-387.9084C97.0472,-379.7786 62.2297,-364.9521 39.4498,-341.7401 14.6626,-316.4827 13.193,-304.2711 5.4498,-269.7401 .2238,-246.4344 -2.8368,-238.4009 5.4498,-216 20.6686,-174.8598 55.9849,-138.0878 82.1108,-115.0395"/>
+<polygon fill="#000000" stroke="#000000" points="84.5989,-117.5158 89.8914,-108.3375 80.0304,-112.2121 84.5989,-117.5158"/>
+</g>
+<!-- wc2 -->
+<g id="node15" class="node">
+<title>wc2</title>
+<polygon fill="none" stroke="#000000" points="102.4498,-341.7401 48.4498,-341.7401 48.4498,-305.7401 102.4498,-305.7401 102.4498,-341.7401"/>
+<text text-anchor="middle" x="75.4498" y="-320.0401" font-family="Times,serif" font-size="14.00" fill="#000000">wc &#45;c</text>
+</g>
+<!-- tee3&#45;&gt;wc2 -->
+<g id="edge20" class="edge">
+<title>tee3&#45;&gt;wc2</title>
+<path fill="none" stroke="#000000" d="M130.0288,-377.5715C121.2969,-368.9591 110.7181,-358.5252 101.2075,-349.1449"/>
+<polygon fill="#000000" stroke="#000000" points="103.4424,-346.4332 93.8649,-341.9029 98.5269,-351.417 103.4424,-346.4332"/>
+</g>
+<!-- diffid -->
+<g id="node24" class="node">
+<title>diffid</title>
+<g id="a_node24"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/config.md#layer-diffid" xlink:title="diffid">
+<ellipse fill="none" stroke="#000000" cx="167.4498" cy="-552.767" rx="30.5947" ry="18"/>
+<text text-anchor="middle" x="167.4498" y="-549.067" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text>
+</a>
+</g>
+</g>
+<!-- sha256sum&#45;&gt;diffid -->
+<g id="edge5" class="edge">
+<title>sha256sum&#45;&gt;diffid</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M203.6944,-619.7618C197.3279,-607.9938 188.9074,-592.4294 181.7852,-579.2647"/>
+<polygon fill="#000000" stroke="#000000" points="184.7133,-577.3214 176.8766,-570.1915 178.5566,-580.6523 184.7133,-577.3214"/>
+</g>
+<!-- layer_digest -->
+<g id="node23" class="node">
+<title>layer_digest</title>
+<ellipse fill="none" stroke="#000000" cx="324.4498" cy="-242.8701" rx="51.9908" ry="18"/>
+<text text-anchor="middle" x="324.4498" y="-239.1701" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text>
+</g>
+<!-- sha256sum2&#45;&gt;layer_digest -->
+<g id="edge10" class="edge">
+<title>sha256sum2&#45;&gt;layer_digest</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M329.8614,-305.3894C328.9782,-295.1851 327.8606,-282.274 326.8707,-270.8377"/>
+<polygon fill="#000000" stroke="#000000" points="330.3573,-270.5304 326.0078,-260.8695 323.3833,-271.1341 330.3573,-270.5304"/>
+</g>
+<!-- config_digest -->
+<g id="node22" class="node">
+<title>config_digest</title>
+<g id="a_node22"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/config.md#imageid" xlink:title="config digest\n(image id)">
+<ellipse fill="none" stroke="#000000" cx="192.4498" cy="-242.8701" rx="61.5366" ry="26.7407"/>
+<text text-anchor="middle" x="192.4498" y="-246.6701" font-family="Times,serif" font-size="14.00" fill="#000000">config digest</text>
+<text text-anchor="middle" x="192.4498" y="-231.6701" font-family="Times,serif" font-size="14.00" fill="#000000">(image id)</text>
+</a>
+</g>
+</g>
+<!-- sha256sum3&#45;&gt;config_digest -->
+<g id="edge23" class="edge">
+<title>sha256sum3&#45;&gt;config_digest</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M183.9459,-305.3894C184.9756,-297.8196 186.2078,-288.7601 187.4105,-279.9182"/>
+<polygon fill="#000000" stroke="#000000" points="190.9151,-280.1212 188.7949,-269.7407 183.9789,-279.1777 190.9151,-280.1212"/>
+</g>
+<!-- registry -->
+<g id="node19" class="node">
+<title>registry</title>
+<g id="a_node19"><a xlink:href="https://github.com/opencontainers/distribution-spec/blob/master/spec.md" xlink:title="registry">
+<path fill="none" stroke="#000000" d="M281.4498,-32.7273C281.4498,-34.5331 268.4516,-36 252.4498,-36 236.4481,-36 223.4498,-34.5331 223.4498,-32.7273 223.4498,-32.7273 223.4498,-3.2727 223.4498,-3.2727 223.4498,-1.4669 236.4481,0 252.4498,0 268.4516,0 281.4498,-1.4669 281.4498,-3.2727 281.4498,-3.2727 281.4498,-32.7273 281.4498,-32.7273"/>
+<path fill="none" stroke="#000000" d="M281.4498,-32.7273C281.4498,-30.9214 268.4516,-29.4545 252.4498,-29.4545 236.4481,-29.4545 223.4498,-30.9214 223.4498,-32.7273"/>
+<text text-anchor="middle" x="252.4498" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">registry</text>
+</a>
+</g>
+</g>
+<!-- curl&#45;&gt;registry -->
+<g id="edge15" class="edge">
+<title>curl&#45;&gt;registry</title>
+<path fill="none" stroke="#000000" d="M424.4996,-143.7623C388.8852,-117.7294 323.4665,-69.9107 284.3667,-41.3301"/>
+<polygon fill="#000000" stroke="#000000" points="286.1098,-38.2689 275.9712,-35.1933 281.9789,-43.9201 286.1098,-38.2689"/>
+</g>
+<!-- curl2&#45;&gt;registry -->
+<g id="edge19" class="edge">
+<title>curl2&#45;&gt;registry</title>
+<path fill="none" stroke="#000000" d="M140.5804,-75.9468C161.5049,-65.1082 190.7151,-49.9777 214.0402,-37.8956"/>
+<polygon fill="#000000" stroke="#000000" points="215.7151,-40.9698 222.9848,-33.2625 212.4955,-34.7541 215.7151,-40.9698"/>
+</g>
+<!-- curl3 -->
+<g id="node13" class="node">
+<title>curl3</title>
+<polygon fill="none" stroke="#000000" points="279.4498,-108 225.4498,-108 225.4498,-72 279.4498,-72 279.4498,-108"/>
+<text text-anchor="middle" x="252.4498" y="-86.3" font-family="Times,serif" font-size="14.00" fill="#000000">curl</text>
+</g>
+<!-- curl3&#45;&gt;registry -->
+<g id="edge28" class="edge">
+<title>curl3&#45;&gt;registry</title>
+<path fill="none" stroke="#000000" d="M252.4498,-71.8314C252.4498,-64.131 252.4498,-54.9743 252.4498,-46.4166"/>
+<polygon fill="#000000" stroke="#000000" points="255.9499,-46.4132 252.4498,-36.4133 248.9499,-46.4133 255.9499,-46.4132"/>
+</g>
+<!-- layer_size -->
+<g id="node21" class="node">
+<title>layer_size</title>
+<ellipse fill="none" stroke="#000000" cx="438.4498" cy="-242.8701" rx="44.393" ry="18"/>
+<text text-anchor="middle" x="438.4498" y="-239.1701" font-family="Times,serif" font-size="14.00" fill="#000000">layer size</text>
+</g>
+<!-- wc&#45;&gt;layer_size -->
+<g id="edge12" class="edge">
+<title>wc&#45;&gt;layer_size</title>
+<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M426.8536,-305.3894C428.7463,-295.1851 431.1411,-282.274 433.2623,-270.8377"/>
+<polygon fill="#000000" stroke="#000000" points="436.7287,-271.3402 435.1112,-260.8695 429.8461,-270.0635 436.7287,-271.3402"/>
+</g>
+<!-- config_size -->
+<g id="node20" class="node">
+<title>config_size</title>
+<ellipse fill="none" stroke="#000000" cx="63.4498" cy="-242.8701" rx="49.2915" ry="18"/>
+<text text-anchor="middle" x="63.4498" y="-239.1701" font-family="Times,serif" font-size="14.00" fill="#000000">config size</text>
+</g>
+<!-- wc2&#45;&gt;config_size -->
+<g id="edge22" class="edge">
+<title>wc2&#45;&gt;config_size</title>
+<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M72.7268,-305.3894C71.2127,-295.1851 69.2968,-282.274 67.5998,-270.8377"/>
+<polygon fill="#000000" stroke="#000000" points="71.0507,-270.2475 66.1207,-260.8695 64.1265,-271.275 71.0507,-270.2475"/>
+</g>
+<!-- config&#45;&gt;tee3 -->
+<g id="edge17" class="edge">
+<title>config&#45;&gt;tee3</title>
+<path fill="none" stroke="#000000" d="M162.6553,-449.5715C160.6009,-441.7865 158.1538,-432.513 155.8743,-423.8748"/>
+<polygon fill="#000000" stroke="#000000" points="159.2447,-422.9293 153.3089,-414.1534 152.4764,-424.7155 159.2447,-422.9293"/>
+</g>
+<!-- layer&#45;&gt;tee2 -->
+<g id="edge8" class="edge">
+<title>layer&#45;&gt;tee2</title>
+<path fill="none" stroke="#000000" d="M357.4324,-534.754C361.9143,-507.6851 370.4272,-456.272 375.7497,-424.1265"/>
+<polygon fill="#000000" stroke="#000000" points="379.2414,-424.4641 377.422,-414.0267 372.3354,-423.3206 379.2414,-424.4641"/>
+</g>
+<!-- manifest -->
+<g id="node18" class="node">
+<title>manifest</title>
+<g id="a_node18"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/manifest.md" xlink:title="manifest">
+<polygon fill="none" stroke="#000000" points="278.4498,-180 220.4498,-180 220.4498,-144 284.4498,-144 284.4498,-174 278.4498,-180"/>
+<polyline fill="none" stroke="#000000" points="278.4498,-180 278.4498,-174 "/>
+<polyline fill="none" stroke="#000000" points="284.4498,-174 278.4498,-174 "/>
+<text text-anchor="middle" x="252.4498" y="-158.3" font-family="Times,serif" font-size="14.00" fill="#000000">manifest</text>
+</a>
+</g>
+</g>
+<!-- manifest&#45;&gt;curl3 -->
+<g id="edge27" class="edge">
+<title>manifest&#45;&gt;curl3</title>
+<path fill="none" stroke="#000000" d="M252.4498,-143.8314C252.4498,-136.131 252.4498,-126.9743 252.4498,-118.4166"/>
+<polygon fill="#000000" stroke="#000000" points="255.9499,-118.4132 252.4498,-108.4133 248.9499,-118.4133 255.9499,-118.4132"/>
+</g>
+<!-- config_size&#45;&gt;manifest -->
+<g id="edge25" class="edge">
+<title>config_size&#45;&gt;manifest</title>
+<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M93.7415,-228.5389C102.634,-224.4127 112.4011,-219.9589 121.4498,-216 151.126,-203.0165 184.9324,-189.1026 210.7685,-178.6506"/>
+<polygon fill="#000000" stroke="#000000" points="212.2908,-181.8106 220.255,-174.8234 209.6718,-175.319 212.2908,-181.8106"/>
+</g>
+<!-- layer_size&#45;&gt;manifest -->
+<g id="edge13" class="edge">
+<title>layer_size&#45;&gt;manifest</title>
+<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M411.4267,-228.6221C403.1596,-224.412 393.9931,-219.8918 385.4498,-216 355.1525,-202.1984 320.2569,-188.0959 293.8131,-177.765"/>
+<polygon fill="#000000" stroke="#000000" points="295.0659,-174.4969 284.4772,-174.1371 292.5304,-181.0216 295.0659,-174.4969"/>
+</g>
+<!-- config_digest&#45;&gt;manifest -->
+<g id="edge24" class="edge">
+<title>config_digest&#45;&gt;manifest</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M211.4342,-217.2823C218.2685,-208.0708 225.9885,-197.6656 232.8723,-188.3872"/>
+<polygon fill="#000000" stroke="#000000" points="235.7622,-190.3661 238.9098,-180.2497 230.1405,-186.1952 235.7622,-190.3661"/>
+</g>
+<!-- layer_digest&#45;&gt;manifest -->
+<g id="edge26" class="edge">
+<title>layer_digest&#45;&gt;manifest</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M308.8269,-225.3225C299.0559,-214.3477 286.2986,-200.0188 275.3913,-187.7678"/>
+<polygon fill="#000000" stroke="#000000" points="277.8045,-185.2148 268.5408,-180.0733 272.5764,-189.8695 277.8045,-185.2148"/>
+</g>
+<!-- diffid&#45;&gt;config -->
+<g id="edge16" class="edge">
+<title>diffid&#45;&gt;config</title>
+<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M167.4498,-534.735C167.4498,-523.462 167.4498,-508.7054 167.4498,-495.9117"/>
+<polygon fill="#000000" stroke="#000000" points="170.9499,-495.7461 167.4498,-485.7461 163.9499,-495.7462 170.9499,-495.7461"/>
+</g>
+</g>
+</svg>
diff --git a/internal/and/and_closer.go b/internal/and/and_closer.go
new file mode 100644
index 0000000..14a05ea
--- /dev/null
+++ b/internal/and/and_closer.go
@@ -0,0 +1,48 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package and provides helpers for adding Close to io.{Reader|Writer}.
+package and
+
+import (
+ "io"
+)
+
+// ReadCloser implements io.ReadCloser by reading from a particular io.Reader
+// and then calling the provided "Close()" method.
+type ReadCloser struct {
+ io.Reader
+ CloseFunc func() error
+}
+
+var _ io.ReadCloser = (*ReadCloser)(nil)
+
+// Close implements io.ReadCloser
+func (rac *ReadCloser) Close() error {
+ return rac.CloseFunc()
+}
+
+// WriteCloser implements io.WriteCloser by reading from a particular io.Writer
+// and then calling the provided "Close()" method.
+type WriteCloser struct {
+ io.Writer
+ CloseFunc func() error
+}
+
+var _ io.WriteCloser = (*WriteCloser)(nil)
+
+// Close implements io.WriteCloser
+func (wac *WriteCloser) Close() error {
+ return wac.CloseFunc()
+}
diff --git a/internal/and/and_closer_test.go b/internal/and/and_closer_test.go
new file mode 100644
index 0000000..947ceae
--- /dev/null
+++ b/internal/and/and_closer_test.go
@@ -0,0 +1,85 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package and
+
+import (
+ "bytes"
+ "io"
+ "testing"
+)
+
+func TestRead(t *testing.T) {
+ want := "asdf"
+ r := bytes.NewBufferString(want)
+ called := false
+
+ rac := &ReadCloser{
+ Reader: r,
+ CloseFunc: func() error {
+ called = true
+ return nil
+ },
+ }
+
+ data, err := io.ReadAll(rac)
+ if err != nil {
+ t.Errorf("ReadAll(rac) = %v", err)
+ }
+ if got := string(data); got != want {
+ t.Errorf("ReadAll(rac); got %q, want %q", got, want)
+ }
+
+ if called {
+ t.Error("called before Close(); got true, wanted false")
+ }
+ if err := rac.Close(); err != nil {
+ t.Errorf("Close() = %v", err)
+ }
+ if !called {
+ t.Error("called after Close(); got false, wanted true")
+ }
+}
+
+func TestWrite(t *testing.T) {
+ w := bytes.NewBuffer([]byte{})
+ called := false
+
+ wac := &WriteCloser{
+ Writer: w,
+ CloseFunc: func() error {
+ called = true
+ return nil
+ },
+ }
+
+ want := "asdf"
+ if _, err := wac.Write([]byte(want)); err != nil {
+ t.Errorf("Write(%q); = %v", want, err)
+ }
+
+ if called {
+ t.Error("called before Close(); got true, wanted false")
+ }
+ if err := wac.Close(); err != nil {
+ t.Errorf("Close() = %v", err)
+ }
+ if !called {
+ t.Error("called after Close(); got false, wanted true")
+ }
+
+ if got := w.String(); got != want {
+ t.Errorf("w.String(); got %q, want %q", got, want)
+ }
+}
diff --git a/internal/cmd/edit.go b/internal/cmd/edit.go
new file mode 100644
index 0000000..707e80c
--- /dev/null
+++ b/internal/cmd/edit.go
@@ -0,0 +1,485 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "archive/tar"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/google/go-containerregistry/internal/editor"
+ "github.com/google/go-containerregistry/internal/verify"
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/static"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/spf13/cobra"
+)
+
+// NewCmdEdit creates a new cobra.Command for the edit subcommand.
+//
+// This is currently hidden until we're happy with the interface and can test
+// it on different operating systems and editors.
+func NewCmdEdit(options *[]crane.Option) *cobra.Command {
+ cmd := &cobra.Command{
+ Hidden: true,
+ Use: "edit",
+ Short: "Edit the contents of an image.",
+ Args: cobra.ExactArgs(1),
+ Run: func(cmd *cobra.Command, _ []string) {
+ cmd.Usage()
+ },
+ }
+ cmd.AddCommand(NewCmdEditManifest(options), NewCmdEditConfig(options), NewCmdEditFs(options))
+
+ return cmd
+}
+
+// NewCmdConfig creates a new cobra.Command for the config subcommand.
+func NewCmdEditConfig(options *[]crane.Option) *cobra.Command {
+ var dst string
+ cmd := &cobra.Command{
+ Use: "config",
+ Short: "Edit an image's config file.",
+ Example: ` # Edit ubuntu's config file
+ crane edit config ubuntu
+
+ # Overwrite ubuntu's config file with '{}'
+ echo '{}' | crane edit config ubuntu`,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ref, err := editConfig(cmd.InOrStdin(), cmd.OutOrStdout(), args[0], dst, *options...)
+ if err != nil {
+ return fmt.Errorf("editing config: %w", err)
+ }
+ fmt.Println(ref.String())
+ return nil
+ },
+ }
+ cmd.Flags().StringVarP(&dst, "tag", "t", "", "New tag reference to apply to mutated image. If not provided, uses original tag or pushes a new digest.")
+
+ return cmd
+}
+
+// NewCmdManifest creates a new cobra.Command for the manifest subcommand.
+func NewCmdEditManifest(options *[]crane.Option) *cobra.Command {
+ var (
+ dst string
+ mt string
+ )
+ cmd := &cobra.Command{
+ Use: "manifest",
+ Short: "Edit an image's manifest.",
+ Example: ` # Edit ubuntu's manifest
+ crane edit manifest ubuntu`,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ref, err := editManifest(cmd.InOrStdin(), cmd.OutOrStdout(), args[0], dst, mt, *options...)
+ if err != nil {
+ return fmt.Errorf("editing manifest: %w", err)
+ }
+ fmt.Println(ref.String())
+ return nil
+ },
+ }
+ cmd.Flags().StringVarP(&dst, "tag", "t", "", "New tag reference to apply to mutated image. If not provided, uses original tag or pushes a new digest.")
+ cmd.Flags().StringVarP(&mt, "media-type", "m", "", "Override the mediaType used as the Content-Type for PUT")
+
+ return cmd
+}
+
+// NewCmdExport creates a new cobra.Command for the export subcommand.
+func NewCmdEditFs(options *[]crane.Option) *cobra.Command {
+ var dst, name string
+ cmd := &cobra.Command{
+ Use: "fs IMAGE",
+ Short: "Edit the contents of an image's filesystem.",
+ Example: ` # Edit motd-news using $EDITOR
+ crane edit fs ubuntu -f /etc/default/motd-news
+
+ # Overwrite motd-news with 'ENABLED=0'
+ echo 'ENABLED=0' | crane edit fs ubuntu -f /etc/default/motd-news`,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ref, err := editFile(cmd.InOrStdin(), cmd.OutOrStdout(), args[0], name, dst, *options...)
+ if err != nil {
+ return fmt.Errorf("editing file: %w", err)
+ }
+ fmt.Println(ref.String())
+ return nil
+ },
+ }
+ cmd.Flags().StringVarP(&name, "filename", "f", "", "Edit the given filename")
+ cmd.Flags().StringVarP(&dst, "tag", "t", "", "New tag reference to apply to mutated image. If not provided, uses original tag or pushes a new digest.")
+ cmd.MarkFlagRequired("filename")
+
+ return cmd
+}
+
+func interactive(in io.Reader, out io.Writer) bool {
+ return interactiveFile(in) && interactiveFile(out)
+}
+
+func interactiveFile(i any) bool {
+ f, ok := i.(*os.File)
+ if !ok {
+ return false
+ }
+ stat, err := f.Stat()
+ if err != nil {
+ return false
+ }
+ return (stat.Mode() & os.ModeCharDevice) != 0
+}
+
+func editConfig(in io.Reader, out io.Writer, src, dst string, options ...crane.Option) (name.Reference, error) {
+ o := crane.GetOptions(options...)
+
+ img, err := crane.Pull(src, options...)
+ if err != nil {
+ return nil, err
+ }
+
+ mt, err := img.MediaType()
+ if err != nil {
+ return nil, err
+ }
+
+ // We want to omit Layers in certain situations, so we don't use v1.Image.Manifest() here.
+ // Instead, we treat the manifest as a map[string]any and just manipulate the config desc.
+ mb, err := img.RawManifest()
+ if err != nil {
+ return nil, err
+ }
+
+ jsonMap := map[string]any{}
+ if err := json.Unmarshal(mb, &jsonMap); err != nil {
+ return nil, err
+ }
+
+ cv, ok := jsonMap["config"]
+ if !ok {
+ return nil, fmt.Errorf("config missing")
+ }
+ cb, err := json.Marshal(cv)
+ if err != nil {
+ return nil, fmt.Errorf("json.Marshal config: %w", err)
+ }
+
+ config := v1.Descriptor{}
+ if err := json.Unmarshal(cb, &config); err != nil {
+ return nil, fmt.Errorf("json.Unmarshal config: %w", err)
+ }
+
+ var edited []byte
+ if interactive(in, out) {
+ rcf, err := img.RawConfigFile()
+ if err != nil {
+ return nil, err
+ }
+ edited, err = editor.Edit(bytes.NewReader(rcf), ".json")
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ b, err := io.ReadAll(in)
+ if err != nil {
+ return nil, err
+ }
+ edited = b
+ }
+
+ // this has to happen before we modify the descriptor (so we can use verify.Descriptor to validate whether m.Config.Data matches m.Config.Digest/Size)
+ if config.Data != nil && verify.Descriptor(config) == nil {
+ // https://github.com/google/go-containerregistry/issues/1552#issuecomment-1452653875
+ // "if data is non-empty and correct, we should update it"
+ config.Data = edited
+ }
+
+ l := static.NewLayer(edited, config.MediaType)
+ layerDigest, err := l.Digest()
+ if err != nil {
+ return nil, err
+ }
+
+ config.Digest = layerDigest
+ config.Size = int64(len(edited))
+
+ jsonMap["config"] = config
+ b, err := json.Marshal(jsonMap)
+ if err != nil {
+ return nil, err
+ }
+ rm := &rawManifest{
+ body: b,
+ mediaType: mt,
+ }
+
+ digest, _, _ := v1.SHA256(bytes.NewReader(b))
+
+ if dst == "" {
+ dst = src
+ ref, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := ref.(name.Digest); ok {
+ dst = ref.Context().Digest(digest.String()).String()
+ }
+ }
+
+ dstRef, err := name.ParseReference(dst, o.Name...)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := remote.WriteLayer(dstRef.Context(), l, o.Remote...); err != nil {
+ return nil, err
+ }
+
+ if err := remote.Put(dstRef, rm, o.Remote...); err != nil {
+ return nil, err
+ }
+
+ return dstRef, nil
+}
+
+func editManifest(in io.Reader, out io.Writer, src string, dst string, mt string, options ...crane.Option) (name.Reference, error) {
+ o := crane.GetOptions(options...)
+
+ ref, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ return nil, err
+ }
+
+ desc, err := remote.Get(ref, o.Remote...)
+ if err != nil {
+ return nil, err
+ }
+
+ var edited []byte
+ if interactive(in, out) {
+ edited, err = editor.Edit(bytes.NewReader(desc.Manifest), ".json")
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ b, err := io.ReadAll(in)
+ if err != nil {
+ return nil, err
+ }
+ edited = b
+ }
+
+ digest, _, err := v1.SHA256(bytes.NewReader(edited))
+ if err != nil {
+ return nil, err
+ }
+
+ if dst == "" {
+ dst = src
+ if _, ok := ref.(name.Digest); ok {
+ dst = ref.Context().Digest(digest.String()).String()
+ }
+ }
+ dstRef, err := name.ParseReference(dst, o.Name...)
+ if err != nil {
+ return nil, err
+ }
+
+ if mt == "" {
+ // If --media-type is unset, use Content-Type by default.
+ mt = string(desc.MediaType)
+
+ // If document contains mediaType, default to that.
+ wmt := withMediaType{}
+ if err := json.Unmarshal(edited, &wmt); err == nil {
+ if wmt.MediaType != "" {
+ mt = wmt.MediaType
+ }
+ }
+ }
+
+ rm := &rawManifest{
+ body: edited,
+ mediaType: types.MediaType(mt),
+ }
+
+ if err := remote.Put(dstRef, rm, o.Remote...); err != nil {
+ return nil, err
+ }
+
+ return dstRef, nil
+}
+
+func editFile(in io.Reader, out io.Writer, src, file, dst string, options ...crane.Option) (name.Reference, error) {
+ o := crane.GetOptions(options...)
+
+ img, err := crane.Pull(src, options...)
+ if err != nil {
+ return nil, err
+ }
+
+ // If stdin has content, read it in and use that for the file.
+ // Otherwise, scran through the image and open that file in an editor.
+ var (
+ edited []byte
+ header *tar.Header
+ )
+ if interactive(in, out) {
+ f, h, err := findFile(img, file)
+ if err != nil {
+ return nil, err
+ }
+ ext := filepath.Ext(h.Name)
+ if strings.Contains(ext, "..") {
+ return nil, fmt.Errorf("this is impossible but this check satisfies CWE-22 for file name %q", h.Name)
+ }
+ edited, err = editor.Edit(f, ext)
+ if err != nil {
+ return nil, err
+ }
+ header = h
+ } else {
+ b, err := io.ReadAll(in)
+ if err != nil {
+ return nil, err
+ }
+ edited = b
+ header = blankHeader(file)
+ }
+
+ buf := bytes.NewBuffer(nil)
+ buf.Grow(len(edited))
+ tw := tar.NewWriter(buf)
+
+ header.Size = int64(len(edited))
+ if err := tw.WriteHeader(header); err != nil {
+ return nil, err
+ }
+ if _, err := io.Copy(tw, bytes.NewReader(edited)); err != nil {
+ return nil, err
+ }
+ if err := tw.Close(); err != nil {
+ return nil, err
+ }
+
+ fileBytes := buf.Bytes()
+ fileLayer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewBuffer(fileBytes)), nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ img, err = mutate.Append(img, mutate.Addendum{
+ Layer: fileLayer,
+ History: v1.History{
+ Author: "crane",
+ CreatedBy: strings.Join(os.Args, " "),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ digest, err := img.Digest()
+ if err != nil {
+ return nil, err
+ }
+
+ if dst == "" {
+ dst = src
+ ref, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := ref.(name.Digest); ok {
+ dst = ref.Context().Digest(digest.String()).String()
+ }
+ }
+
+ dstRef, err := name.ParseReference(dst, o.Name...)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := crane.Push(img, dst, options...); err != nil {
+ return nil, err
+ }
+
+ return dstRef, nil
+}
+
+func findFile(img v1.Image, name string) (io.Reader, *tar.Header, error) {
+ name = normalize(name)
+ tr := tar.NewReader(mutate.Extract(img))
+ for {
+ header, err := tr.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return nil, nil, fmt.Errorf("reading tar: %w", err)
+ }
+ if normalize(header.Name) == name {
+ return tr, header, nil
+ }
+ }
+
+ // If we don't find the file, we should create a new one.
+ return bytes.NewBufferString(""), blankHeader(name), nil
+}
+
+func blankHeader(name string) *tar.Header {
+ return &tar.Header{
+ Name: name,
+ Typeflag: tar.TypeReg,
+ // Use a fixed Mode, so that this isn't sensitive to the directory and umask
+ // under which it was created. Additionally, windows can only set 0222,
+ // 0444, or 0666, none of which are executable.
+ Mode: 0555,
+ }
+}
+
+func normalize(name string) string {
+ return filepath.Clean("/" + name)
+}
+
+type withMediaType struct {
+ MediaType string `json:"mediaType,omitempty"`
+}
+
+type rawManifest struct {
+ body []byte
+ mediaType types.MediaType
+}
+
+func (r *rawManifest) RawManifest() ([]byte, error) {
+ return r.body, nil
+}
+
+func (r *rawManifest) MediaType() (types.MediaType, error) {
+ return r.mediaType, nil
+}
diff --git a/internal/cmd/edit_test.go b/internal/cmd/edit_test.go
new file mode 100644
index 0000000..df18c45
--- /dev/null
+++ b/internal/cmd/edit_test.go
@@ -0,0 +1,174 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "bytes"
+ "io"
+ "net/http/httptest"
+ "net/url"
+ "path"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/registry"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+)
+
+func mustRegistry(t *testing.T) (*httptest.Server, string) {
+ t.Helper()
+ s := httptest.NewServer(registry.New())
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return s, u.Host
+}
+
+func TestEditConfig(t *testing.T) {
+ reg, host := mustRegistry(t)
+ defer reg.Close()
+ src := path.Join(host, "crane/edit/config")
+
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := crane.Push(img, src); err != nil {
+ t.Fatal(err)
+ }
+
+ cmd := NewCmdEditConfig(&[]crane.Option{})
+ cmd.SetArgs([]string{src})
+ cmd.SetIn(strings.NewReader("{}"))
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditManifest(t *testing.T) {
+ reg, host := mustRegistry(t)
+ defer reg.Close()
+ src := path.Join(host, "crane/edit/manifest")
+
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := crane.Push(img, src); err != nil {
+ t.Fatal(err)
+ }
+
+ cmd := NewCmdEditManifest(&[]crane.Option{})
+ cmd.SetArgs([]string{src})
+ cmd.SetIn(strings.NewReader("{}"))
+ if err := cmd.Execute(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEditFilesystem(t *testing.T) {
+ reg, host := mustRegistry(t)
+ defer reg.Close()
+ src := path.Join(host, "crane/edit/fs")
+
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := crane.Push(img, src); err != nil {
+ t.Fatal(err)
+ }
+
+ cmd := NewCmdEditFs(&[]crane.Option{})
+ cmd.SetArgs([]string{src})
+ cmd.Flags().Set("filename", "/foo/bar")
+ cmd.SetIn(strings.NewReader("baz"))
+ if err := cmd.Execute(); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err = crane.Pull(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r, _, err := findFile(img, "/foo/bar")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := io.ReadAll(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(got, []byte("baz")) {
+ t.Fatalf("got: %s, want %s", got, "baz")
+ }
+
+ // Edit the same file to make sure we can edit existing files.
+ cmd = NewCmdEditFs(&[]crane.Option{})
+ cmd.SetArgs([]string{src})
+ cmd.Flags().Set("filename", "/foo/bar")
+ cmd.SetIn(strings.NewReader("quux"))
+ if err := cmd.Execute(); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err = crane.Pull(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r, _, err = findFile(img, "/foo/bar")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ got, err = io.ReadAll(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(got, []byte("quux")) {
+ t.Fatalf("got: %s, want %s", got, "quux")
+ }
+}
+
+func TestFindFile(t *testing.T) {
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r, h, err := findFile(img, "/does-not-exist")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ b, err := io.ReadAll(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(b) != 0 {
+ t.Errorf("expected empty reader, got: %s", string(b))
+ }
+
+ if h.Name != "/does-not-exist" {
+ t.Errorf("tar.Header has wrong name: %v", h)
+ }
+}
diff --git a/internal/compare/doc.go b/internal/compare/doc.go
new file mode 100644
index 0000000..c8ca497
--- /dev/null
+++ b/internal/compare/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package compare provides methods for comparing images, indexes, and layers.
+package compare
diff --git a/internal/compare/image.go b/internal/compare/image.go
new file mode 100644
index 0000000..fe5fd4c
--- /dev/null
+++ b/internal/compare/image.go
@@ -0,0 +1,111 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compare
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Images compares the given images to each other and returns an error if they
+// differ.
+func Images(a, b v1.Image) error {
+ digests := []v1.Hash{}
+ manifests := []*v1.Manifest{}
+ cns := []v1.Hash{}
+ sizes := []int64{}
+ mts := []types.MediaType{}
+ layerss := [][]v1.Layer{}
+
+ errs := []string{}
+
+ for _, img := range []v1.Image{a, b} {
+ layers, err := img.Layers()
+ if err != nil {
+ return err
+ }
+ layerss = append(layerss, layers)
+
+ digest, err := img.Digest()
+ if err != nil {
+ return err
+ }
+ digests = append(digests, digest)
+
+ manifest, err := img.Manifest()
+ if err != nil {
+ return err
+ }
+ manifests = append(manifests, manifest)
+
+ cn, err := img.ConfigName()
+ if err != nil {
+ return err
+ }
+ cns = append(cns, cn)
+
+ size, err := img.Size()
+ if err != nil {
+ return err
+ }
+ sizes = append(sizes, size)
+
+ mt, err := img.MediaType()
+ if err != nil {
+ return err
+ }
+ mts = append(mts, mt)
+ }
+
+ if want, got := digests[0], digests[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.Digest() != b.Digest(); %s != %s", want, got))
+ }
+ if want, got := cns[0], cns[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.ConfigName() != b.ConfigName(); %s != %s", want, got))
+ }
+ if want, got := manifests[0], manifests[1]; !reflect.DeepEqual(want, got) {
+ errs = append(errs, fmt.Sprintf("a.Manifest() != b.Manifest(); %v != %v", want, got))
+ }
+ if want, got := sizes[0], sizes[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.Size() != b.Size(); %d != %d", want, got))
+ }
+ if want, got := mts[0], mts[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.MediaType() != b.MediaType(); %s != %s", want, got))
+ }
+
+ if len(layerss[0]) != len(layerss[1]) {
+ // If we have fewer layers than the first image, abort with an error so we don't panic.
+ return errors.New("len(a.Layers()) != len(b.Layers())")
+ }
+
+ // Compare each layer.
+ for i := 0; i < len(layerss[0]); i++ {
+ if err := Layers(layerss[0][i], layerss[1][i]); err != nil {
+ // Wrap the error in newlines to delineate layer errors.
+ errs = append(errs, fmt.Sprintf("Layers[%d]: %v\n", i, err))
+ }
+ }
+
+ if len(errs) != 0 {
+ return errors.New("Images differ:\n" + strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
diff --git a/internal/compare/image_test.go b/internal/compare/image_test.go
new file mode 100644
index 0000000..fe011a4
--- /dev/null
+++ b/internal/compare/image_test.go
@@ -0,0 +1,66 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compare
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestDifferentImages(t *testing.T) {
+ a, err := random.Image(100, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b, err := random.Image(100, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ b = mutate.MediaType(b, types.OCIManifestSchema1)
+
+ if err := Images(a, b); err == nil {
+ t.Errorf("got nil err, should have something")
+ }
+}
+
+func TestMismatchedLayers(t *testing.T) {
+ a, err := random.Image(100, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b, err := random.Image(100, 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Images(a, b); err == nil {
+ t.Errorf("got nil err, should have something")
+ }
+}
+
+func TestEqualImages(t *testing.T) {
+ a, err := random.Image(100, 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Images(a, a); err != nil {
+ t.Errorf("got err: %v", err)
+ }
+}
diff --git a/internal/compare/index.go b/internal/compare/index.go
new file mode 100644
index 0000000..c19620b
--- /dev/null
+++ b/internal/compare/index.go
@@ -0,0 +1,83 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compare
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Indexes compares the given indexes to each other and returns an error if
+// they differ.
+func Indexes(a, b v1.ImageIndex) error {
+ digests := []v1.Hash{}
+ manifests := []*v1.IndexManifest{}
+ sizes := []int64{}
+ mts := []types.MediaType{}
+
+ errs := []string{}
+
+ for _, idx := range []v1.ImageIndex{a, b} {
+ digest, err := idx.Digest()
+ if err != nil {
+ return err
+ }
+ digests = append(digests, digest)
+
+ manifest, err := idx.IndexManifest()
+ if err != nil {
+ return err
+ }
+ manifests = append(manifests, manifest)
+
+ size, err := idx.Size()
+ if err != nil {
+ return err
+ }
+ sizes = append(sizes, size)
+
+ mt, err := idx.MediaType()
+ if err != nil {
+ return err
+ }
+ mts = append(mts, mt)
+ }
+
+ if want, got := digests[0], digests[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.Digest() != b.Digest(); %s != %s", want, got))
+ }
+ if want, got := manifests[0], manifests[1]; !reflect.DeepEqual(want, got) {
+ errs = append(errs, fmt.Sprintf("a.Manifest() != b.Manifest(); %v != %v", want, got))
+ }
+ if want, got := sizes[0], sizes[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.Size() != b.Size(); %d != %d", want, got))
+ }
+ if want, got := mts[0], mts[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.MediaType() != b.MediaType(); %s != %s", want, got))
+ }
+
+ // TODO(jonjohnsonjr): Iterate over Manifest and compare Image and ImageIndex results.
+
+ if len(errs) != 0 {
+ return errors.New("Indexes differ:\n" + strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
diff --git a/internal/compare/index_test.go b/internal/compare/index_test.go
new file mode 100644
index 0000000..962bf22
--- /dev/null
+++ b/internal/compare/index_test.go
@@ -0,0 +1,51 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compare
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestDifferentIndexes(t *testing.T) {
+ a, err := random.Index(100, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b, err := random.Index(100, 2, 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ b = mutate.IndexMediaType(b, types.DockerManifestList)
+
+ if err := Indexes(a, b); err == nil {
+ t.Errorf("got nil err, should have something")
+ }
+}
+
+func TestEqualIndexes(t *testing.T) {
+ a, err := random.Index(100, 2, 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Indexes(a, a); err != nil {
+ t.Errorf("got err: %v", err)
+ }
+}
diff --git a/internal/compare/layer.go b/internal/compare/layer.go
new file mode 100644
index 0000000..01e63aa
--- /dev/null
+++ b/internal/compare/layer.go
@@ -0,0 +1,80 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compare
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Layers compares the given layers to each other and returns an error if they
+// differ. Note that this does not compare the actual contents (by calling
+// Compressed or Uncompressed).
+func Layers(a, b v1.Layer) error {
+ digests := []v1.Hash{}
+ diffids := []v1.Hash{}
+ sizes := []int64{}
+ mts := []types.MediaType{}
+ errs := []string{}
+
+ for _, layer := range []v1.Layer{a, b} {
+ digest, err := layer.Digest()
+ if err != nil {
+ return err
+ }
+ digests = append(digests, digest)
+
+ diffid, err := layer.DiffID()
+ if err != nil {
+ return err
+ }
+ diffids = append(diffids, diffid)
+
+ size, err := layer.Size()
+ if err != nil {
+ return err
+ }
+ sizes = append(sizes, size)
+
+ mt, err := layer.MediaType()
+ if err != nil {
+ return err
+ }
+ mts = append(mts, mt)
+ }
+
+ if want, got := digests[0], digests[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.Digest() != b.Digest(); %s != %s", want, got))
+ }
+ if want, got := diffids[0], diffids[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.DiffID() != b.DiffID(); %s != %s", want, got))
+ }
+ if want, got := sizes[0], sizes[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.Size() != b.Size(); %d != %d", want, got))
+ }
+ if want, got := mts[0], mts[1]; want != got {
+ errs = append(errs, fmt.Sprintf("a.MediaType() != b.MediaType(); %s != %s", want, got))
+ }
+
+ if len(errs) != 0 {
+ return errors.New("Layers differ:\n" + strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
diff --git a/internal/compare/layer_test.go b/internal/compare/layer_test.go
new file mode 100644
index 0000000..6db9751
--- /dev/null
+++ b/internal/compare/layer_test.go
@@ -0,0 +1,48 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compare
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestDifferentLayers(t *testing.T) {
+ a, err := random.Layer(100, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b, err := random.Layer(100, types.OCILayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Layers(a, b); err == nil {
+ t.Errorf("got nil err, should have something")
+ }
+}
+
+func TestEqualLayers(t *testing.T) {
+ a, err := random.Layer(100, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Layers(a, a); err != nil {
+ t.Errorf("got err: %v", err)
+ }
+}
diff --git a/internal/compression/compression.go b/internal/compression/compression.go
new file mode 100644
index 0000000..0124871
--- /dev/null
+++ b/internal/compression/compression.go
@@ -0,0 +1,97 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package compression abstracts over gzip and zstd.
+package compression
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+
+ "github.com/google/go-containerregistry/internal/gzip"
+ "github.com/google/go-containerregistry/internal/zstd"
+ "github.com/google/go-containerregistry/pkg/compression"
+)
+
+// Opener represents e.g. opening a file.
+type Opener = func() (io.ReadCloser, error)
+
+// GetCompression detects whether an Opener is compressed and which algorithm is used.
+func GetCompression(opener Opener) (compression.Compression, error) {
+ rc, err := opener()
+ if err != nil {
+ return compression.None, err
+ }
+ defer rc.Close()
+
+ cp, _, err := PeekCompression(rc)
+ if err != nil {
+ return compression.None, err
+ }
+
+ return cp, nil
+}
+
+// PeekCompression detects whether the input stream is compressed and which algorithm is used.
+//
+// If r implements Peek, we will use that directly, otherwise a small number
+// of bytes are buffered to Peek at the gzip/zstd header, and the returned
+// PeekReader can be used as a replacement for the consumed input io.Reader.
+func PeekCompression(r io.Reader) (compression.Compression, PeekReader, error) {
+ pr := intoPeekReader(r)
+
+ if isGZip, _, err := checkHeader(pr, gzip.MagicHeader); err != nil {
+ return compression.None, pr, err
+ } else if isGZip {
+ return compression.GZip, pr, nil
+ }
+
+ if isZStd, _, err := checkHeader(pr, zstd.MagicHeader); err != nil {
+ return compression.None, pr, err
+ } else if isZStd {
+ return compression.ZStd, pr, nil
+ }
+
+ return compression.None, pr, nil
+}
+
+// PeekReader is an io.Reader that also implements Peek a la bufio.Reader.
+type PeekReader interface {
+ io.Reader
+ Peek(n int) ([]byte, error)
+}
+
+// IntoPeekReader creates a PeekReader from an io.Reader.
+// If the reader already has a Peek method, it will just return the passed reader.
+func intoPeekReader(r io.Reader) PeekReader {
+ if p, ok := r.(PeekReader); ok {
+ return p
+ }
+
+ return bufio.NewReader(r)
+}
+
+// CheckHeader checks whether the first bytes from a PeekReader match an expected header
+func checkHeader(pr PeekReader, expectedHeader []byte) (bool, PeekReader, error) {
+ header, err := pr.Peek(len(expectedHeader))
+ if err != nil {
+ // https://github.com/google/go-containerregistry/issues/367
+ if err == io.EOF {
+ return false, pr, nil
+ }
+ return false, pr, err
+ }
+ return bytes.Equal(header, expectedHeader), pr, nil
+}
diff --git a/internal/compression/compression_test.go b/internal/compression/compression_test.go
new file mode 100644
index 0000000..5279dfe
--- /dev/null
+++ b/internal/compression/compression_test.go
@@ -0,0 +1,78 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compression
+
+import (
+ "bytes"
+ "io"
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/and"
+ "github.com/google/go-containerregistry/internal/gzip"
+ "github.com/google/go-containerregistry/internal/zstd"
+)
+
+type Compressor = func(rc io.ReadCloser) io.ReadCloser
+type Decompressor = func(rc io.ReadCloser) (io.ReadCloser, error)
+
+func testPeekCompression(t *testing.T,
+ compressionExpected string,
+ compress Compressor,
+ decompress Decompressor,
+) {
+ content := "This is the input string."
+ contentBuf := bytes.NewBufferString(content)
+
+ compressed := compress(io.NopCloser(contentBuf))
+ compressionDetected, pr, err := PeekCompression(compressed)
+ if err != nil {
+ t.Error("PeekCompression() =", err)
+ }
+
+ if got := string(compressionDetected); got != compressionExpected {
+ t.Errorf("PeekCompression(); got %q, content %q", got, compressionExpected)
+ }
+
+ decompressed, err := decompress(withCloser(pr, compressed))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ b, err := io.ReadAll(decompressed)
+ if err != nil {
+ t.Error("ReadAll() =", err)
+ }
+
+ if got := string(b); got != content {
+ t.Errorf("ReadAll(); got %q, content %q", got, content)
+ }
+}
+
+func TestPeekCompression(t *testing.T) {
+ testPeekCompression(t, "gzip", gzip.ReadCloser, gzip.UnzipReadCloser)
+ testPeekCompression(t, "zstd", zstd.ReadCloser, zstd.UnzipReadCloser)
+
+ nopCompress := func(rc io.ReadCloser) io.ReadCloser { return rc }
+ nopDecompress := func(rc io.ReadCloser) (io.ReadCloser, error) { return rc, nil }
+
+ testPeekCompression(t, "none", nopCompress, nopDecompress)
+}
+
+func withCloser(pr PeekReader, rc io.ReadCloser) io.ReadCloser {
+ return &and.ReadCloser{
+ Reader: pr,
+ CloseFunc: rc.Close,
+ }
+}
diff --git a/internal/depcheck/depcheck.go b/internal/depcheck/depcheck.go
new file mode 100644
index 0000000..ba24665
--- /dev/null
+++ b/internal/depcheck/depcheck.go
@@ -0,0 +1,186 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package depcheck defines a test utility for ensuring certain packages don't
+// take on heavy dependencies.
+//
+// This is forked from https://pkg.go.dev/knative.dev/pkg/depcheck
+package depcheck
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/go/packages"
+)
+
+type node struct {
+ importpath string
+ consumers map[string]struct{}
+}
+
+type graph map[string]node
+
+func (g graph) contains(name string) bool {
+ _, ok := g[name]
+ return ok
+}
+
+func (g graph) order() []string {
+ order := make(sort.StringSlice, 0, len(g))
+ for k := range g {
+ order = append(order, k)
+ }
+ order.Sort()
+ return order
+}
+
+// path constructs an examplary path that looks something like:
+//
+// knative.dev/pkg/apis/duck
+// knative.dev/pkg/apis # Also: [knative.dev/pkg/kmeta knative.dev/pkg/tracker]
+// k8s.io/api/core/v1
+func (g graph) path(name string) []string {
+ n := g[name]
+ // Base case.
+ if len(n.consumers) == 0 {
+ return []string{name}
+ }
+ // Inductive step.
+ consumers := make(sort.StringSlice, 0, len(n.consumers))
+ for k := range n.consumers {
+ consumers = append(consumers, k)
+ }
+ consumers.Sort()
+ base := g.path(consumers[0])
+ if len(base) > 1 { // Don't decorate the first entry, which is always an entrypoint.
+ if len(consumers) > 1 {
+ // Attach other consumers to the last entry in base.
+ base = append(base[:len(base)-1], fmt.Sprintf("%s # Also: %v", consumers[0], consumers[1:]))
+ }
+ }
+ return append(base, name)
+}
+
+func buildGraph(importpath string, buildFlags ...string) (graph, error) {
+ g := make(graph, 1)
+ pkgs, err := packages.Load(&packages.Config{
+ Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedDeps | packages.NeedModule,
+ BuildFlags: buildFlags,
+ }, importpath)
+ if err != nil {
+ return nil, err
+ }
+ packages.Visit(pkgs, func(pkg *packages.Package) bool {
+ g[pkg.PkgPath] = node{
+ importpath: pkg.PkgPath,
+ consumers: make(map[string]struct{}),
+ }
+ return pkg.Module != nil
+ }, func(pkg *packages.Package) {
+ for _, imp := range pkg.Imports {
+ if _, ok := g[imp.PkgPath]; ok {
+ g[imp.PkgPath].consumers[pkg.PkgPath] = struct{}{}
+ }
+ }
+ })
+ return g, nil
+}
+
+// StdlibPackages returns the list of all standard library packages, including
+// some golang.org/x/ dependencies.
+func StdlibPackages() []string {
+ // pkg/registry is allowed to depend on any stdlib package, so collect
+ // all of those -- this also includes golang.org/x/ packages.
+ pkgs, err := packages.Load(nil, "std")
+ if err != nil {
+ panic(fmt.Sprintf("Loading stdlib packages: %v", err))
+ }
+ pkgnames := make([]string, len(pkgs))
+ for idx, p := range pkgs {
+ pkgnames[idx] = p.PkgPath
+ }
+ return pkgnames
+}
+
+// CheckNoDependency checks that the given import paths (ip) does not
+// depend (transitively) on certain banned imports.
+func CheckNoDependency(ip string, banned []string, buildFlags ...string) error {
+ g, err := buildGraph(ip, buildFlags...)
+ if err != nil {
+ return fmt.Errorf("buildGraph(%q) = %w", ip, err)
+ }
+ for _, dip := range banned {
+ if g.contains(dip) {
+ return fmt.Errorf("%s depends on banned dependency %s\n%s", ip, dip,
+ strings.Join(g.path(dip), "\n"))
+ }
+ }
+ return nil
+}
+
+// AssertNoDependency checks that the given import paths (the keys) do not
+// depend (transitively) on certain banned imports (the values)
+func AssertNoDependency(t *testing.T, banned map[string][]string, buildFlags ...string) {
+ t.Helper()
+ for ip, banned := range banned {
+ t.Run(ip, func(t *testing.T) {
+ if err := CheckNoDependency(ip, banned, buildFlags...); err != nil {
+ t.Error("CheckNoDependency() =", err)
+ }
+ })
+ }
+}
+
+// AssertOnlyDependencies checks that the given import paths (the keys) only
+// depend (transitively) on certain allowed imports (the values).
+// Note: while perhaps counterintuitive we allow the value to be a superset
+// of the actual imports to that folks can use a constant that holds blessed
+// import paths.
+func AssertOnlyDependencies(t *testing.T, allowed map[string][]string, buildFlags ...string) {
+ t.Helper()
+ for ip, allow := range allowed {
+ // Always include our own package in the set of allowed dependencies.
+ allowed := make(map[string]struct{}, len(allow)+1)
+ for _, x := range append(allow, ip) {
+ allowed[x] = struct{}{}
+ }
+ t.Run(ip, func(t *testing.T) {
+ if err := CheckOnlyDependencies(ip, allowed, buildFlags...); err != nil {
+ t.Error("CheckOnlyDependencies() =", err)
+ }
+ })
+ }
+}
+
+// CheckOnlyDependencies checks that the given import path only
+// depends (transitively) on certain allowed imports.
+// Note: while perhaps counterintuitive we allow the value to be a superset
+// of the actual imports to that folks can use a constant that holds blessed
+// import paths.
+func CheckOnlyDependencies(ip string, allowed map[string]struct{}, buildFlags ...string) error {
+ g, err := buildGraph(ip, buildFlags...)
+ if err != nil {
+ return fmt.Errorf("buildGraph(%q) = %w", ip, err)
+ }
+ for _, name := range g.order() {
+ if _, ok := allowed[name]; !ok {
+ return fmt.Errorf("dependency %s of %s is not explicitly allowed\n%s", name, ip,
+ strings.Join(g.path(name), "\n"))
+ }
+ }
+ return nil
+}
diff --git a/internal/editor/editor.go b/internal/editor/editor.go
new file mode 100644
index 0000000..6a70fa0
--- /dev/null
+++ b/internal/editor/editor.go
@@ -0,0 +1,64 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package editor implements a simple interface for interactive file editing.
+// It most likely does not work on windows.
+package editor
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+// Edit opens a temporary file in the default editor (per $EDITOR, falling back
+// to "vi") with the contents of the given io.Reader and a filename ending in
+// the given extension (to give a hint to the editor for syntax highlighting).
+//
+// The contents of the edited file are returned, and the temporary file removed.
+func Edit(input io.Reader, extension string) ([]byte, error) {
+ f, err := os.CreateTemp("", fmt.Sprintf("%s-edit.*.%s", filepath.Base(os.Args[0]), extension))
+ if err != nil {
+ return nil, err
+ }
+ defer os.Remove(f.Name())
+
+ if _, err := io.Copy(f, input); err != nil {
+ return nil, err
+ }
+ f.Close()
+
+ editor := "vi"
+ if env := os.Getenv("EDITOR"); env != "" {
+ editor = env
+ }
+
+ path, err := exec.LookPath(editor)
+ if err != nil {
+ return nil, err
+ }
+
+ cmd := exec.Command(path, f.Name())
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ if err := cmd.Run(); err != nil {
+ return nil, err
+ }
+
+ return os.ReadFile(f.Name())
+}
diff --git a/internal/estargz/estargz.go b/internal/estargz/estargz.go
new file mode 100644
index 0000000..69021bc
--- /dev/null
+++ b/internal/estargz/estargz.go
@@ -0,0 +1,54 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package estargz adapts the containerd estargz package to our abstractions.
+package estargz
+
+import (
+ "bytes"
+ "io"
+
+ "github.com/containerd/stargz-snapshotter/estargz"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// Assert that what we're returning is an io.ReadCloser
+var _ io.ReadCloser = (*estargz.Blob)(nil)
+
+// ReadCloser reads uncompressed tarball input from the io.ReadCloser and
+// returns:
+// - An io.ReadCloser from which compressed data may be read, and
+// - A v1.Hash with the hash of the estargz table of contents, or
+// - An error if the estargz processing encountered a problem.
+//
+// Refer to estargz for the options:
+// https://pkg.go.dev/github.com/containerd/stargz-snapshotter/estargz@v0.4.1#Option
+func ReadCloser(r io.ReadCloser, opts ...estargz.Option) (*estargz.Blob, v1.Hash, error) {
+ defer r.Close()
+
+ // TODO(#876): Avoid buffering into memory.
+ bs, err := io.ReadAll(r)
+ if err != nil {
+ return nil, v1.Hash{}, err
+ }
+ br := bytes.NewReader(bs)
+
+ rc, err := estargz.Build(io.NewSectionReader(br, 0, int64(len(bs))), opts...)
+ if err != nil {
+ return nil, v1.Hash{}, err
+ }
+
+ h, err := v1.NewHash(rc.TOCDigest().String())
+ return rc, h, err
+}
diff --git a/internal/estargz/estargz_test.go b/internal/estargz/estargz_test.go
new file mode 100644
index 0000000..1eb3114
--- /dev/null
+++ b/internal/estargz/estargz_test.go
@@ -0,0 +1,108 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package estargz
+
+import (
+ "archive/tar"
+ "bytes"
+ "fmt"
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/gzip"
+)
+
+func TestReader(t *testing.T) {
+ want := "This is the input string."
+ buf := bytes.NewBuffer(nil)
+ tw := tar.NewWriter(buf)
+
+ if err := tw.WriteHeader(&tar.Header{
+ Name: "foo",
+ Size: int64(len(want)),
+ }); err != nil {
+ t.Fatal("WriteHeader() =", err)
+ }
+ if _, err := tw.Write([]byte(want)); err != nil {
+ t.Fatal("tw.Write() =", err)
+ }
+ tw.Close()
+
+ zipped, _, err := ReadCloser(io.NopCloser(buf))
+ if err != nil {
+ t.Fatal("ReadCloser() =", err)
+ }
+ unzipped, err := gzip.UnzipReadCloser(zipped)
+ if err != nil {
+ t.Error("gzip.UnzipReadCloser() =", err)
+ }
+ defer unzipped.Close()
+
+ found := false
+
+ r := tar.NewReader(unzipped)
+ for {
+ hdr, err := r.Next()
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ t.Fatal("tar.Next() =", err)
+ }
+
+ if hdr.Name != "foo" {
+ continue
+ }
+ found = true
+
+ b, err := io.ReadAll(r)
+ if err != nil {
+ t.Error("ReadAll() =", err)
+ }
+ if got := string(b); got != want {
+ t.Errorf("ReadAll(); got %q, want %q", got, want)
+ }
+ if err := unzipped.Close(); err != nil {
+ t.Error("Close() =", err)
+ }
+ }
+
+ if !found {
+ t.Error(`Did not find the expected file "foo"`)
+ }
+}
+
+var (
+ errRead = fmt.Errorf("Read failed")
+)
+
+type failReader struct{}
+
+func (f failReader) Read(_ []byte) (int, error) {
+ return 0, errRead
+}
+
+func TestReadErrors(t *testing.T) {
+ fr := failReader{}
+
+ if _, _, err := ReadCloser(io.NopCloser(fr)); err != errRead {
+ t.Error("ReadCloser: expected errRead, got", err)
+ }
+
+ buf := bytes.NewBufferString("not a tarball")
+ if _, _, err := ReadCloser(io.NopCloser(buf)); !strings.Contains(err.Error(), "failed to parse tar file") {
+ t.Error(`ReadCloser: expected "failed to parse tar file", got`, err)
+ }
+}
diff --git a/internal/gzip/zip.go b/internal/gzip/zip.go
new file mode 100644
index 0000000..018c0f8
--- /dev/null
+++ b/internal/gzip/zip.go
@@ -0,0 +1,118 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package gzip provides helper functions for interacting with gzipped streams.
+package gzip
+
+import (
+ "bufio"
+ "bytes"
+ "compress/gzip"
+ "io"
+
+ "github.com/google/go-containerregistry/internal/and"
+)
+
+// MagicHeader is the start of gzip files.
+var MagicHeader = []byte{'\x1f', '\x8b'}
+
+// ReadCloser reads uncompressed input data from the io.ReadCloser and
+// returns an io.ReadCloser from which compressed data may be read.
+// This uses gzip.BestSpeed for the compression level.
+func ReadCloser(r io.ReadCloser) io.ReadCloser {
+ return ReadCloserLevel(r, gzip.BestSpeed)
+}
+
+// ReadCloserLevel reads uncompressed input data from the io.ReadCloser and
+// returns an io.ReadCloser from which compressed data may be read.
+// Refer to compress/gzip for the level:
+// https://golang.org/pkg/compress/gzip/#pkg-constants
+func ReadCloserLevel(r io.ReadCloser, level int) io.ReadCloser {
+ pr, pw := io.Pipe()
+
+ // For highly compressible layers, gzip.Writer will output a very small
+ // number of bytes per Write(). This is normally fine, but when pushing
+ // to a registry, we want to ensure that we're taking full advantage of
+ // the available bandwidth instead of sending tons of tiny writes over
+ // the wire.
+ // 64K ought to be small enough for anybody.
+ bw := bufio.NewWriterSize(pw, 2<<16)
+
+ // Returns err so we can pw.CloseWithError(err)
+ go func() error {
+ // TODO(go1.14): Just defer {pw,gw,r}.Close like you'd expect.
+ // Context: https://golang.org/issue/24283
+ gw, err := gzip.NewWriterLevel(bw, level)
+ if err != nil {
+ return pw.CloseWithError(err)
+ }
+
+ if _, err := io.Copy(gw, r); err != nil {
+ defer r.Close()
+ defer gw.Close()
+ return pw.CloseWithError(err)
+ }
+
+ // Close gzip writer to Flush it and write gzip trailers.
+ if err := gw.Close(); err != nil {
+ return pw.CloseWithError(err)
+ }
+
+ // Flush bufio writer to ensure we write out everything.
+ if err := bw.Flush(); err != nil {
+ return pw.CloseWithError(err)
+ }
+
+ // We don't really care if these fail.
+ defer pw.Close()
+ defer r.Close()
+
+ return nil
+ }()
+
+ return pr
+}
+
+// UnzipReadCloser reads compressed input data from the io.ReadCloser and
+// returns an io.ReadCloser from which uncompressed data may be read.
+func UnzipReadCloser(r io.ReadCloser) (io.ReadCloser, error) {
+ gr, err := gzip.NewReader(r)
+ if err != nil {
+ return nil, err
+ }
+ return &and.ReadCloser{
+ Reader: gr,
+ CloseFunc: func() error {
+ // If the unzip fails, then this seems to return the same
+ // error as the read. We don't want this to interfere with
+ // us closing the main ReadCloser, since this could leave
+ // an open file descriptor (fails on Windows).
+ gr.Close()
+ return r.Close()
+ },
+ }, nil
+}
+
+// Is detects whether the input stream is compressed.
+func Is(r io.Reader) (bool, error) {
+ magicHeader := make([]byte, 2)
+ n, err := r.Read(magicHeader)
+ if n == 0 && err == io.EOF {
+ return false, nil
+ }
+ if err != nil {
+ return false, err
+ }
+ return bytes.Equal(magicHeader, MagicHeader), nil
+}
diff --git a/internal/gzip/zip_test.go b/internal/gzip/zip_test.go
new file mode 100644
index 0000000..d8c27f6
--- /dev/null
+++ b/internal/gzip/zip_test.go
@@ -0,0 +1,98 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gzip
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "strings"
+ "testing"
+)
+
+func TestReader(t *testing.T) {
+ want := "This is the input string."
+ buf := bytes.NewBufferString(want)
+ zipped := ReadCloser(io.NopCloser(buf))
+ unzipped, err := UnzipReadCloser(zipped)
+ if err != nil {
+ t.Error("UnzipReadCloser() =", err)
+ }
+
+ b, err := io.ReadAll(unzipped)
+ if err != nil {
+ t.Error("ReadAll() =", err)
+ }
+ if got := string(b); got != want {
+ t.Errorf("ReadAll(); got %q, want %q", got, want)
+ }
+ if err := unzipped.Close(); err != nil {
+ t.Error("Close() =", err)
+ }
+}
+
+func TestIs(t *testing.T) {
+ tests := []struct {
+ in []byte
+ out bool
+ err error
+ }{
+ {[]byte{}, false, nil},
+ {[]byte{'\x00', '\x00', '\x00'}, false, nil},
+ {[]byte{'\x1f', '\x8b', '\x1b'}, true, nil},
+ }
+ for _, test := range tests {
+ reader := bytes.NewReader(test.in)
+ got, err := Is(reader)
+ if got != test.out {
+ t.Errorf("Is; n: got %v, wanted %v\n", got, test.out)
+ }
+ if err != test.err {
+ t.Errorf("Is; err: got %v, wanted %v\n", err, test.err)
+ }
+ }
+}
+
+var (
+ errRead = fmt.Errorf("Read failed")
+)
+
+type failReader struct{}
+
+func (f failReader) Read(_ []byte) (int, error) {
+ return 0, errRead
+}
+
+func TestReadErrors(t *testing.T) {
+ fr := failReader{}
+ if _, err := Is(fr); err != errRead {
+ t.Error("Is: expected errRead, got", err)
+ }
+
+ frc := io.NopCloser(fr)
+ if _, err := UnzipReadCloser(frc); err != errRead {
+ t.Error("UnzipReadCloser: expected errRead, got", err)
+ }
+
+ zr := ReadCloser(io.NopCloser(fr))
+ if _, err := zr.Read(nil); err != errRead {
+ t.Error("ReadCloser: expected errRead, got", err)
+ }
+
+ zr = ReadCloserLevel(io.NopCloser(strings.NewReader("zip me")), -10)
+ if _, err := zr.Read(nil); err == nil {
+ t.Error("Expected invalid level error, got:", err)
+ }
+}
diff --git a/internal/httptest/httptest.go b/internal/httptest/httptest.go
new file mode 100644
index 0000000..85b1719
--- /dev/null
+++ b/internal/httptest/httptest.go
@@ -0,0 +1,104 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package httptest provides a method for testing a TLS server a la net/http/httptest.
+package httptest
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/pem"
+ "math/big"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "time"
+)
+
+// NewTLSServer returns an httptest server, with an http client that has been configured to
+// send all requests to the returned server. The TLS certs are generated for the given domain.
+// If you need a transport, Client().Transport is correctly configured.
+func NewTLSServer(domain string, handler http.Handler) (*httptest.Server, error) {
+ s := httptest.NewUnstartedServer(handler)
+
+ template := x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ NotBefore: time.Now().Add(-1 * time.Hour),
+ NotAfter: time.Now().Add(time.Hour),
+ IPAddresses: []net.IP{
+ net.IPv4(127, 0, 0, 1),
+ net.IPv6loopback,
+ },
+ DNSNames: []string{domain},
+
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ BasicConstraintsValid: true,
+ IsCA: true,
+ }
+
+ priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+ if err != nil {
+ return nil, err
+ }
+
+ b, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
+ if err != nil {
+ return nil, err
+ }
+
+ pc := &bytes.Buffer{}
+ if err := pem.Encode(pc, &pem.Block{Type: "CERTIFICATE", Bytes: b}); err != nil {
+ return nil, err
+ }
+
+ ek, err := x509.MarshalECPrivateKey(priv)
+ if err != nil {
+ return nil, err
+ }
+
+ pk := &bytes.Buffer{}
+ if err := pem.Encode(pk, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ek}); err != nil {
+ return nil, err
+ }
+
+ c, err := tls.X509KeyPair(pc.Bytes(), pk.Bytes())
+ if err != nil {
+ return nil, err
+ }
+ s.TLS = &tls.Config{
+ Certificates: []tls.Certificate{c},
+ }
+ s.StartTLS()
+
+ certpool := x509.NewCertPool()
+ certpool.AddCert(s.Certificate())
+
+ t := &http.Transport{
+ TLSClientConfig: &tls.Config{
+ RootCAs: certpool,
+ },
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return net.Dial(s.Listener.Addr().Network(), s.Listener.Addr().String())
+ },
+ }
+ s.Client().Transport = t
+
+ return s, nil
+}
diff --git a/internal/legacy/copy.go b/internal/legacy/copy.go
new file mode 100644
index 0000000..10467ba
--- /dev/null
+++ b/internal/legacy/copy.go
@@ -0,0 +1,57 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package legacy provides methods for interacting with legacy image formats.
+package legacy
+
+import (
+ "bytes"
+ "encoding/json"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// CopySchema1 allows `[g]crane cp` to work with old images without adding
+// full support for schema 1 images to this package.
+func CopySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference, opts ...remote.Option) error {
+ m := schema1{}
+ if err := json.NewDecoder(bytes.NewReader(desc.Manifest)).Decode(&m); err != nil {
+ return err
+ }
+
+ for _, layer := range m.FSLayers {
+ src := srcRef.Context().Digest(layer.BlobSum)
+ dst := dstRef.Context().Digest(layer.BlobSum)
+
+ blob, err := remote.Layer(src, opts...)
+ if err != nil {
+ return err
+ }
+
+ if err := remote.WriteLayer(dst.Context(), blob, opts...); err != nil {
+ return err
+ }
+ }
+
+ return remote.Put(dstRef, desc, opts...)
+}
+
+type fslayer struct {
+ BlobSum string `json:"blobSum"`
+}
+
+type schema1 struct {
+ FSLayers []fslayer `json:"fsLayers"`
+}
diff --git a/internal/legacy/copy_test.go b/internal/legacy/copy_test.go
new file mode 100644
index 0000000..b8ca799
--- /dev/null
+++ b/internal/legacy/copy_test.go
@@ -0,0 +1,97 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package legacy
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestCopySchema1(t *testing.T) {
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // We'll copy from src to dst.
+ src := fmt.Sprintf("%s/schema1/src", u.Host)
+ srcRef, err := name.ParseReference(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/schema1/dst", u.Host)
+ dstRef, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Create a random layer.
+ layer, err := random.Layer(1024, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest, err := layer.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ layerRef, err := name.NewDigest(fmt.Sprintf("%s@%s", src, digest))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Populate the registry with a layer and a schema 1 manifest referencing it.
+ if err := remote.WriteLayer(layerRef.Context(), layer); err != nil {
+ t.Fatal(err)
+ }
+ manifest := schema1{
+ FSLayers: []fslayer{{
+ BlobSum: digest.String(),
+ }},
+ }
+ b, err := json.Marshal(manifest)
+ if err != nil {
+ t.Fatal(err)
+ }
+ desc := &remote.Descriptor{
+ Manifest: b,
+ Descriptor: v1.Descriptor{
+ MediaType: types.DockerManifestSchema1,
+ Digest: v1.Hash{Algorithm: "sha256",
+ Hex: strings.Repeat("a", 64),
+ },
+ },
+ }
+ if err := remote.Put(dstRef, desc); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := CopySchema1(desc, srcRef, dstRef); err != nil {
+ t.Errorf("failed to copy schema 1: %v", err)
+ }
+}
diff --git a/internal/redact/redact.go b/internal/redact/redact.go
new file mode 100644
index 0000000..b2e3f18
--- /dev/null
+++ b/internal/redact/redact.go
@@ -0,0 +1,89 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package redact contains a simple context signal for redacting requests.
+package redact
+
+import (
+ "context"
+ "errors"
+ "net/url"
+)
+
+type contextKey string
+
+var redactKey = contextKey("redact")
+
+// NewContext creates a new ctx with the reason for redaction.
+func NewContext(ctx context.Context, reason string) context.Context {
+ return context.WithValue(ctx, redactKey, reason)
+}
+
+// FromContext returns the redaction reason, if any.
+func FromContext(ctx context.Context) (bool, string) {
+ reason, ok := ctx.Value(redactKey).(string)
+ return ok, reason
+}
+
+// Error redacts potentially sensitive query parameter values in the URL from the error's message.
+//
+// If the error is a *url.Error, this returns a *url.Error with the URL redacted.
+// Any other error type, or nil, is returned unchanged.
+func Error(err error) error {
+ // If the error is a url.Error, we can redact the URL.
+ // Otherwise (including if err is nil), we can't redact.
+ var uerr *url.Error
+ if ok := errors.As(err, &uerr); !ok {
+ return err
+ }
+ u, perr := url.Parse(uerr.URL)
+ if perr != nil {
+ return err // If the URL can't be parsed, just return the original error.
+ }
+ uerr.URL = URL(u).String() // Update the URL to the redacted URL.
+ return uerr
+}
+
+// The set of query string keys that we expect to send as part of the registry
+// protocol. Anything else is potentially dangerous to leak, as it's probably
+// from a redirect. These redirects often included tokens or signed URLs.
+var paramAllowlist = map[string]struct{}{
+ // Token exchange
+ "scope": {},
+ "service": {},
+ // Cross-repo mounting
+ "mount": {},
+ "from": {},
+ // Layer PUT
+ "digest": {},
+ // Listing tags and catalog
+ "n": {},
+ "last": {},
+}
+
+// URL redacts potentially sensitive query parameter values from the URL's query string.
+func URL(u *url.URL) *url.URL {
+ qs := u.Query()
+ for k, v := range qs {
+ for i := range v {
+ if _, ok := paramAllowlist[k]; !ok {
+ // key is not in the Allowlist
+ v[i] = "REDACTED"
+ }
+ }
+ }
+ r := *u
+ r.RawQuery = qs.Encode()
+ return &r
+}
diff --git a/internal/retry/retry.go b/internal/retry/retry.go
new file mode 100644
index 0000000..c9e3564
--- /dev/null
+++ b/internal/retry/retry.go
@@ -0,0 +1,94 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package retry provides methods for retrying operations. It is a thin wrapper
+// around k8s.io/apimachinery/pkg/util/wait to make certain operations easier.
+package retry
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/google/go-containerregistry/internal/retry/wait"
+)
+
+// Backoff is an alias of our own wait.Backoff to avoid name conflicts with
+// the kubernetes wait package. Typing retry.Backoff is aesier than fixing
+// the wrong import every time you use wait.Backoff.
+type Backoff = wait.Backoff
+
+// This is implemented by several errors in the net package as well as our
+// transport.Error.
+type temporary interface {
+ Temporary() bool
+}
+
+// IsTemporary returns true if err implements Temporary() and it returns true.
+func IsTemporary(err error) bool {
+ if errors.Is(err, context.DeadlineExceeded) {
+ return false
+ }
+ if te, ok := err.(temporary); ok && te.Temporary() {
+ return true
+ }
+ return false
+}
+
+// IsNotNil returns true if err is not nil.
+func IsNotNil(err error) bool {
+ return err != nil
+}
+
+// Predicate determines whether an error should be retried.
+type Predicate func(error) (retry bool)
+
+// Retry retries a given function, f, until a predicate is satisfied, using
+// exponential backoff. If the predicate is never satisfied, it will return the
+// last error returned by f.
+func Retry(f func() error, p Predicate, backoff wait.Backoff) (err error) {
+ if f == nil {
+ return fmt.Errorf("nil f passed to retry")
+ }
+ if p == nil {
+ return fmt.Errorf("nil p passed to retry")
+ }
+
+ condition := func() (bool, error) {
+ err = f()
+ if p(err) {
+ return false, nil
+ }
+ return true, err
+ }
+
+ wait.ExponentialBackoff(backoff, condition)
+ return
+}
+
+type contextKey string
+
+var key = contextKey("never")
+
+// Never returns a context that signals something should not be retried.
+// This is a hack and can be used to communicate across package boundaries
+// to avoid retry amplification.
+func Never(ctx context.Context) context.Context {
+ return context.WithValue(ctx, key, true)
+}
+
+// Ever returns true if the context was wrapped by Never.
+func Ever(ctx context.Context) bool {
+ return ctx.Value(key) == nil
+}
diff --git a/internal/retry/retry_test.go b/internal/retry/retry_test.go
new file mode 100644
index 0000000..2091ca5
--- /dev/null
+++ b/internal/retry/retry_test.go
@@ -0,0 +1,100 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package retry
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+)
+
+type temp struct{}
+
+func (e temp) Error() string {
+ return "temporary error"
+}
+
+func (e temp) Temporary() bool {
+ return true
+}
+
+func TestRetry(t *testing.T) {
+ for i, test := range []struct {
+ predicate Predicate
+ err error
+ shouldRetry bool
+ }{{
+ predicate: IsTemporary,
+ err: nil,
+ shouldRetry: false,
+ }, {
+ predicate: IsTemporary,
+ err: fmt.Errorf("not temporary"),
+ shouldRetry: false,
+ }, {
+ predicate: IsNotNil,
+ err: fmt.Errorf("not temporary"),
+ shouldRetry: true,
+ }, {
+ predicate: IsTemporary,
+ err: temp{},
+ shouldRetry: true,
+ }, {
+ predicate: IsTemporary,
+ err: context.DeadlineExceeded,
+ shouldRetry: false,
+ }, {
+ predicate: IsTemporary,
+ err: &url.Error{
+ Op: http.MethodPost,
+ URL: "http://127.0.0.1:56520/v2/example/blobs/uploads/",
+ Err: context.DeadlineExceeded,
+ },
+ shouldRetry: false,
+ }} {
+ // Make sure we retry 5 times if we shouldRetry.
+ steps := 5
+ backoff := Backoff{
+ Steps: steps,
+ }
+
+ // Count how many times this function is invoked.
+ count := 0
+ f := func() error {
+ count++
+ return test.err
+ }
+
+ Retry(f, test.predicate, backoff)
+
+ if test.shouldRetry && count != steps {
+ t.Errorf("expected %d to retry %v, did not", i, test.err)
+ } else if !test.shouldRetry && count == steps {
+ t.Errorf("expected %d not to retry %v, but did", i, test.err)
+ }
+ }
+}
+
+// Make sure we don't panic.
+func TestNil(t *testing.T) {
+ if err := Retry(nil, nil, Backoff{}); err == nil {
+ t.Errorf("got nil when passing in nil f")
+ }
+ if err := Retry(func() error { return nil }, nil, Backoff{}); err == nil {
+ t.Errorf("got nil when passing in nil p")
+ }
+}
diff --git a/internal/retry/wait/kubernetes_apimachinery_wait.go b/internal/retry/wait/kubernetes_apimachinery_wait.go
new file mode 100644
index 0000000..ab06e5f
--- /dev/null
+++ b/internal/retry/wait/kubernetes_apimachinery_wait.go
@@ -0,0 +1,123 @@
+/*
+Copyright 2014 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package wait is a subset of k8s.io/apimachinery to avoid conflicts
+// in dependencies (specifically, logging).
+package wait
+
+import (
+ "errors"
+ "math/rand"
+ "time"
+)
+
+// Jitter returns a time.Duration between duration and duration + maxFactor *
+// duration.
+//
+// This allows clients to avoid converging on periodic behavior. If maxFactor
+// is 0.0, a suggested default value will be chosen.
+func Jitter(duration time.Duration, maxFactor float64) time.Duration {
+ if maxFactor <= 0.0 {
+ maxFactor = 1.0
+ }
+ wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration))
+ return wait
+}
+
+// ErrWaitTimeout is returned when the condition exited without success.
+var ErrWaitTimeout = errors.New("timed out waiting for the condition")
+
+// ConditionFunc returns true if the condition is satisfied, or an error
+// if the loop should be aborted.
+type ConditionFunc func() (done bool, err error)
+
+// Backoff holds parameters applied to a Backoff function.
+type Backoff struct {
+ // The initial duration.
+ Duration time.Duration
+ // Duration is multiplied by factor each iteration, if factor is not zero
+ // and the limits imposed by Steps and Cap have not been reached.
+ // Should not be negative.
+ // The jitter does not contribute to the updates to the duration parameter.
+ Factor float64
+ // The sleep at each iteration is the duration plus an additional
+ // amount chosen uniformly at random from the interval between
+ // zero and `jitter*duration`.
+ Jitter float64
+ // The remaining number of iterations in which the duration
+ // parameter may change (but progress can be stopped earlier by
+ // hitting the cap). If not positive, the duration is not
+ // changed. Used for exponential backoff in combination with
+ // Factor and Cap.
+ Steps int
+ // A limit on revised values of the duration parameter. If a
+ // multiplication by the factor parameter would make the duration
+ // exceed the cap then the duration is set to the cap and the
+ // steps parameter is set to zero.
+ Cap time.Duration
+}
+
+// Step (1) returns an amount of time to sleep determined by the
+// original Duration and Jitter and (2) mutates the provided Backoff
+// to update its Steps and Duration.
+func (b *Backoff) Step() time.Duration {
+ if b.Steps < 1 {
+ if b.Jitter > 0 {
+ return Jitter(b.Duration, b.Jitter)
+ }
+ return b.Duration
+ }
+ b.Steps--
+
+ duration := b.Duration
+
+ // calculate the next step
+ if b.Factor != 0 {
+ b.Duration = time.Duration(float64(b.Duration) * b.Factor)
+ if b.Cap > 0 && b.Duration > b.Cap {
+ b.Duration = b.Cap
+ b.Steps = 0
+ }
+ }
+
+ if b.Jitter > 0 {
+ duration = Jitter(duration, b.Jitter)
+ }
+ return duration
+}
+
+// ExponentialBackoff repeats a condition check with exponential backoff.
+//
+// It repeatedly checks the condition and then sleeps, using `backoff.Step()`
+// to determine the length of the sleep and adjust Duration and Steps.
+// Stops and returns as soon as:
+// 1. the condition check returns true or an error,
+// 2. `backoff.Steps` checks of the condition have been done, or
+// 3. a sleep truncated by the cap on duration has been completed.
+// In case (1) the returned error is what the condition function returned.
+// In all other cases, ErrWaitTimeout is returned.
+func ExponentialBackoff(backoff Backoff, condition ConditionFunc) error {
+ for backoff.Steps > 0 {
+ if ok, err := condition(); err != nil || ok {
+ return err
+ }
+ if backoff.Steps == 1 {
+ break
+ }
+ time.Sleep(backoff.Step())
+ }
+ return ErrWaitTimeout
+}
diff --git a/internal/verify/verify.go b/internal/verify/verify.go
new file mode 100644
index 0000000..463f7e4
--- /dev/null
+++ b/internal/verify/verify.go
@@ -0,0 +1,122 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package verify provides a ReadCloser that verifies content matches the
+// expected hash values.
+package verify
+
+import (
+ "bytes"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "hash"
+ "io"
+
+ "github.com/google/go-containerregistry/internal/and"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// SizeUnknown is a sentinel value to indicate that the expected size is not known.
+const SizeUnknown = -1
+
+type verifyReader struct {
+ inner io.Reader
+ hasher hash.Hash
+ expected v1.Hash
+ gotSize, wantSize int64
+}
+
+// Error provides information about the failed hash verification.
+type Error struct {
+ got string
+ want v1.Hash
+ gotSize int64
+}
+
+func (v Error) Error() string {
+ return fmt.Sprintf("error verifying %s checksum after reading %d bytes; got %q, want %q",
+ v.want.Algorithm, v.gotSize, v.got, v.want)
+}
+
+// Read implements io.Reader
+func (vc *verifyReader) Read(b []byte) (int, error) {
+ n, err := vc.inner.Read(b)
+ vc.gotSize += int64(n)
+ if err == io.EOF {
+ if vc.wantSize != SizeUnknown && vc.gotSize != vc.wantSize {
+ return n, fmt.Errorf("error verifying size; got %d, want %d", vc.gotSize, vc.wantSize)
+ }
+ got := hex.EncodeToString(vc.hasher.Sum(nil))
+ if want := vc.expected.Hex; got != want {
+ return n, Error{
+ got: vc.expected.Algorithm + ":" + got,
+ want: vc.expected,
+ gotSize: vc.gotSize,
+ }
+ }
+ }
+ return n, err
+}
+
+// ReadCloser wraps the given io.ReadCloser to verify that its contents match
+// the provided v1.Hash before io.EOF is returned.
+//
+// The reader will only be read up to size bytes, to prevent resource
+// exhaustion. If EOF is returned before size bytes are read, an error is
+// returned.
+//
+// A size of SizeUnknown (-1) indicates disables size verification when the size
+// is unknown ahead of time.
+func ReadCloser(r io.ReadCloser, size int64, h v1.Hash) (io.ReadCloser, error) {
+ w, err := v1.Hasher(h.Algorithm)
+ if err != nil {
+ return nil, err
+ }
+ r2 := io.TeeReader(r, w) // pass all writes to the hasher.
+ if size != SizeUnknown {
+ r2 = io.LimitReader(r2, size) // if we know the size, limit to that size.
+ }
+ return &and.ReadCloser{
+ Reader: &verifyReader{
+ inner: r2,
+ hasher: w,
+ expected: h,
+ wantSize: size,
+ },
+ CloseFunc: r.Close,
+ }, nil
+}
+
+// Descriptor verifies that the embedded Data field matches the Size and Digest
+// fields of the given v1.Descriptor, returning an error if the Data field is
+// missing or if it contains incorrect data.
+func Descriptor(d v1.Descriptor) error {
+ if d.Data == nil {
+ return errors.New("error verifying descriptor; Data == nil")
+ }
+
+ h, sz, err := v1.SHA256(bytes.NewReader(d.Data))
+ if err != nil {
+ return err
+ }
+ if h != d.Digest {
+ return fmt.Errorf("error verifying Digest; got %q, want %q", h, d.Digest)
+ }
+ if sz != d.Size {
+ return fmt.Errorf("error verifying Size; got %d, want %d", sz, d.Size)
+ }
+
+ return nil
+}
diff --git a/internal/verify/verify_test.go b/internal/verify/verify_test.go
new file mode 100644
index 0000000..a2cbbdc
--- /dev/null
+++ b/internal/verify/verify_test.go
@@ -0,0 +1,147 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package verify
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+func mustHash(s string, t *testing.T) v1.Hash {
+ h, _, err := v1.SHA256(strings.NewReader(s))
+ if err != nil {
+ t.Fatalf("v1.SHA256(%s) = %v", s, err)
+ }
+ t.Logf("Hashed: %q -> %q", s, h)
+ return h
+}
+
+func TestVerificationFailure(t *testing.T) {
+ want := "This is the input string."
+ buf := bytes.NewBufferString(want)
+
+ verified, err := ReadCloser(io.NopCloser(buf), int64(len(want)), mustHash("not the same", t))
+ if err != nil {
+ t.Fatal("ReadCloser() =", err)
+ }
+ if b, err := io.ReadAll(verified); err == nil {
+ t.Errorf("ReadAll() = %q; want verification error", string(b))
+ }
+}
+
+func TestVerification(t *testing.T) {
+ want := "This is the input string."
+ buf := bytes.NewBufferString(want)
+
+ verified, err := ReadCloser(io.NopCloser(buf), int64(len(want)), mustHash(want, t))
+ if err != nil {
+ t.Fatal("ReadCloser() =", err)
+ }
+ if _, err := io.ReadAll(verified); err != nil {
+ t.Error("ReadAll() =", err)
+ }
+}
+
+func TestVerificationSizeUnknown(t *testing.T) {
+ want := "This is the input string."
+ buf := bytes.NewBufferString(want)
+
+ verified, err := ReadCloser(io.NopCloser(buf), SizeUnknown, mustHash(want, t))
+ if err != nil {
+ t.Fatal("ReadCloser() =", err)
+ }
+ if _, err := io.ReadAll(verified); err != nil {
+ t.Error("ReadAll() =", err)
+ }
+}
+
+func TestBadHash(t *testing.T) {
+ h := v1.Hash{
+ Algorithm: "fake256",
+ Hex: "whatever",
+ }
+ _, err := ReadCloser(io.NopCloser(strings.NewReader("hi")), 0, h)
+ if err == nil {
+ t.Errorf("ReadCloser() = %v, wanted err", err)
+ }
+}
+
+func TestBadSize(t *testing.T) {
+ want := "This is the input string."
+
+ // having too much content or expecting too much content returns an error.
+ for _, size := range []int64{3, 100} {
+ t.Run(fmt.Sprintf("expecting size %d", size), func(t *testing.T) {
+ buf := bytes.NewBufferString(want)
+ rc, err := ReadCloser(io.NopCloser(buf), size, mustHash(want, t))
+ if err != nil {
+ t.Fatal("ReadCloser() =", err)
+ }
+ if b, err := io.ReadAll(rc); err == nil {
+ t.Errorf("ReadAll() = %q; want verification error", string(b))
+ }
+ })
+ }
+}
+
+func TestDescriptor(t *testing.T) {
+ for _, tc := range []struct {
+ err error
+ desc v1.Descriptor
+ }{{
+ err: errors.New("error verifying descriptor; Data == nil"),
+ }, {
+ err: errors.New(`error verifying Digest; got "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", want ":"`),
+ desc: v1.Descriptor{
+ Data: []byte("abc"),
+ },
+ }, {
+ err: errors.New("error verifying Size; got 3, want 0"),
+ desc: v1.Descriptor{
+ Data: []byte("abc"),
+ Digest: v1.Hash{
+ Algorithm: "sha256",
+ Hex: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ },
+ },
+ }, {
+ desc: v1.Descriptor{
+ Data: []byte("abc"),
+ Size: 3,
+ Digest: v1.Hash{
+ Algorithm: "sha256",
+ Hex: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ },
+ },
+ }} {
+ got, want := Descriptor(tc.desc), tc.err
+
+ if got == nil {
+ if want != nil {
+ t.Errorf("Descriptor(): got nil, want %v", want)
+ }
+ } else if want == nil {
+ t.Errorf("Descriptor(): got %v, want nil", got)
+ } else if got, want := got.Error(), want.Error(); got != want {
+ t.Errorf("Descriptor(): got %q, want %q", got, want)
+ }
+ }
+}
diff --git a/internal/windows/windows.go b/internal/windows/windows.go
new file mode 100644
index 0000000..62d04cf
--- /dev/null
+++ b/internal/windows/windows.go
@@ -0,0 +1,114 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package windows
+
+import (
+ "archive/tar"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "path"
+ "strings"
+
+ "github.com/google/go-containerregistry/internal/gzip"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+// userOwnerAndGroupSID is a magic value needed to make the binary executable
+// in a Windows container.
+//
+// owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU")
+const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA=="
+
+// Windows returns a Layer that is converted to be pullable on Windows.
+func Windows(layer v1.Layer) (v1.Layer, error) {
+ // TODO: do this lazily.
+
+ layerReader, err := layer.Uncompressed()
+ if err != nil {
+ return nil, fmt.Errorf("getting layer: %w", err)
+ }
+ defer layerReader.Close()
+ tarReader := tar.NewReader(layerReader)
+ w := new(bytes.Buffer)
+ tarWriter := tar.NewWriter(w)
+ defer tarWriter.Close()
+
+ for _, dir := range []string{"Files", "Hives"} {
+ if err := tarWriter.WriteHeader(&tar.Header{
+ Name: dir,
+ Typeflag: tar.TypeDir,
+ // Use a fixed Mode, so that this isn't sensitive to the directory and umask
+ // under which it was created. Additionally, windows can only set 0222,
+ // 0444, or 0666, none of which are executable.
+ Mode: 0555,
+ Format: tar.FormatPAX,
+ }); err != nil {
+ return nil, fmt.Errorf("writing %s directory: %w", dir, err)
+ }
+ }
+
+ for {
+ header, err := tarReader.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("reading layer: %w", err)
+ }
+
+ if strings.HasPrefix(header.Name, "Files/") {
+ return nil, fmt.Errorf("file path %q already suitable for Windows", header.Name)
+ }
+
+ header.Name = path.Join("Files", header.Name)
+ header.Format = tar.FormatPAX
+
+ // TODO: this seems to make the file executable on Windows;
+ // only do this if the file should be executable.
+ if header.PAXRecords == nil {
+ header.PAXRecords = map[string]string{}
+ }
+ header.PAXRecords["MSWINDOWS.rawsd"] = userOwnerAndGroupSID
+
+ if err := tarWriter.WriteHeader(header); err != nil {
+ return nil, fmt.Errorf("writing tar header: %w", err)
+ }
+
+ if header.Typeflag == tar.TypeReg {
+ if _, err = io.Copy(tarWriter, tarReader); err != nil {
+ return nil, fmt.Errorf("writing layer file: %w", err)
+ }
+ }
+ }
+
+ if err := tarWriter.Close(); err != nil {
+ return nil, err
+ }
+
+ b := w.Bytes()
+ // gzip the contents, then create the layer
+ opener := func() (io.ReadCloser, error) {
+ return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil
+ }
+ layer, err = tarball.LayerFromOpener(opener)
+ if err != nil {
+ return nil, fmt.Errorf("creating layer: %w", err)
+ }
+
+ return layer, nil
+}
diff --git a/internal/windows/windows_test.go b/internal/windows/windows_test.go
new file mode 100644
index 0000000..3caf54c
--- /dev/null
+++ b/internal/windows/windows_test.go
@@ -0,0 +1,81 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package windows
+
+import (
+ "archive/tar"
+ "errors"
+ "io"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+func TestWindows(t *testing.T) {
+ tarLayer, err := tarball.LayerFromFile("../../pkg/v1/tarball/testdata/content.tar")
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+
+ win, err := Windows(tarLayer)
+ if err != nil {
+ t.Fatalf("Windows: %v", err)
+ }
+ if _, err := Windows(win); err == nil {
+ t.Error("expected an error double-Windowsifying a layer; got nil")
+ }
+
+ rc, err := win.Uncompressed()
+ if err != nil {
+ t.Fatalf("Uncompressed: %v", err)
+ }
+ defer rc.Close()
+ tr := tar.NewReader(rc)
+ var sawHives, sawFiles bool
+ for {
+ h, err := tr.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if h.Name == "Hives" && h.Typeflag == tar.TypeDir {
+ sawHives = true
+ continue
+ }
+ if h.Name == "Files" && h.Typeflag == tar.TypeDir {
+ sawFiles = true
+ continue
+ }
+ if !strings.HasPrefix(h.Name, "Files/") {
+ t.Errorf("tar entry %q didn't have Files prefix", h.Name)
+ }
+ if h.Format != tar.FormatPAX {
+ t.Errorf("tar entry %q had unexpected Format; got %v, want %v", h.Name, h.Format, tar.FormatPAX)
+ }
+ want := map[string]string{
+ "MSWINDOWS.rawsd": userOwnerAndGroupSID,
+ }
+ if !reflect.DeepEqual(h.PAXRecords, want) {
+ t.Errorf("tar entry %q: got %v, want %v", h.Name, h.PAXRecords, want)
+ }
+ }
+ if !sawHives {
+ t.Errorf("didn't see Hives/ directory")
+ }
+ if !sawFiles {
+ t.Errorf("didn't see Files/ directory")
+ }
+}
diff --git a/internal/zstd/zstd.go b/internal/zstd/zstd.go
new file mode 100644
index 0000000..cccf54a
--- /dev/null
+++ b/internal/zstd/zstd.go
@@ -0,0 +1,116 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package zstd provides helper functions for interacting with zstd streams.
+package zstd
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+
+ "github.com/google/go-containerregistry/internal/and"
+ "github.com/klauspost/compress/zstd"
+)
+
+// MagicHeader is the start of zstd files.
+var MagicHeader = []byte{'\x28', '\xb5', '\x2f', '\xfd'}
+
+// ReadCloser reads uncompressed input data from the io.ReadCloser and
+// returns an io.ReadCloser from which compressed data may be read.
+// This uses zstd level 1 for the compression.
+func ReadCloser(r io.ReadCloser) io.ReadCloser {
+ return ReadCloserLevel(r, 1)
+}
+
+// ReadCloserLevel reads uncompressed input data from the io.ReadCloser and
+// returns an io.ReadCloser from which compressed data may be read.
+func ReadCloserLevel(r io.ReadCloser, level int) io.ReadCloser {
+ pr, pw := io.Pipe()
+
+ // For highly compressible layers, zstd.Writer will output a very small
+ // number of bytes per Write(). This is normally fine, but when pushing
+ // to a registry, we want to ensure that we're taking full advantage of
+ // the available bandwidth instead of sending tons of tiny writes over
+ // the wire.
+ // 64K ought to be small enough for anybody.
+ bw := bufio.NewWriterSize(pw, 2<<16)
+
+ // Returns err so we can pw.CloseWithError(err)
+ go func() error {
+ // TODO(go1.14): Just defer {pw,zw,r}.Close like you'd expect.
+ // Context: https://golang.org/issue/24283
+ zw, err := zstd.NewWriter(bw, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(level)))
+ if err != nil {
+ return pw.CloseWithError(err)
+ }
+
+ if _, err := io.Copy(zw, r); err != nil {
+ defer r.Close()
+ defer zw.Close()
+ return pw.CloseWithError(err)
+ }
+
+ // Close zstd writer to Flush it and write zstd trailers.
+ if err := zw.Close(); err != nil {
+ return pw.CloseWithError(err)
+ }
+
+ // Flush bufio writer to ensure we write out everything.
+ if err := bw.Flush(); err != nil {
+ return pw.CloseWithError(err)
+ }
+
+ // We don't really care if these fail.
+ defer pw.Close()
+ defer r.Close()
+
+ return nil
+ }()
+
+ return pr
+}
+
+// UnzipReadCloser reads compressed input data from the io.ReadCloser and
+// returns an io.ReadCloser from which uncompressed data may be read.
+func UnzipReadCloser(r io.ReadCloser) (io.ReadCloser, error) {
+ gr, err := zstd.NewReader(r)
+ if err != nil {
+ return nil, err
+ }
+ return &and.ReadCloser{
+ Reader: gr,
+ CloseFunc: func() error {
+ // If the unzip fails, then this seems to return the same
+ // error as the read. We don't want this to interfere with
+ // us closing the main ReadCloser, since this could leave
+ // an open file descriptor (fails on Windows).
+ gr.Close()
+ return r.Close()
+ },
+ }, nil
+}
+
+// Is detects whether the input stream is compressed.
+func Is(r io.Reader) (bool, error) {
+ magicHeader := make([]byte, 4)
+ n, err := r.Read(magicHeader)
+ if n == 0 && err == io.EOF {
+ return false, nil
+ }
+ if err != nil {
+ return false, err
+ }
+ return bytes.Equal(magicHeader, MagicHeader), nil
+}
diff --git a/internal/zstd/zstd_test.go b/internal/zstd/zstd_test.go
new file mode 100644
index 0000000..c422e27
--- /dev/null
+++ b/internal/zstd/zstd_test.go
@@ -0,0 +1,96 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zstd
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "testing"
+)
+
+func TestReader(t *testing.T) {
+ want := "This is the input string."
+ buf := bytes.NewBufferString(want)
+ zipped := ReadCloser(io.NopCloser(buf))
+ unzipped, err := UnzipReadCloser(zipped)
+ if err != nil {
+ t.Error("UnzipReadCloser() =", err)
+ }
+
+ b, err := io.ReadAll(unzipped)
+ if err != nil {
+ t.Error("ReadAll() =", err)
+ }
+ if got := string(b); got != want {
+ t.Errorf("ReadAll(); got %q, want %q", got, want)
+ }
+ if err := unzipped.Close(); err != nil {
+ t.Error("Close() =", err)
+ }
+}
+
+func TestIs(t *testing.T) {
+ tests := []struct {
+ in []byte
+ out bool
+ err error
+ }{
+ {[]byte{}, false, nil},
+ {[]byte{'\x00', '\x00', '\x00', '\x00', '\x00'}, false, nil},
+ {[]byte{'\x28', '\xb5', '\x2f', '\xfd', '\x1b'}, true, nil},
+ }
+ for _, test := range tests {
+ reader := bytes.NewReader(test.in)
+ got, err := Is(reader)
+ if got != test.out {
+ t.Errorf("Is; n: got %v, wanted %v\n", got, test.out)
+ }
+ if err != test.err {
+ t.Errorf("Is; err: got %v, wanted %v\n", err, test.err)
+ }
+ }
+}
+
+var (
+ errRead = fmt.Errorf("read failed")
+)
+
+type failReader struct{}
+
+func (f failReader) Read(_ []byte) (int, error) {
+ return 0, errRead
+}
+
+func TestReadErrors(t *testing.T) {
+ fr := failReader{}
+ if _, err := Is(fr); err != errRead {
+ t.Error("Is: expected errRead, got", err)
+ }
+
+ frc := io.NopCloser(fr)
+ if r, err := UnzipReadCloser(frc); err != errRead {
+ data := make([]byte, 100)
+ _, err := r.Read(data)
+ if err != errRead {
+ t.Error("UnzipReadCloser: expected errRead, got", err)
+ }
+ }
+
+ zr := ReadCloser(io.NopCloser(fr))
+ if _, err := zr.Read(nil); err != errRead {
+ t.Error("ReadCloser: expected errRead, got", err)
+ }
+}
diff --git a/pkg/authn/README.md b/pkg/authn/README.md
new file mode 100644
index 0000000..042bdde
--- /dev/null
+++ b/pkg/authn/README.md
@@ -0,0 +1,322 @@
+# `authn`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/authn?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/authn)
+
+This README outlines how we acquire and use credentials when interacting with a registry.
+
+As much as possible, we attempt to emulate `docker`'s authentication behavior and configuration so that this library "just works" if you've already configured credentials that work with `docker`; however, when things don't work, a basic understanding of what's going on can help with debugging.
+
+The official documentation for how authentication with `docker` works is (reasonably) scattered across several different sites and GitHub repositories, so we've tried to summarize the relevant bits here.
+
+## tl;dr for consumers of this package
+
+By default, [`pkg/v1/remote`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote) uses [`Anonymous`](https://godoc.org/github.com/google/go-containerregistry/pkg/authn#Anonymous) credentials (i.e. _none_), which for most registries will only allow read access to public images.
+
+To use the credentials found in your Docker config file, you can use the [`DefaultKeychain`](https://godoc.org/github.com/google/go-containerregistry/pkg/authn#DefaultKeychain), e.g.:
+
+```go
+package main
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func main() {
+ ref, err := name.ParseReference("registry.example.com/private/repo")
+ if err != nil {
+ panic(err)
+ }
+
+ // Fetch the manifest using default credentials.
+ img, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
+ if err != nil {
+ panic(err)
+ }
+
+ // Prints the digest of registry.example.com/private/repo
+ fmt.Println(img.Digest)
+}
+```
+
+The `DefaultKeychain` will use credentials as described in your Docker config file -- usually `~/.docker/config.json`, or `%USERPROFILE%\.docker\config.json` on Windows -- or the location described by the `DOCKER_CONFIG` environment variable, if set.
+
+If those are not found, `DefaultKeychain` will look for credentials configured using [Podman's expectation](https://docs.podman.io/en/latest/markdown/podman-login.1.html) that these are found in `${XDG_RUNTIME_DIR}/containers/auth.json`.
+
+[See below](#docker-config-auth) for more information about what is configured in this file.
+
+## Emulating Cloud Provider Credential Helpers
+
+[`pkg/v1/google.Keychain`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/google#Keychain) provides a `Keychain` implementation that emulates [`docker-credential-gcr`](https://github.com/GoogleCloudPlatform/docker-credential-gcr) to find credentials in the environment.
+See [`google.NewEnvAuthenticator`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/google#NewEnvAuthenticator) and [`google.NewGcloudAuthenticator`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/google#NewGcloudAuthenticator) for more information.
+
+To emulate other credential helpers without requiring them to be available as executables, [`NewKeychainFromHelper`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn#NewKeychainFromHelper) provides an adapter that takes a Go implementation satisfying a subset of the [`credentials.Helper`](https://pkg.go.dev/github.com/docker/docker-credential-helpers/credentials#Helper) interface, and makes it available as a `Keychain`.
+
+This means that you can emulate, for example, [Amazon ECR's `docker-credential-ecr-login` credential helper](https://github.com/awslabs/amazon-ecr-credential-helper) using the same implementation:
+
+```go
+import (
+ ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
+ "github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func main() {
+ // ...
+ ecrHelper := ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}
+ img, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.NewKeychainFromHelper(ecrHelper)))
+ if err != nil {
+ panic(err)
+ }
+ // ...
+}
+```
+
+Likewise, you can emulate [Azure's ACR `docker-credential-acr-env` credential helper](https://github.com/chrismellard/docker-credential-acr-env):
+
+```go
+import (
+ "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func main() {
+ // ...
+ acrHelper := credhelper.NewACRCredentialsHelper()
+ img, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.NewKeychainFromHelper(acrHelper)))
+ if err != nil {
+ panic(err)
+ }
+ // ...
+}
+```
+
+<!-- TODO(jasonhall): Wrap these in docker-credential-magic and reference those from here. -->
+
+## Using Multiple `Keychain`s
+
+[`NewMultiKeychain`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn#NewMultiKeychain) allows you to specify multiple `Keychain` implementations, which will be checked in order when credentials are needed.
+
+For example:
+
+```go
+kc := authn.NewMultiKeychain(
+ authn.DefaultKeychain,
+ google.Keychain,
+ authn.NewKeychainFromHelper(ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}),
+ authn.NewKeychainFromHelper(acr.ACRCredHelper{}),
+)
+```
+
+This multi-keychain will:
+
+- first check for credentials found in the Docker config file, as describe above, then
+- check for GCP credentials available in the environment, as described above, then
+- check for ECR credentials by emulating the ECR credential helper, then
+- check for ACR credentials by emulating the ACR credential helper.
+
+If any keychain implementation is able to provide credentials for the request, they will be used, and further keychain implementations will not be consulted.
+
+If no implementations are able to provide credentials, `Anonymous` credentials will be used.
+
+## Docker Config Auth
+
+What follows attempts to gather useful information about Docker's config.json and make it available in one place.
+
+If you have questions, please [file an issue](https://github.com/google/go-containerregistry/issues/new).
+
+### Plaintext
+
+The config file is where your credentials are stored when you invoke `docker login`, e.g. the contents may look something like this:
+
+```json
+{
+ "auths": {
+ "registry.example.com": {
+ "auth": "QXp1cmVEaWFtb25kOmh1bnRlcjI="
+ }
+ }
+}
+```
+
+The `auths` map has an entry per registry, and the `auth` field contains your username and password encoded as [HTTP 'Basic' Auth](https://tools.ietf.org/html/rfc7617).
+
+**NOTE**: This means that your credentials are stored _in plaintext_:
+
+```bash
+$ echo "QXp1cmVEaWFtb25kOmh1bnRlcjI=" | base64 -d
+AzureDiamond:hunter2
+```
+
+For what it's worth, this config file is equivalent to:
+
+```json
+{
+ "auths": {
+ "registry.example.com": {
+ "username": "AzureDiamond",
+ "password": "hunter2"
+ }
+ }
+}
+```
+
+... which is useful to know if e.g. your CI system provides you a registry username and password via environment variables and you want to populate this file manually without invoking `docker login`.
+
+### Helpers
+
+If you log in like this, `docker` will warn you that you should use a [credential helper](https://docs.docker.com/engine/reference/commandline/login/#credentials-store), and you should!
+
+To configure a global credential helper:
+```json
+{
+ "credsStore": "osxkeychain"
+}
+```
+
+To configure a per-registry credential helper:
+```json
+{
+ "credHelpers": {
+ "gcr.io": "gcr"
+ }
+}
+```
+
+We use [`github.com/docker/cli/cli/config.Load`](https://godoc.org/github.com/docker/cli/cli/config#Load) to parse the config file and invoke any necessary credential helpers. This handles the logic of taking a [`ConfigFile`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/configfile/file.go#L25-L54) + registry domain and producing an [`AuthConfig`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L3-L22), which determines how we authenticate to the registry.
+
+## Credential Helpers
+
+The [credential helper protocol](https://github.com/docker/docker-credential-helpers) allows you to configure a binary that supplies credentials for the registry, rather than hard-coding them in the config file.
+
+The protocol has several verbs, but the one we most care about is `get`.
+
+For example, using the following config file:
+```json
+{
+ "credHelpers": {
+ "gcr.io": "gcr",
+ "eu.gcr.io": "gcr"
+ }
+}
+```
+
+To acquire credentials for `gcr.io`, we look in the `credHelpers` map to find
+the credential helper for `gcr.io` is `gcr`. By appending that value to
+`docker-credential-`, we can get the name of the binary we need to use.
+
+For this example, that's `docker-credential-gcr`, which must be on our `$PATH`.
+We'll then invoke that binary to get credentials:
+
+```bash
+$ echo "gcr.io" | docker-credential-gcr get
+{"Username":"_token","Secret":"<long access token>"}
+```
+
+You can configure the same credential helper for multiple registries, which is
+why we need to pass the domain in via STDIN, e.g. if we were trying to access
+`eu.gcr.io`, we'd do this instead:
+
+```bash
+$ echo "eu.gcr.io" | docker-credential-gcr get
+{"Username":"_token","Secret":"<long access token>"}
+```
+
+### Debugging credential helpers
+
+If a credential helper is configured but doesn't seem to be working, it can be
+challenging to debug. Implementing a fake credential helper lets you poke around
+to make it easier to see where the failure is happening.
+
+This "implements" a credential helper with hard-coded values:
+```
+#!/usr/bin/env bash
+echo '{"Username":"<token>","Secret":"hunter2"}'
+```
+
+
+This implements a credential helper that prints the output of
+`docker-credential-gcr` to both stderr and whatever called it, which allows you
+to snoop on another credential helper:
+```
+#!/usr/bin/env bash
+docker-credential-gcr $@ | tee >(cat 1>&2)
+```
+
+Put those files somewhere on your path, naming them e.g.
+`docker-credential-hardcoded` and `docker-credential-tee`, then modify the
+config file to use them:
+
+```json
+{
+ "credHelpers": {
+ "gcr.io": "tee",
+ "eu.gcr.io": "hardcoded"
+ }
+}
+```
+
+The `docker-credential-tee` trick works with both `crane` and `docker`:
+
+```bash
+$ crane manifest gcr.io/google-containers/pause > /dev/null
+{"ServerURL":"","Username":"_dcgcr_1_5_0_token","Secret":"<redacted>"}
+
+$ docker pull gcr.io/google-containers/pause
+Using default tag: latest
+{"ServerURL":"","Username":"_dcgcr_1_5_0_token","Secret":"<redacted>"}
+latest: Pulling from google-containers/pause
+a3ed95caeb02: Pull complete
+4964c72cd024: Pull complete
+Digest: sha256:a78c2d6208eff9b672de43f880093100050983047b7b0afe0217d3656e1b0d5f
+Status: Downloaded newer image for gcr.io/google-containers/pause:latest
+gcr.io/google-containers/pause:latest
+```
+
+## The Registry
+
+There are two methods for authenticating against a registry:
+[token](https://docs.docker.com/registry/spec/auth/token/) and
+[oauth2](https://docs.docker.com/registry/spec/auth/oauth/).
+
+Both methods are used to acquire an opaque `Bearer` token (or
+[RegistryToken](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L21))
+to use in the `Authorization` header. The registry will return a `401
+Unauthorized` during the [version
+check](https://github.com/opencontainers/distribution-spec/blob/2c3975d1f03b67c9a0203199038adea0413f0573/spec.md#api-version-check)
+(or during normal operations) with
+[Www-Authenticate](https://tools.ietf.org/html/rfc7235#section-4.1) challenge
+indicating how to proceed.
+
+### Token
+
+If we get back an `AuthConfig` containing a [`Username/Password`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L5-L6)
+or
+[`Auth`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L7),
+we'll use the token method for authentication:
+
+![basic](../../images/credhelper-basic.svg)
+
+### OAuth 2
+
+If we get back an `AuthConfig` containing an [`IdentityToken`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L18)
+we'll use the oauth2 method for authentication:
+
+![oauth](../../images/credhelper-oauth.svg)
+
+This happens when a credential helper returns a response with the
+[`Username`](https://github.com/docker/docker-credential-helpers/blob/f78081d1f7fef6ad74ad6b79368de6348386e591/credentials/credentials.go#L16)
+set to `<token>` (no, that's not a placeholder, the literal string `"<token>"`).
+It is unclear why: [moby/moby#36926](https://github.com/moby/moby/issues/36926).
+
+We only support the oauth2 `grant_type` for `refresh_token` ([#629](https://github.com/google/go-containerregistry/issues/629)),
+since it's impossible to determine from the registry response whether we should
+use oauth, and the token method for authentication is widely implemented by
+registries.
diff --git a/pkg/authn/anon.go b/pkg/authn/anon.go
new file mode 100644
index 0000000..8321495
--- /dev/null
+++ b/pkg/authn/anon.go
@@ -0,0 +1,26 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+// anonymous implements Authenticator for anonymous authentication.
+type anonymous struct{}
+
+// Authorization implements Authenticator.
+func (a *anonymous) Authorization() (*AuthConfig, error) {
+ return &AuthConfig{}, nil
+}
+
+// Anonymous is a singleton Authenticator for providing anonymous auth.
+var Anonymous Authenticator = &anonymous{}
diff --git a/pkg/authn/anon_test.go b/pkg/authn/anon_test.go
new file mode 100644
index 0000000..83c8214
--- /dev/null
+++ b/pkg/authn/anon_test.go
@@ -0,0 +1,31 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestAnonymous(t *testing.T) {
+ cfg, err := Anonymous.Authorization()
+ if err != nil {
+ t.Fatalf("Authorization() = %v", err)
+ }
+ want := &AuthConfig{}
+ if !reflect.DeepEqual(cfg, want) {
+ t.Errorf("Authorization(); got %v, wanted {}", cfg)
+ }
+}
diff --git a/pkg/authn/auth.go b/pkg/authn/auth.go
new file mode 100644
index 0000000..0111f1a
--- /dev/null
+++ b/pkg/authn/auth.go
@@ -0,0 +1,30 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+// auth is an Authenticator that simply returns the wrapped AuthConfig.
+type auth struct {
+ config AuthConfig
+}
+
+// FromConfig returns an Authenticator that just returns the given AuthConfig.
+func FromConfig(cfg AuthConfig) Authenticator {
+ return &auth{cfg}
+}
+
+// Authorization implements Authenticator.
+func (a *auth) Authorization() (*AuthConfig, error) {
+ return &a.config, nil
+}
diff --git a/pkg/authn/authn.go b/pkg/authn/authn.go
new file mode 100644
index 0000000..172d218
--- /dev/null
+++ b/pkg/authn/authn.go
@@ -0,0 +1,115 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "strings"
+)
+
+// Authenticator is used to authenticate Docker transports.
+type Authenticator interface {
+ // Authorization returns the value to use in an http transport's Authorization header.
+ Authorization() (*AuthConfig, error)
+}
+
+// AuthConfig contains authorization information for connecting to a Registry
+// Inlined what we use from github.com/docker/cli/cli/config/types
+type AuthConfig struct {
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+ Auth string `json:"auth,omitempty"`
+
+ // IdentityToken is used to authenticate the user and get
+ // an access token for the registry.
+ IdentityToken string `json:"identitytoken,omitempty"`
+
+ // RegistryToken is a bearer token to be sent to a registry
+ RegistryToken string `json:"registrytoken,omitempty"`
+}
+
+// This is effectively a copy of the type AuthConfig. This simplifies
+// JSON unmarshalling since AuthConfig methods are not inherited
+type authConfig AuthConfig
+
+// UnmarshalJSON implements json.Unmarshaler
+func (a *AuthConfig) UnmarshalJSON(data []byte) error {
+ var shadow authConfig
+ err := json.Unmarshal(data, &shadow)
+ if err != nil {
+ return err
+ }
+
+ *a = (AuthConfig)(shadow)
+
+ if len(shadow.Auth) != 0 {
+ var derr error
+ a.Username, a.Password, derr = decodeDockerConfigFieldAuth(shadow.Auth)
+ if derr != nil {
+ err = fmt.Errorf("unable to decode auth field: %w", derr)
+ }
+ } else if len(a.Username) != 0 && len(a.Password) != 0 {
+ a.Auth = encodeDockerConfigFieldAuth(shadow.Username, shadow.Password)
+ }
+
+ return err
+}
+
+// MarshalJSON implements json.Marshaler
+func (a AuthConfig) MarshalJSON() ([]byte, error) {
+ shadow := (authConfig)(a)
+ shadow.Auth = encodeDockerConfigFieldAuth(shadow.Username, shadow.Password)
+ return json.Marshal(shadow)
+}
+
+// decodeDockerConfigFieldAuth deserializes the "auth" field from dockercfg into a
+// username and a password. The format of the auth field is base64(<username>:<password>).
+//
+// From https://github.com/kubernetes/kubernetes/blob/75e49ec824b183288e1dbaccfd7dbe77d89db381/pkg/credentialprovider/config.go
+// Copyright 2014 The Kubernetes Authors.
+// SPDX-License-Identifier: Apache-2.0
+func decodeDockerConfigFieldAuth(field string) (username, password string, err error) {
+ var decoded []byte
+ // StdEncoding can only decode padded string
+ // RawStdEncoding can only decode unpadded string
+ if strings.HasSuffix(strings.TrimSpace(field), "=") {
+ // decode padded data
+ decoded, err = base64.StdEncoding.DecodeString(field)
+ } else {
+ // decode unpadded data
+ decoded, err = base64.RawStdEncoding.DecodeString(field)
+ }
+
+ if err != nil {
+ return
+ }
+
+ parts := strings.SplitN(string(decoded), ":", 2)
+ if len(parts) != 2 {
+ err = fmt.Errorf("must be formatted as base64(username:password)")
+ return
+ }
+
+ username = parts[0]
+ password = parts[1]
+
+ return
+}
+
+func encodeDockerConfigFieldAuth(username, password string) string {
+ return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
+}
diff --git a/pkg/authn/authn_test.go b/pkg/authn/authn_test.go
new file mode 100644
index 0000000..f191acd
--- /dev/null
+++ b/pkg/authn/authn_test.go
@@ -0,0 +1,148 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestAuthConfigMarshalJSON(t *testing.T) {
+ cases := []struct {
+ name string
+ config AuthConfig
+ json string
+ }{{
+ name: "auth field is calculated",
+ config: AuthConfig{
+ Username: "user",
+ Password: "pass",
+ IdentityToken: "id",
+ RegistryToken: "reg",
+ },
+ json: `{"username":"user","password":"pass","auth":"dXNlcjpwYXNz","identitytoken":"id","registrytoken":"reg"}`,
+ }, {
+ name: "auth field replaced",
+ config: AuthConfig{
+ Username: "user",
+ Password: "pass",
+ Auth: "blah",
+ IdentityToken: "id",
+ RegistryToken: "reg",
+ },
+ json: `{"username":"user","password":"pass","auth":"dXNlcjpwYXNz","identitytoken":"id","registrytoken":"reg"}`,
+ }}
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ bytes, err := json.Marshal(&tc.config)
+
+ if err != nil {
+ t.Fatal("Marshal() =", err)
+ }
+
+ if diff := cmp.Diff(tc.json, string(bytes)); diff != "" {
+ t.Error("json output diff (-want, +got): ", diff)
+ }
+ })
+ }
+}
+
+func TestAuthConfigUnmarshalJSON(t *testing.T) {
+ cases := []struct {
+ name string
+ json string
+ err string
+ want AuthConfig
+ }{{
+ name: "valid config no auth",
+ json: `{
+ "username": "user",
+ "password": "pass",
+ "identitytoken": "id",
+ "registrytoken": "reg"
+ }`,
+ want: AuthConfig{
+ // Auth value is set based on username and password
+ Auth: "dXNlcjpwYXNz",
+ Username: "user",
+ Password: "pass",
+ IdentityToken: "id",
+ RegistryToken: "reg",
+ },
+ }, {
+ name: "bad json input",
+ json: `{"username":true}`,
+ err: "json: cannot unmarshal",
+ }, {
+ name: "auth is base64",
+ json: `{ "auth": "dXNlcjpwYXNz" }`, // user:pass
+ want: AuthConfig{
+ Username: "user",
+ Password: "pass",
+ Auth: "dXNlcjpwYXNz",
+ },
+ }, {
+ name: "auth field overrides others",
+ json: `{ "auth": "dXNlcjpwYXNz", "username":"foo", "password":"bar" }`, // user:pass
+ want: AuthConfig{
+ Username: "user",
+ Password: "pass",
+ Auth: "dXNlcjpwYXNz",
+ },
+ }, {
+ name: "auth is base64 padded",
+ json: `{ "auth": "dXNlcjpwYXNzd29yZA==" }`, // user:password
+ want: AuthConfig{
+ Username: "user",
+ Password: "password",
+ Auth: "dXNlcjpwYXNzd29yZA==",
+ },
+ }, {
+ name: "auth is not base64",
+ json: `{ "auth": "bad-auth-bad" }`,
+ err: "unable to decode auth field",
+ }, {
+ name: "decoded auth is not valid",
+ json: `{ "auth": "Zm9vYmFy" }`,
+ err: "unable to decode auth field: must be formatted as base64(username:password)",
+ }}
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ var got AuthConfig
+ err := json.Unmarshal([]byte(tc.json), &got)
+ if tc.err != "" && err == nil {
+ t.Fatal("no error occurred expected:", tc.err)
+ } else if tc.err != "" && err != nil {
+ if !strings.HasPrefix(err.Error(), tc.err) {
+ t.Fatalf("expected err %q to have prefix %q", err, tc.err)
+ }
+ return
+ }
+
+ if err != nil {
+ t.Fatal("Unmarshal()=", err)
+ }
+
+ if diff := cmp.Diff(tc.want, got); diff != "" {
+ t.Fatal("unexpected diff (-want, +got)\n", diff)
+ }
+ })
+ }
+}
diff --git a/pkg/authn/basic.go b/pkg/authn/basic.go
new file mode 100644
index 0000000..500cb66
--- /dev/null
+++ b/pkg/authn/basic.go
@@ -0,0 +1,29 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+// Basic implements Authenticator for basic authentication.
+type Basic struct {
+ Username string
+ Password string
+}
+
+// Authorization implements Authenticator.
+func (b *Basic) Authorization() (*AuthConfig, error) {
+ return &AuthConfig{
+ Username: b.Username,
+ Password: b.Password,
+ }, nil
+}
diff --git a/pkg/authn/basic_test.go b/pkg/authn/basic_test.go
new file mode 100644
index 0000000..aecbe15
--- /dev/null
+++ b/pkg/authn/basic_test.go
@@ -0,0 +1,33 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestBasic(t *testing.T) {
+ basic := &Basic{Username: "foo", Password: "bar"}
+
+ got, err := basic.Authorization()
+ if err != nil {
+ t.Fatalf("Authorization() = %v", err)
+ }
+ want := &AuthConfig{Username: "foo", Password: "bar"}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("Authorization(); got %v, want %v", got, want)
+ }
+}
diff --git a/pkg/authn/bearer.go b/pkg/authn/bearer.go
new file mode 100644
index 0000000..4cf86df
--- /dev/null
+++ b/pkg/authn/bearer.go
@@ -0,0 +1,27 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+// Bearer implements Authenticator for bearer authentication.
+type Bearer struct {
+ Token string `json:"token"`
+}
+
+// Authorization implements Authenticator.
+func (b *Bearer) Authorization() (*AuthConfig, error) {
+ return &AuthConfig{
+ RegistryToken: b.Token,
+ }, nil
+}
diff --git a/pkg/authn/bearer_test.go b/pkg/authn/bearer_test.go
new file mode 100644
index 0000000..7d6b26b
--- /dev/null
+++ b/pkg/authn/bearer_test.go
@@ -0,0 +1,31 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+import (
+ "testing"
+)
+
+func TestBearer(t *testing.T) {
+ anon := &Bearer{Token: "bazinga"}
+
+ auth, err := anon.Authorization()
+ if err != nil {
+ t.Errorf("Authorization() = %v", err)
+ }
+ if got, want := auth.RegistryToken, "bazinga"; got != want {
+ t.Errorf("Authorization(); got %v, want %v", got, want)
+ }
+}
diff --git a/pkg/authn/doc.go b/pkg/authn/doc.go
new file mode 100644
index 0000000..c2a5fc0
--- /dev/null
+++ b/pkg/authn/doc.go
@@ -0,0 +1,17 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package authn defines different methods of authentication for
+// talking to a container registry.
+package authn
diff --git a/pkg/authn/github/keychain.go b/pkg/authn/github/keychain.go
new file mode 100644
index 0000000..97ad34e
--- /dev/null
+++ b/pkg/authn/github/keychain.go
@@ -0,0 +1,59 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package github provides a keychain for the GitHub Container Registry.
+package github
+
+import (
+ "net/url"
+ "os"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+)
+
+const ghcrHostname = "ghcr.io"
+
+// Keychain exports an instance of the GitHub Keychain.
+//
+// This keychain matches on requests for ghcr.io and provides the value of the
+// environment variable $GITHUB_TOKEN, if it's set.
+var Keychain authn.Keychain = githubKeychain{}
+
+type githubKeychain struct{}
+
+func (githubKeychain) Resolve(r authn.Resource) (authn.Authenticator, error) {
+ serverURL, err := url.Parse("https://" + r.String())
+ if err != nil {
+ return authn.Anonymous, nil
+ }
+ if serverURL.Hostname() == ghcrHostname {
+ username := os.Getenv("GITHUB_ACTOR")
+ if username == "" {
+ username = "unset"
+ }
+ if tok := os.Getenv("GITHUB_TOKEN"); tok != "" {
+ return githubAuthenticator{username, tok}, nil
+ }
+ }
+ return authn.Anonymous, nil
+}
+
+type githubAuthenticator struct{ username, password string }
+
+func (g githubAuthenticator) Authorization() (*authn.AuthConfig, error) {
+ return &authn.AuthConfig{
+ Username: g.username,
+ Password: g.password,
+ }, nil
+}
diff --git a/pkg/authn/github/keychain_test.go b/pkg/authn/github/keychain_test.go
new file mode 100644
index 0000000..c3e858a
--- /dev/null
+++ b/pkg/authn/github/keychain_test.go
@@ -0,0 +1,112 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package github
+
+import (
+ "os"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+)
+
+// TestKeychain checks that the keychain resolves when $GITHUB_TOKEN is set and
+// the request is for GHCR.
+func TestKeychain(t *testing.T) {
+ username, tok := "octocat", "my-token"
+ os.Setenv("GITHUB_ACTOR", username)
+ os.Setenv("GITHUB_TOKEN", tok)
+ got, err := Keychain.Resolve(resource("ghcr.io/my/repo"))
+ if err != nil {
+ t.Fatalf("Resolve: %v", err)
+ }
+ if got == authn.Anonymous {
+ t.Fatalf("Got anonymous, wanted authenticator")
+ }
+
+ auth, err := got.Authorization()
+ if err != nil {
+ t.Fatalf("Authorization: %v", err)
+ }
+ if auth.Username != username {
+ t.Errorf("Got username %q, want %q", auth.Username, username)
+ }
+ if auth.Password != tok {
+ t.Errorf("Got password %q, want %q", auth.Password, tok)
+ }
+}
+
+// TestKeychainUsernameUnset checks that the keychain resolves an "unset"
+// username when $GITHUB_ACTOR is not set.
+func TestKeychainUsernameUnset(t *testing.T) {
+ tok := "my-token"
+ os.Unsetenv("GITHUB_ACTOR")
+ os.Setenv("GITHUB_TOKEN", tok)
+ got, err := Keychain.Resolve(resource("ghcr.io/my/repo"))
+ if err != nil {
+ t.Fatalf("Resolve: %v", err)
+ }
+ if got == authn.Anonymous {
+ t.Fatalf("Got anonymous, wanted authenticator")
+ }
+
+ auth, err := got.Authorization()
+ if err != nil {
+ t.Fatalf("Authorization: %v", err)
+ }
+ if auth.Username != "unset" {
+ t.Errorf("Got username %q, want unset", auth.Username)
+ }
+ if auth.Password != tok {
+ t.Errorf("Got password %q, want %q", auth.Password, tok)
+ }
+}
+
+// TestKeychainUnset checks that the keychain doesn't resolve when the
+// environment variable is unset.
+func TestKeychainUnset(t *testing.T) {
+ os.Unsetenv("GITHUB_TOKEN")
+
+ got, err := Keychain.Resolve(resource("ghcr.io/my/repo"))
+ if err != nil {
+ t.Fatalf("Resolve: %v", err)
+ }
+ if got != authn.Anonymous {
+ t.Errorf("Resolve(ghcr.io) got %v, want Anonymous", got)
+ }
+}
+
+// TestNoMatch checks that the keychain doesn't resolve for non-GHCR registries.
+func TestNoMatch(t *testing.T) {
+ os.Setenv("GITHUB_TOKEN", "my-token")
+ for _, s := range []string{
+ "gcr.io",
+ "example.com",
+ "ghcr.io.example.com",
+ "invalid-domain-name -- %U)(@*)(%*)@(*#%@",
+ } {
+ got, err := Keychain.Resolve(resource(s))
+ if err != nil {
+ t.Fatalf("Resolve: %v", err)
+ }
+ if got != authn.Anonymous {
+ t.Errorf("Resolve(%q) got %v, want Anonymous", s, got)
+ }
+ }
+}
+
+type resource string
+
+func (r resource) String() string { return string(r) }
+func (r resource) RegistryStr() string { return string(r) }
diff --git a/pkg/authn/k8schain/README.md b/pkg/authn/k8schain/README.md
new file mode 100644
index 0000000..0bf4371
--- /dev/null
+++ b/pkg/authn/k8schain/README.md
@@ -0,0 +1,49 @@
+# `k8schain`
+
+This is an implementation of the [`authn.Keychain`](https://godoc.org/github.com/google/go-containerregistry/authn#Keychain) interface loosely based on the authentication semantics used by the Kubelet when performing the pull of a Pod's images.
+
+This keychain supports passing a Kubernetes Service Account and some ImagePullSecrets which may represent registry credentials.
+
+In addition to those, the keychain also includes cloud-specific credential helpers for Google Container Registry (and Artifact Registry), Azure Container Registry, and Amazon AWS Elasic Container Registry.
+This means that if the keychain is used from within Kubernetes services on those clouds (GKE, AKS, EKS), any available service credentials will be discovered and used.
+
+In general this keychain should be used when the code is expected to run in a Kubernetes cluster, and especially when it will run in one of those clouds.
+To get a cloud-agnostic keychain, use [`pkg/authn/kubernetes`](../kubernetes) instead.
+
+To get only cloud-aware keychains, use [`google.Keychain`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/google#Keychain), or [`pkg/authn.NewKeychainFromHelper`](https://godoc.org/github.com/google/go-containerregistry/pkg/authn#NewKeychainFromHelper) with a cloud credential helper implementation -- see the implementation of `k8schain.NewNoClient` for more details.
+
+## Usage
+
+### Creating a keychain
+
+A `k8schain` keychain can be built via one of:
+
+```go
+// client is a kubernetes.Interface
+kc, err := k8schain.New(ctx, client, k8schain.Options{})
+...
+
+// This method is suitable for use by controllers or other in-cluster processes.
+kc, err := k8schain.NewInCluster(ctx, k8schain.Options{})
+...
+```
+
+### Using the keychain
+
+The `k8schain` keychain can be used directly as an `authn.Keychain`, e.g.
+
+```go
+auth, err := kc.Resolve(registry)
+if err != nil {
+ ...
+}
+```
+
+Or, with the [`remote.WithAuthFromKeychain`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/remote#WithAuthFromKeychain) option:
+
+```go
+img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc))
+if err != nil {
+ ...
+}
+```
diff --git a/pkg/authn/k8schain/doc.go b/pkg/authn/k8schain/doc.go
new file mode 100644
index 0000000..c9ae7f1
--- /dev/null
+++ b/pkg/authn/k8schain/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package k8schain exposes an implementation of the authn.Keychain interface
+// based on the semantics the Kubelet follows when pulling the images for a
+// Pod in Kubernetes.
+package k8schain
diff --git a/pkg/authn/k8schain/go.mod b/pkg/authn/k8schain/go.mod
new file mode 100644
index 0000000..b49beac
--- /dev/null
+++ b/pkg/authn/k8schain/go.mod
@@ -0,0 +1,96 @@
+module github.com/google/go-containerregistry/pkg/authn/k8schain
+
+go 1.18
+
+replace (
+ github.com/google/go-containerregistry => ../../../
+ github.com/google/go-containerregistry/pkg/authn/kubernetes => ../kubernetes/
+)
+
+require (
+ github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1
+ github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589
+ github.com/google/go-containerregistry v0.13.0
+ github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20230309011546-ff810c186c77
+ k8s.io/api v0.26.2
+ k8s.io/client-go v0.26.2
+)
+
+require (
+ cloud.google.com/go/compute v1.18.0 // indirect
+ cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
+ github.com/Azure/go-autorest v14.2.0+incompatible // indirect
+ github.com/Azure/go-autorest/autorest v0.11.28 // indirect
+ github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
+ github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
+ github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
+ github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
+ github.com/Azure/go-autorest/logger v0.2.1 // indirect
+ github.com/Azure/go-autorest/tracing v0.6.0 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.17.5 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.18.15 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 // indirect
+ github.com/aws/smithy-go v1.13.5 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/dimchansky/utfbom v1.1.1 // indirect
+ github.com/docker/cli v23.0.1+incompatible // indirect
+ github.com/docker/distribution v2.8.1+incompatible // indirect
+ github.com/docker/docker v23.0.1+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.7.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.10.2 // indirect
+ github.com/go-logr/logr v1.2.3 // indirect
+ github.com/go-openapi/jsonpointer v0.19.6 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
+ github.com/go-openapi/swag v0.22.3 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/gnostic v0.6.9 // indirect
+ github.com/google/go-cmp v0.5.9 // indirect
+ github.com/google/gofuzz v1.2.0 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.16.0 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/sirupsen/logrus v1.9.0 // indirect
+ golang.org/x/crypto v0.7.0 // indirect
+ golang.org/x/net v0.8.0 // indirect
+ golang.org/x/oauth2 v0.6.0 // indirect
+ golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sys v0.6.0 // indirect
+ golang.org/x/term v0.6.0 // indirect
+ golang.org/x/text v0.8.0 // indirect
+ golang.org/x/time v0.3.0 // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/protobuf v1.29.0 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ k8s.io/apimachinery v0.26.2 // indirect
+ k8s.io/klog/v2 v2.90.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect
+ k8s.io/utils v0.0.0-20230308161112-d77c459e9343 // indirect
+ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
+ sigs.k8s.io/yaml v1.3.0 // indirect
+)
diff --git a/pkg/authn/k8schain/go.sum b/pkg/authn/k8schain/go.sum
new file mode 100644
index 0000000..e5cb660
--- /dev/null
+++ b/pkg/authn/k8schain/go.sum
@@ -0,0 +1,364 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
+cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
+github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
+github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
+github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
+github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=
+github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
+github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc=
+github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
+github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk=
+github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0=
+github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
+github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
+github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
+github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
+github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
+github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
+github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/aws/aws-sdk-go-v2 v1.17.5 h1:TzCUW1Nq4H8Xscph5M/skINUitxM5UBAyvm2s7XBzL4=
+github.com/aws/aws-sdk-go-v2 v1.17.5/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
+github.com/aws/aws-sdk-go-v2/config v1.18.15 h1:509yMO0pJUGUugBP2H9FOFyV+7Mz7sRR+snfDN5W4NY=
+github.com/aws/aws-sdk-go-v2/config v1.18.15/go.mod h1:vS0tddZqpE8cD9CyW0/kITHF5Bq2QasW9Y1DFHD//O0=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.15 h1:0rZQIi6deJFjOEgHI9HI2eZcLPPEGQPictX66oRFLL8=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.15/go.mod h1:vRMLMD3/rXU+o6j2MW5YefrGMBmdTvkLLGqFwMLBHQc=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 h1:b/Vn141DBuLVgXbhRWIrl9g+ww7G+ScV5SzniWR13jQ=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23/go.mod h1:mr6c4cHC+S/MMkrjtSlG4QA36kOznDep+0fga5L/fGQ=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 h1:IVx9L7YFhpPq0tTnGo8u8TpluFu7nAn9X3sUDMb11c0=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30/go.mod h1:vsbq62AOBwQ1LJ/GWKFxX8beUEYeRp/Agitrxee2/qM=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5 h1:tGA4ZoAsrYhGBypKAo2jwoX/Z5ponBZOTEUMNN/rHP4=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5/go.mod h1:cDZh+PHP8Adt9E0zfZT9cK4qadbtIuU/czLpEJtm4wc=
+github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4 h1:6OBVD6KE4gLReaNfG7CSXFvNIVqKIqrywRcG1kUKr4M=
+github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4/go.mod h1:gUxgbzXs+gHsj/6al9dzzoByeSrEl03Oj4iJBu/m/Rk=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 h1:QoOybhwRfciWUBbZ0gp9S7XaDnCuSTeK/fySB99V1ls=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23/go.mod h1:9uPh+Hrz2Vn6oMnQYiUi/zbh3ovbnQk19YKINkQny44=
+github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 h1:qJdM48OOLl1FBSzI7ZrA1ZfLwOyCYqkXV5lko1hYDBw=
+github.com/aws/aws-sdk-go-v2/service/sso v1.12.4/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 h1:YRkWXQveFb0tFC0TLktmmhGsOcCgLwvq88MC2al47AA=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 h1:L1600eLr0YvTT7gNh3Ni24yGI7NSHkq9Gp62vijPRCs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.18.5/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s=
+github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
+github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
+github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1 h1:uQhxQriOPUu/knXSPM7D/VyS3GMz+4wsE43eB8f9ojg=
+github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1/go.mod h1:/JmJjW2NJpzRSI3pOxQPC6eOD/tR8SfOA9X1FurmzXI=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4=
+github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
+github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
+github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM=
+github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
+github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY=
+github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
+github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE=
+github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
+github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
+github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
+github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
+github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs=
+github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
+github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
+google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.26.2 h1:dM3cinp3PGB6asOySalOZxEG4CZ0IAdJsrYZXE/ovGQ=
+k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU=
+k8s.io/apimachinery v0.26.2 h1:da1u3D5wfR5u2RpLhE/ZtZS2P7QvDgLZTi9wrNZl/tQ=
+k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I=
+k8s.io/client-go v0.26.2 h1:s1WkVujHX3kTp4Zn4yGNFK+dlDXy1bAAkIl+cFAiuYI=
+k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU=
+k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
+k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg=
+k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
+k8s.io/utils v0.0.0-20230308161112-d77c459e9343 h1:m7tbIjXGcGIAtpmQr7/NAi7RsWoW3E7Zcm4jI1HicTc=
+k8s.io/utils v0.0.0-20230308161112-d77c459e9343/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
+sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/pkg/authn/k8schain/k8schain.go b/pkg/authn/k8schain/k8schain.go
new file mode 100644
index 0000000..8ecbd5f
--- /dev/null
+++ b/pkg/authn/k8schain/k8schain.go
@@ -0,0 +1,105 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package k8schain
+
+import (
+ "context"
+ "io"
+
+ ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
+ "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper"
+ "github.com/google/go-containerregistry/pkg/authn"
+ kauth "github.com/google/go-containerregistry/pkg/authn/kubernetes"
+ "github.com/google/go-containerregistry/pkg/v1/google"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+)
+
+var (
+ amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard)))
+ azureKeychain authn.Keychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper())
+)
+
+// Options holds configuration data for guiding credential resolution.
+type Options = kauth.Options
+
+// New returns a new authn.Keychain suitable for resolving image references as
+// scoped by the provided Options. It speaks to Kubernetes through the provided
+// client interface.
+func New(ctx context.Context, client kubernetes.Interface, opt Options) (authn.Keychain, error) {
+ k8s, err := kauth.New(ctx, client, kauth.Options(opt))
+ if err != nil {
+ return nil, err
+ }
+
+ return authn.NewMultiKeychain(
+ k8s,
+ authn.DefaultKeychain,
+ google.Keychain,
+ amazonKeychain,
+ azureKeychain,
+ ), nil
+}
+
+// NewInCluster returns a new authn.Keychain suitable for resolving image references as
+// scoped by the provided Options, constructing a kubernetes.Interface based on in-cluster
+// authentication.
+func NewInCluster(ctx context.Context, opt Options) (authn.Keychain, error) {
+ clusterConfig, err := rest.InClusterConfig()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := kubernetes.NewForConfig(clusterConfig)
+ if err != nil {
+ return nil, err
+ }
+ return New(ctx, client, opt)
+}
+
+// NewNoClient returns a new authn.Keychain that supports the portions of the K8s keychain
+// that don't read ImagePullSecrets. This limits it to roughly the Node-identity-based
+// authentication schemes in Kubernetes pkg/credentialprovider. This version of the
+// k8schain drops the requirement that we run as a K8s serviceaccount with access to all
+// of the on-cluster secrets. This drop in fidelity also diminishes its value as a stand-in
+// for Kubernetes authentication, but this actually targets a different use-case. What
+// remains is an interesting sweet spot: this variant can serve as a credential provider
+// for all of the major public clouds, but in library form (vs. an executable you exec).
+func NewNoClient(ctx context.Context) (authn.Keychain, error) {
+ return authn.NewMultiKeychain(
+ authn.DefaultKeychain,
+ google.Keychain,
+ amazonKeychain,
+ azureKeychain,
+ ), nil
+}
+
+// NewFromPullSecrets returns a new authn.Keychain suitable for resolving image references as
+// scoped by the pull secrets.
+func NewFromPullSecrets(ctx context.Context, pullSecrets []corev1.Secret) (authn.Keychain, error) {
+ k8s, err := kauth.NewFromPullSecrets(ctx, pullSecrets)
+ if err != nil {
+ return nil, err
+ }
+
+ return authn.NewMultiKeychain(
+ k8s,
+ authn.DefaultKeychain,
+ google.Keychain,
+ amazonKeychain,
+ azureKeychain,
+ ), nil
+}
diff --git a/pkg/authn/k8schain/tests/explicit/main.go b/pkg/authn/k8schain/tests/explicit/main.go
new file mode 100644
index 0000000..744320e
--- /dev/null
+++ b/pkg/authn/k8schain/tests/explicit/main.go
@@ -0,0 +1,52 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/google/go-containerregistry/pkg/authn/k8schain"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func main() {
+ ref, err := name.NewTag("gcr.io/build-crd-testing/secret-sauce:latest")
+ if err != nil {
+ log.Fatalf("NewTag() = %v", err)
+ }
+
+ kc, err := k8schain.NewInCluster(context.Background(), k8schain.Options{
+ Namespace: "explicit-namespace",
+ ImagePullSecrets: []string{
+ "explicit-secret",
+ },
+ })
+ if err != nil {
+ log.Fatalf("k8schain.New() = %v", err)
+ }
+
+ img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc))
+ if err != nil {
+ log.Fatalf("remote.Image() = %v", err)
+ }
+
+ digest, err := img.Digest()
+ if err != nil {
+ log.Fatalf("Digest() = %v", err)
+ }
+ log.Printf("got digest: %v", digest)
+}
diff --git a/pkg/authn/k8schain/tests/explicit/test.yaml b/pkg/authn/k8schain/tests/explicit/test.yaml
new file mode 100644
index 0000000..10cdfba
--- /dev/null
+++ b/pkg/authn/k8schain/tests/explicit/test.yaml
@@ -0,0 +1,59 @@
+# Copyright 2018 Google LLC All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: explicit-namespace
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: explicit-secret
+ namespace: explicit-namespace
+type: kubernetes.io/dockercfg
+data:
+ # This service account is JUST a storage reader on gcr.io/build-crd-testing
+ .dockercfg: eyJodHRwczovL2djci5pbyI6eyJ1c2VybmFtZSI6Il9qc29uX2tleSIsInBhc3N3b3JkIjoie1xuICBcInR5cGVcIjogXCJzZXJ2aWNlX2FjY291bnRcIixcbiAgXCJwcm9qZWN0X2lkXCI6IFwiYnVpbGQtY3JkLXRlc3RpbmdcIixcbiAgXCJwcml2YXRlX2tleV9pZFwiOiBcIjA1MDJhNDFhODEyZmI2NGNlNTZhNjhlYzU4MzJhYjBiYTExYzExZTZcIixcbiAgXCJwcml2YXRlX2tleVwiOiBcIi0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxcbk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzlYNEVZT0FSYnhRTThcXG5EMnhYY2FaVGsrZ1k4ZWp1OTh0THFDUXFUckdNVzlSZVQyeE9ZNUF5Z2FsUFArcDd5WEVja3dCRC9IaE0wZ2xJXFxuN01UTGRlZUtXcityQTFMd0haeVdGVzdIME9uZjd3bllIRUhMV1VtYzNCQ09SRUR0SFJaN1pyUEJmMUhUQUEvM1xcbk1uVzVsWkhTTjlvanpTU0Z3NkFWdTZqNmF4YkJJSUo3NTRMcmdLZUFZdXJ3ZklRMlJMVHUyMDFrMklxTFliaGJcXG4zbVNWRzVSK3RiS3oxQ3ZNNTNuSENiN0NmdVZlV3NyQThrazd4SHJyTFFLTW1JOXYyc2dSdWd5TUF6d3ovNnpOXFxuaDUvaU14eGdlcTVXOHhrVngzSjJuWThKSmRIYWYvVDZBR3NPTkVvNDNweGVpUVZqblJmL0tuMTBUQ2MyRXNJWVxcblM0OVVzWjdCQWdNQkFBRUNnZ0VBQXVwbGR1a0NRUXVENVUvZ2FtSHQ3R2dXM0FNVjE4ZXFuSG5DYTJqbGFoK1NcXG5BZVVHbmhnSmpOdkUrcE1GbFN2NXVmMnAySzRlZC9veEQ2K0NwOVpYRFJqZ3ZmdEl5cWpsemJ3dkZjZ3p3TnVEXFxueWdVa3VwN0hlY0RzRDhUdGVBb2JUL1Zwd3E2ektNckJ3Q3ZOa3Z5NmJWbG9FajV4M2JYc2F4ZTk1RE8veXB1NlxcbncwVzk3enh3d0RKWTZLUWNJV01qaHJHeHZ3WDduaVVDZU00bGVXQkR5R3R3MXplSm40aEVjNk4zYWpRYWNYS2NcXG4rNFFseGNpYW1ZcVFXYlBudHhXUWhoUXpjSFdMaTJsOWNGYlpENyt1SkxGNGlONnk4bVZOVTNLM0sxYlJZclNEXFxuUlVwM2FVVkJYbUZnK1ovMnB1VkwrbVUzajNMTFdZeUJPa2V2dU9tZGdRS0JnUURlM0dJUWt5V0lTMTRUZE1PU1xcbkJpS0JDRHk4aDk2ZWhMMEhrRGJ5T2tTdFBLZEY5cHVFeFp4aHk3b2pIQ0lNNUZWcnBSTjI1cDRzRXp3RmFjK3ZcXG5KSUZnRXZxN21YZm1YaVhJTmllUG9FUWFDbm54RHhXZ21yMEhVS0VtUzlvTWRnTGNHVStrQ1ZHTnN6N0FPdW0wXFxuS3FZM3MyMlE5bFE2N0ZPeXFpdThXRlE3UVFLQmdRRFppRmhURVprUEVjcVpqbndKcFRCNTZaV1A5S1RzbFpQN1xcbndVNGJ6aTZ5K21leWYzTUorNEwyU3lIYzNjcFNNYmp0Tk9aQ3Q0N2I5MDhGVW1MWFVHTmhjd3VaakVReEZleTBcXG5tNDFjUzVlNFA0OWI5bjZ5TEJqQnJCb3FzMldCYWwyZWdkaE5KU3NDV29pWlA4L1pUOGVnWHZoN2I5MWp6b0syXFxucTJQVW1BNERnUUtCZ0FXTDJJanZFSTBPeXgyUzExY24vZTNXSmFUUGdOUFRHOTAzVXBhK3FuemhPSXgrTWFxaFxcblBGNFdzdUF5MEFvZ0dKd2dOSmJOOEh2S1VzRVR2QTV3eXlOMzlYTjd3MGNoYXJGTDM3b3NVK1dPQXpEam5qY3NcXG5BcTVPN0dQR21YdWI2RUJRQlBKaEpQMXd5NHYvSzFmSGcvRjQ3cTRmNDBMQUpPa2FZUkpENUh6QkFvR0JBTlVoXFxubklCUEpxcTRJTXZRNmNDOWc4QisxeFlEZWE5L1lrMXcrU21QR3Z3ckVYeTNHS3g0SzdsS3BiUHo3bTRYMzNzeFxcbnNFVS8rWTJWUW13UmExeFFtLzUzcks3VjJsNUpmL0Q0MDBqUm02WmZTQU92Z0RUcnRablVHSk1yejlFN3VOdzdcXG5sZ1VIM0pyaXZ5Ri9meE1JOHFzelFid1hQMCt4bnlxQXhFQWdkdUtCQW9HQUlNK1BTTllXQ1pYeERwU0hJMThkXFxuaktrb0FidzJNb3l3UUlsa2V1QW4xZFhGYWQxenNYUUdkVHJtWHl2N05QUCs4R1hCa25CTGkzY3Z4VGlsSklTeVxcbnVjTnJDTWlxTkFTbi9kcTdjV0RGVUFCZ2pYMTZKSDJETkZaL2wvVVZGM05EQUpqWENzMVg3eUlKeVhCNm94L3pcXG5hU2xxbElNVjM1REJEN3F4Unl1S3Nnaz1cXG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXFxuXCIsXG4gIFwiY2xpZW50X2VtYWlsXCI6IFwicHVsbC1zZWNyZXQtdGVzdGluZ0BidWlsZC1jcmQtdGVzdGluZy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbVwiLFxuICBcImNsaWVudF9pZFwiOiBcIjEwNzkzNTg2MjAzMzAyNTI1MTM1MlwiLFxuICBcImF1dGhfdXJpXCI6IFwiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGhcIixcbiAgXCJ0b2tlbl91cmlcIjogXCJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW5cIixcbiAgXCJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmxcIjogXCJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHNcIixcbiAgXCJjbGllbnRfeDUwOV9jZXJ0X3VybFwiOiBcImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL3JvYm90L3YxL21ldGFkYXRhL3g1MDkvcHVsbC1zZWNyZXQtdGVzdGluZyU0MGJ1aWxkLWNyZC10ZXN0aW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tXCJcbn0iLCJlbWFpbCI6Im5vcmVwbHlAZ29vZ2xlLmNvbSIsImF1dGgiOiJYMnB6YjI1ZmEyVjVPbnNLSUNBaWRIbHdaU0k2SUNKelpYSjJhV05sWDJGalkyOTFiblFpTEFvZ0lDSndjbTlxWldOMFgybGtJam9nSW1KMWFXeGtMV055WkMxMFpYTjBhVzVuSWl3S0lDQWljSEpwZG1GMFpWOXJaWGxmYVdRaU9pQWlNRFV3TW1FME1XRTRNVEptWWpZMFkyVTFObUUyT0dWak5UZ3pNbUZpTUdKaE1URmpNVEZsTmlJc0NpQWdJbkJ5YVhaaGRHVmZhMlY1SWpvZ0lpMHRMUzB0UWtWSFNVNGdVRkpKVmtGVVJTQkxSVmt0TFMwdExWeHVUVWxKUlhaUlNVSkJSRUZPUW1kcmNXaHJhVWM1ZHpCQ1FWRkZSa0ZCVTBOQ1MyTjNaMmRUYWtGblJVRkJiMGxDUVZGRE9WZzBSVmxQUVZKaWVGRk5PRnh1UkRKNFdHTmhXbFJySzJkWk9HVnFkVGs0ZEV4eFExRnhWSEpIVFZjNVVtVlVNbmhQV1RWQmVXZGhiRkJRSzNBM2VWaEZZMnQzUWtRdlNHaE5NR2RzU1Z4dU4wMVVUR1JsWlV0WGNpdHlRVEZNZDBoYWVWZEdWemRJTUU5dVpqZDNibGxJUlVoTVYxVnRZek5DUTA5U1JVUjBTRkphTjFweVVFSm1NVWhVUVVFdk0xeHVUVzVYTld4YVNGTk9PVzlxZWxOVFJuYzJRVloxTm1vMllYaGlRa2xKU2pjMU5FeHlaMHRsUVZsMWNuZG1TVkV5VWt4VWRUSXdNV3N5U1hGTVdXSm9ZbHh1TTIxVFZrYzFVaXQwWWt0Nk1VTjJUVFV6YmtoRFlqZERablZXWlZkemNrRTRhMnMzZUVoeWNreFJTMDF0U1RsMk1uTm5VblZuZVUxQmVuZDZMelo2VGx4dWFEVXZhVTE0ZUdkbGNUVlhPSGhyVm5nelNqSnVXVGhLU21SSVlXWXZWRFpCUjNOUFRrVnZORE53ZUdWcFVWWnFibEptTDB0dU1UQlVRMk15UlhOSldWeHVVelE1VlhOYU4wSkJaMDFDUVVGRlEyZG5SVUZCZFhCc1pIVnJRMUZSZFVRMVZTOW5ZVzFJZERkSFoxY3pRVTFXTVRobGNXNUlia05oTW1wc1lXZ3JVMXh1UVdWVlIyNW9aMHBxVG5aRkszQk5SbXhUZGpWMVpqSndNa3MwWldRdmIzaEVOaXREY0RsYVdFUlNhbWQyWm5SSmVYRnFiSHBpZDNaR1kyZDZkMDUxUkZ4dWVXZFZhM1Z3TjBobFkwUnpSRGhVZEdWQmIySlVMMVp3ZDNFMmVrdE5ja0ozUTNaT2EzWjVObUpXYkc5RmFqVjRNMkpZYzJGNFpUazFSRTh2ZVhCMU5seHVkekJYT1RkNmVIZDNSRXBaTmt0UlkwbFhUV3BvY2tkNGRuZFlOMjVwVlVObFRUUnNaVmRDUkhsSGRIY3hlbVZLYmpSb1JXTTJUak5oYWxGaFkxaExZMXh1S3pSUmJIaGphV0Z0V1hGUlYySlFiblI0VjFGb2FGRjZZMGhYVEdreWJEbGpSbUphUkRjcmRVcE1SalJwVGpaNU9HMVdUbFV6U3pOTE1XSlNXWEpUUkZ4dVVsVndNMkZWVmtKWWJVWm5LMW92TW5CMVZrd3JiVlV6YWpOTVRGZFplVUpQYTJWMmRVOXRaR2RSUzBKblVVUmxNMGRKVVd0NVYwbFRNVFJVWkUxUFUxeHVRbWxMUWtORWVUaG9PVFpsYUV3d1NHdEVZbmxQYTFOMFVFdGtSamx3ZFVWNFduaG9lVGR2YWtoRFNVMDFSbFp5Y0ZKT01qVndOSE5GZW5kR1lXTXJkbHh1U2tsR1owVjJjVGR0V0dadFdHbFlTVTVwWlZCdlJWRmhRMjV1ZUVSNFYyZHRjakJJVlV0RmJWTTViMDFrWjB4alIxVXJhME5XUjA1emVqZEJUM1Z0TUZ4dVMzRlpNM015TWxFNWJGRTJOMFpQZVhGcGRUaFhSbEUzVVZGTFFtZFJSRnBwUm1oVVJWcHJVRVZqY1ZwcWJuZEtjRlJDTlRaYVYxQTVTMVJ6YkZwUU4xeHVkMVUwWW5wcE5ua3JiV1Y1WmpOTlNpczBUREpUZVVoak0yTndVMDFpYW5ST1QxcERkRFEzWWprd09FWlZiVXhZVlVkT2FHTjNkVnBxUlZGNFJtVjVNRnh1YlRReFkxTTFaVFJRTkRsaU9XNDJlVXhDYWtKeVFtOXhjekpYUW1Gc01tVm5aR2hPU2xOelExZHZhVnBRT0M5YVZEaGxaMWgyYURkaU9URnFlbTlMTWx4dWNUSlFWVzFCTkVSblVVdENaMEZYVERKSmFuWkZTVEJQZVhneVV6RXhZMjR2WlROWFNtRlVVR2RPVUZSSE9UQXpWWEJoSzNGdWVtaFBTWGdyVFdGeGFGeHVVRVkwVjNOMVFYa3dRVzluUjBwM1owNUtZazQ0U0haTFZYTkZWSFpCTlhkNWVVNHpPVmhPTjNjd1kyaGhja1pNTXpkdmMxVXJWMDlCZWtScWJtcGpjMXh1UVhFMVR6ZEhVRWR0V0hWaU5rVkNVVUpRU21oS1VERjNlVFIyTDBzeFpraG5MMFkwTjNFMFpqUXdURUZLVDJ0aFdWSktSRFZJZWtKQmIwZENRVTVWYUZ4dWJrbENVRXB4Y1RSSlRYWlJObU5ET1djNFFpc3hlRmxFWldFNUwxbHJNWGNyVTIxUVIzWjNja1ZZZVROSFMzZzBTemRzUzNCaVVIbzNiVFJZTXpOemVGeHVjMFZWTHl0Wk1sWlJiWGRTWVRGNFVXMHZOVE55U3pkV01tdzFTbVl2UkRRd01HcFNiVFphWmxOQlQzWm5SRlJ5ZEZwdVZVZEtUWEo2T1VVM2RVNTNOMXh1YkdkVlNETktjbWwyZVVZdlpuaE5TVGh4YzNwUlluZFlVREFyZUc1NWNVRjRSVUZuWkhWTFFrRnZSMEZKVFN0UVUwNVpWME5hV0hoRWNGTklTVEU0WkZ4dWFrdHJiMEZpZHpKTmIzbDNVVWxzYTJWMVFXNHhaRmhHWVdReGVuTllVVWRrVkhKdFdIbDJOMDVRVUNzNFIxaENhMjVDVEdrelkzWjRWR2xzU2tsVGVWeHVkV05PY2tOTmFYRk9RVk51TDJSeE4yTlhSRVpWUVVKbmFsZ3hOa3BJTWtST1Jsb3ZiQzlWVmtZelRrUkJTbXBZUTNNeFdEZDVTVXA1V0VJMmIzZ3ZlbHh1WVZOc2NXeEpUVll6TlVSQ1JEZHhlRko1ZFV0eloyczlYRzR0TFMwdExVVk9SQ0JRVWtsV1FWUkZJRXRGV1MwdExTMHRYRzRpTEFvZ0lDSmpiR2xsYm5SZlpXMWhhV3dpT2lBaWNIVnNiQzF6WldOeVpYUXRkR1Z6ZEdsdVowQmlkV2xzWkMxamNtUXRkR1Z6ZEdsdVp5NXBZVzB1WjNObGNuWnBZMlZoWTJOdmRXNTBMbU52YlNJc0NpQWdJbU5zYVdWdWRGOXBaQ0k2SUNJeE1EYzVNelU0TmpJd016TXdNalV5TlRFek5USWlMQW9nSUNKaGRYUm9YM1Z5YVNJNklDSm9kSFJ3Y3pvdkwyRmpZMjkxYm5SekxtZHZiMmRzWlM1amIyMHZieTl2WVhWMGFESXZZWFYwYUNJc0NpQWdJblJ2YTJWdVgzVnlhU0k2SUNKb2RIUndjem92TDJGalkyOTFiblJ6TG1kdmIyZHNaUzVqYjIwdmJ5OXZZWFYwYURJdmRHOXJaVzRpTEFvZ0lDSmhkWFJvWDNCeWIzWnBaR1Z5WDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2YjJGMWRHZ3lMM1l4TDJObGNuUnpJaXdLSUNBaVkyeHBaVzUwWDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2Y205aWIzUXZkakV2YldWMFlXUmhkR0V2ZURVd09TOXdkV3hzTFhObFkzSmxkQzEwWlhOMGFXNW5KVFF3WW5WcGJHUXRZM0prTFhSbGMzUnBibWN1YVdGdExtZHpaWEoyYVdObFlXTmpiM1Z1ZEM1amIyMGlDbjA9In19
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: explicit
+ namespace: default
+---
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRoleBinding
+metadata:
+ name: explicit
+subjects:
+ - kind: ServiceAccount
+ name: explicit
+ namespace: default
+roleRef:
+ kind: ClusterRole
+ name: cluster-admin
+ apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: explicit
+ annotations:
+ sidecar.istio.io/inject: "false"
+spec:
+ serviceAccountName: explicit
+ containers:
+ - name: explicit
+ image: github.com/google/go-containerregistry/pkg/authn/k8schain/tests/explicit
+ restartPolicy: Never
diff --git a/pkg/authn/k8schain/tests/implicit/main.go b/pkg/authn/k8schain/tests/implicit/main.go
new file mode 100644
index 0000000..369240f
--- /dev/null
+++ b/pkg/authn/k8schain/tests/implicit/main.go
@@ -0,0 +1,52 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "log"
+ "os"
+
+ "github.com/google/go-containerregistry/pkg/authn/k8schain"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func main() {
+ if len(os.Args) != 2 {
+ log.Fatalf("expected usage: <command> <arg>, got: %v", os.Args)
+ }
+
+ kc, err := k8schain.NewInCluster(context.Background(), k8schain.Options{})
+ if err != nil {
+ log.Fatalf("k8schain.New() = %v", err)
+ }
+
+ ref, err := name.NewDigest(os.Args[1])
+ if err != nil {
+ log.Fatalf("NewDigest() = %v", err)
+ }
+
+ img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc))
+ if err != nil {
+ log.Fatalf("remote.Image() = %v", err)
+ }
+
+ digest, err := img.Digest()
+ if err != nil {
+ log.Fatalf("Digest() = %v", err)
+ }
+ log.Printf("got digest: %v", digest)
+}
diff --git a/pkg/authn/k8schain/tests/implicit/test.yaml b/pkg/authn/k8schain/tests/implicit/test.yaml
new file mode 100644
index 0000000..fba7e33
--- /dev/null
+++ b/pkg/authn/k8schain/tests/implicit/test.yaml
@@ -0,0 +1,47 @@
+# Copyright 2018 Google LLC All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: implicit
+ namespace: default
+---
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRoleBinding
+metadata:
+ name: implicit
+subjects:
+ - kind: ServiceAccount
+ name: implicit
+ namespace: default
+roleRef:
+ kind: ClusterRole
+ name: cluster-admin
+ apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: implicit
+ annotations:
+ sidecar.istio.io/inject: "false"
+spec:
+ serviceAccountName: implicit
+ containers:
+ - name: implicit
+ image: github.com/google/go-containerregistry/pkg/authn/k8schain/tests/implicit
+ args:
+ # This test assumes that the KO_DOCKER_REPO is private.
+ - github.com/google/go-containerregistry/pkg/authn/k8schain/tests/implicit
+ restartPolicy: Never
diff --git a/pkg/authn/k8schain/tests/noauth/main.go b/pkg/authn/k8schain/tests/noauth/main.go
new file mode 100644
index 0000000..cff69fa
--- /dev/null
+++ b/pkg/authn/k8schain/tests/noauth/main.go
@@ -0,0 +1,47 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/google/go-containerregistry/pkg/authn/k8schain"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func main() {
+ kc, err := k8schain.NewInCluster(context.Background(), k8schain.Options{})
+ if err != nil {
+ log.Fatalf("k8schain.New() = %v", err)
+ }
+
+ ref, err := name.ParseReference("ubuntu:latest")
+ if err != nil {
+ log.Fatalf("ParseReference() = %v", err)
+ }
+
+ img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc))
+ if err != nil {
+ log.Fatalf("remote.Image() = %v", err)
+ }
+
+ digest, err := img.Digest()
+ if err != nil {
+ log.Fatalf("Digest() = %v", err)
+ }
+ log.Printf("got digest: %v", digest)
+}
diff --git a/pkg/authn/k8schain/tests/noauth/test.yaml b/pkg/authn/k8schain/tests/noauth/test.yaml
new file mode 100644
index 0000000..a02b302
--- /dev/null
+++ b/pkg/authn/k8schain/tests/noauth/test.yaml
@@ -0,0 +1,44 @@
+# Copyright 2018 Google LLC All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: noauth
+ namespace: default
+---
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRoleBinding
+metadata:
+ name: noauth
+subjects:
+ - kind: ServiceAccount
+ name: noauth
+ namespace: default
+roleRef:
+ kind: ClusterRole
+ name: cluster-admin
+ apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: noauth
+ annotations:
+ sidecar.istio.io/inject: "false"
+spec:
+ serviceAccountName: noauth
+ containers:
+ - name: noauth
+ image: github.com/google/go-containerregistry/pkg/authn/k8schain/tests/noauth
+ restartPolicy: Never
diff --git a/pkg/authn/k8schain/tests/serviceaccount/main.go b/pkg/authn/k8schain/tests/serviceaccount/main.go
new file mode 100644
index 0000000..a895dc6
--- /dev/null
+++ b/pkg/authn/k8schain/tests/serviceaccount/main.go
@@ -0,0 +1,54 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/google/go-containerregistry/pkg/authn/k8schain"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func main() {
+ ref, err := name.NewTag("gcr.io/build-crd-testing/secret-sauce:latest")
+ if err != nil {
+ log.Fatalf("NewTag() = %v", err)
+ }
+
+ kc, err := k8schain.NewInCluster(context.Background(), k8schain.Options{
+ Namespace: "serviceaccount-namespace",
+ ServiceAccountName: "serviceaccount",
+ // This is the name of the imagePullSecrets attached to this service account.
+ // ImagePullSecrets: []string{
+ // "serviceaccount-secret",
+ // },
+ })
+ if err != nil {
+ log.Fatalf("k8schain.New() = %v", err)
+ }
+
+ img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc))
+ if err != nil {
+ log.Fatalf("remote.Image() = %v", err)
+ }
+
+ digest, err := img.Digest()
+ if err != nil {
+ log.Fatalf("Digest() = %v", err)
+ }
+ log.Printf("got digest: %v", digest)
+}
diff --git a/pkg/authn/k8schain/tests/serviceaccount/test.yaml b/pkg/authn/k8schain/tests/serviceaccount/test.yaml
new file mode 100644
index 0000000..f8fa089
--- /dev/null
+++ b/pkg/authn/k8schain/tests/serviceaccount/test.yaml
@@ -0,0 +1,67 @@
+# Copyright 2018 Google LLC All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: serviceaccount-namespace
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: serviceaccount-secret
+ namespace: serviceaccount-namespace
+type: kubernetes.io/dockercfg
+data:
+ # This service account is JUST a storage reader on gcr.io/build-crd-testing
+ .dockercfg: eyJodHRwczovL2djci5pbyI6eyJ1c2VybmFtZSI6Il9qc29uX2tleSIsInBhc3N3b3JkIjoie1xuICBcInR5cGVcIjogXCJzZXJ2aWNlX2FjY291bnRcIixcbiAgXCJwcm9qZWN0X2lkXCI6IFwiYnVpbGQtY3JkLXRlc3RpbmdcIixcbiAgXCJwcml2YXRlX2tleV9pZFwiOiBcIjA1MDJhNDFhODEyZmI2NGNlNTZhNjhlYzU4MzJhYjBiYTExYzExZTZcIixcbiAgXCJwcml2YXRlX2tleVwiOiBcIi0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxcbk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzlYNEVZT0FSYnhRTThcXG5EMnhYY2FaVGsrZ1k4ZWp1OTh0THFDUXFUckdNVzlSZVQyeE9ZNUF5Z2FsUFArcDd5WEVja3dCRC9IaE0wZ2xJXFxuN01UTGRlZUtXcityQTFMd0haeVdGVzdIME9uZjd3bllIRUhMV1VtYzNCQ09SRUR0SFJaN1pyUEJmMUhUQUEvM1xcbk1uVzVsWkhTTjlvanpTU0Z3NkFWdTZqNmF4YkJJSUo3NTRMcmdLZUFZdXJ3ZklRMlJMVHUyMDFrMklxTFliaGJcXG4zbVNWRzVSK3RiS3oxQ3ZNNTNuSENiN0NmdVZlV3NyQThrazd4SHJyTFFLTW1JOXYyc2dSdWd5TUF6d3ovNnpOXFxuaDUvaU14eGdlcTVXOHhrVngzSjJuWThKSmRIYWYvVDZBR3NPTkVvNDNweGVpUVZqblJmL0tuMTBUQ2MyRXNJWVxcblM0OVVzWjdCQWdNQkFBRUNnZ0VBQXVwbGR1a0NRUXVENVUvZ2FtSHQ3R2dXM0FNVjE4ZXFuSG5DYTJqbGFoK1NcXG5BZVVHbmhnSmpOdkUrcE1GbFN2NXVmMnAySzRlZC9veEQ2K0NwOVpYRFJqZ3ZmdEl5cWpsemJ3dkZjZ3p3TnVEXFxueWdVa3VwN0hlY0RzRDhUdGVBb2JUL1Zwd3E2ektNckJ3Q3ZOa3Z5NmJWbG9FajV4M2JYc2F4ZTk1RE8veXB1NlxcbncwVzk3enh3d0RKWTZLUWNJV01qaHJHeHZ3WDduaVVDZU00bGVXQkR5R3R3MXplSm40aEVjNk4zYWpRYWNYS2NcXG4rNFFseGNpYW1ZcVFXYlBudHhXUWhoUXpjSFdMaTJsOWNGYlpENyt1SkxGNGlONnk4bVZOVTNLM0sxYlJZclNEXFxuUlVwM2FVVkJYbUZnK1ovMnB1VkwrbVUzajNMTFdZeUJPa2V2dU9tZGdRS0JnUURlM0dJUWt5V0lTMTRUZE1PU1xcbkJpS0JDRHk4aDk2ZWhMMEhrRGJ5T2tTdFBLZEY5cHVFeFp4aHk3b2pIQ0lNNUZWcnBSTjI1cDRzRXp3RmFjK3ZcXG5KSUZnRXZxN21YZm1YaVhJTmllUG9FUWFDbm54RHhXZ21yMEhVS0VtUzlvTWRnTGNHVStrQ1ZHTnN6N0FPdW0wXFxuS3FZM3MyMlE5bFE2N0ZPeXFpdThXRlE3UVFLQmdRRFppRmhURVprUEVjcVpqbndKcFRCNTZaV1A5S1RzbFpQN1xcbndVNGJ6aTZ5K21leWYzTUorNEwyU3lIYzNjcFNNYmp0Tk9aQ3Q0N2I5MDhGVW1MWFVHTmhjd3VaakVReEZleTBcXG5tNDFjUzVlNFA0OWI5bjZ5TEJqQnJCb3FzMldCYWwyZWdkaE5KU3NDV29pWlA4L1pUOGVnWHZoN2I5MWp6b0syXFxucTJQVW1BNERnUUtCZ0FXTDJJanZFSTBPeXgyUzExY24vZTNXSmFUUGdOUFRHOTAzVXBhK3FuemhPSXgrTWFxaFxcblBGNFdzdUF5MEFvZ0dKd2dOSmJOOEh2S1VzRVR2QTV3eXlOMzlYTjd3MGNoYXJGTDM3b3NVK1dPQXpEam5qY3NcXG5BcTVPN0dQR21YdWI2RUJRQlBKaEpQMXd5NHYvSzFmSGcvRjQ3cTRmNDBMQUpPa2FZUkpENUh6QkFvR0JBTlVoXFxubklCUEpxcTRJTXZRNmNDOWc4QisxeFlEZWE5L1lrMXcrU21QR3Z3ckVYeTNHS3g0SzdsS3BiUHo3bTRYMzNzeFxcbnNFVS8rWTJWUW13UmExeFFtLzUzcks3VjJsNUpmL0Q0MDBqUm02WmZTQU92Z0RUcnRablVHSk1yejlFN3VOdzdcXG5sZ1VIM0pyaXZ5Ri9meE1JOHFzelFid1hQMCt4bnlxQXhFQWdkdUtCQW9HQUlNK1BTTllXQ1pYeERwU0hJMThkXFxuaktrb0FidzJNb3l3UUlsa2V1QW4xZFhGYWQxenNYUUdkVHJtWHl2N05QUCs4R1hCa25CTGkzY3Z4VGlsSklTeVxcbnVjTnJDTWlxTkFTbi9kcTdjV0RGVUFCZ2pYMTZKSDJETkZaL2wvVVZGM05EQUpqWENzMVg3eUlKeVhCNm94L3pcXG5hU2xxbElNVjM1REJEN3F4Unl1S3Nnaz1cXG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXFxuXCIsXG4gIFwiY2xpZW50X2VtYWlsXCI6IFwicHVsbC1zZWNyZXQtdGVzdGluZ0BidWlsZC1jcmQtdGVzdGluZy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbVwiLFxuICBcImNsaWVudF9pZFwiOiBcIjEwNzkzNTg2MjAzMzAyNTI1MTM1MlwiLFxuICBcImF1dGhfdXJpXCI6IFwiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGhcIixcbiAgXCJ0b2tlbl91cmlcIjogXCJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW5cIixcbiAgXCJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmxcIjogXCJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHNcIixcbiAgXCJjbGllbnRfeDUwOV9jZXJ0X3VybFwiOiBcImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL3JvYm90L3YxL21ldGFkYXRhL3g1MDkvcHVsbC1zZWNyZXQtdGVzdGluZyU0MGJ1aWxkLWNyZC10ZXN0aW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tXCJcbn0iLCJlbWFpbCI6Im5vcmVwbHlAZ29vZ2xlLmNvbSIsImF1dGgiOiJYMnB6YjI1ZmEyVjVPbnNLSUNBaWRIbHdaU0k2SUNKelpYSjJhV05sWDJGalkyOTFiblFpTEFvZ0lDSndjbTlxWldOMFgybGtJam9nSW1KMWFXeGtMV055WkMxMFpYTjBhVzVuSWl3S0lDQWljSEpwZG1GMFpWOXJaWGxmYVdRaU9pQWlNRFV3TW1FME1XRTRNVEptWWpZMFkyVTFObUUyT0dWak5UZ3pNbUZpTUdKaE1URmpNVEZsTmlJc0NpQWdJbkJ5YVhaaGRHVmZhMlY1SWpvZ0lpMHRMUzB0UWtWSFNVNGdVRkpKVmtGVVJTQkxSVmt0TFMwdExWeHVUVWxKUlhaUlNVSkJSRUZPUW1kcmNXaHJhVWM1ZHpCQ1FWRkZSa0ZCVTBOQ1MyTjNaMmRUYWtGblJVRkJiMGxDUVZGRE9WZzBSVmxQUVZKaWVGRk5PRnh1UkRKNFdHTmhXbFJySzJkWk9HVnFkVGs0ZEV4eFExRnhWSEpIVFZjNVVtVlVNbmhQV1RWQmVXZGhiRkJRSzNBM2VWaEZZMnQzUWtRdlNHaE5NR2RzU1Z4dU4wMVVUR1JsWlV0WGNpdHlRVEZNZDBoYWVWZEdWemRJTUU5dVpqZDNibGxJUlVoTVYxVnRZek5DUTA5U1JVUjBTRkphTjFweVVFSm1NVWhVUVVFdk0xeHVUVzVYTld4YVNGTk9PVzlxZWxOVFJuYzJRVloxTm1vMllYaGlRa2xKU2pjMU5FeHlaMHRsUVZsMWNuZG1TVkV5VWt4VWRUSXdNV3N5U1hGTVdXSm9ZbHh1TTIxVFZrYzFVaXQwWWt0Nk1VTjJUVFV6YmtoRFlqZERablZXWlZkemNrRTRhMnMzZUVoeWNreFJTMDF0U1RsMk1uTm5VblZuZVUxQmVuZDZMelo2VGx4dWFEVXZhVTE0ZUdkbGNUVlhPSGhyVm5nelNqSnVXVGhLU21SSVlXWXZWRFpCUjNOUFRrVnZORE53ZUdWcFVWWnFibEptTDB0dU1UQlVRMk15UlhOSldWeHVVelE1VlhOYU4wSkJaMDFDUVVGRlEyZG5SVUZCZFhCc1pIVnJRMUZSZFVRMVZTOW5ZVzFJZERkSFoxY3pRVTFXTVRobGNXNUlia05oTW1wc1lXZ3JVMXh1UVdWVlIyNW9aMHBxVG5aRkszQk5SbXhUZGpWMVpqSndNa3MwWldRdmIzaEVOaXREY0RsYVdFUlNhbWQyWm5SSmVYRnFiSHBpZDNaR1kyZDZkMDUxUkZ4dWVXZFZhM1Z3TjBobFkwUnpSRGhVZEdWQmIySlVMMVp3ZDNFMmVrdE5ja0ozUTNaT2EzWjVObUpXYkc5RmFqVjRNMkpZYzJGNFpUazFSRTh2ZVhCMU5seHVkekJYT1RkNmVIZDNSRXBaTmt0UlkwbFhUV3BvY2tkNGRuZFlOMjVwVlVObFRUUnNaVmRDUkhsSGRIY3hlbVZLYmpSb1JXTTJUak5oYWxGaFkxaExZMXh1S3pSUmJIaGphV0Z0V1hGUlYySlFiblI0VjFGb2FGRjZZMGhYVEdreWJEbGpSbUphUkRjcmRVcE1SalJwVGpaNU9HMVdUbFV6U3pOTE1XSlNXWEpUUkZ4dVVsVndNMkZWVmtKWWJVWm5LMW92TW5CMVZrd3JiVlV6YWpOTVRGZFplVUpQYTJWMmRVOXRaR2RSUzBKblVVUmxNMGRKVVd0NVYwbFRNVFJVWkUxUFUxeHVRbWxMUWtORWVUaG9PVFpsYUV3d1NHdEVZbmxQYTFOMFVFdGtSamx3ZFVWNFduaG9lVGR2YWtoRFNVMDFSbFp5Y0ZKT01qVndOSE5GZW5kR1lXTXJkbHh1U2tsR1owVjJjVGR0V0dadFdHbFlTVTVwWlZCdlJWRmhRMjV1ZUVSNFYyZHRjakJJVlV0RmJWTTViMDFrWjB4alIxVXJhME5XUjA1emVqZEJUM1Z0TUZ4dVMzRlpNM015TWxFNWJGRTJOMFpQZVhGcGRUaFhSbEUzVVZGTFFtZFJSRnBwUm1oVVJWcHJVRVZqY1ZwcWJuZEtjRlJDTlRaYVYxQTVTMVJ6YkZwUU4xeHVkMVUwWW5wcE5ua3JiV1Y1WmpOTlNpczBUREpUZVVoak0yTndVMDFpYW5ST1QxcERkRFEzWWprd09FWlZiVXhZVlVkT2FHTjNkVnBxUlZGNFJtVjVNRnh1YlRReFkxTTFaVFJRTkRsaU9XNDJlVXhDYWtKeVFtOXhjekpYUW1Gc01tVm5aR2hPU2xOelExZHZhVnBRT0M5YVZEaGxaMWgyYURkaU9URnFlbTlMTWx4dWNUSlFWVzFCTkVSblVVdENaMEZYVERKSmFuWkZTVEJQZVhneVV6RXhZMjR2WlROWFNtRlVVR2RPVUZSSE9UQXpWWEJoSzNGdWVtaFBTWGdyVFdGeGFGeHVVRVkwVjNOMVFYa3dRVzluUjBwM1owNUtZazQ0U0haTFZYTkZWSFpCTlhkNWVVNHpPVmhPTjNjd1kyaGhja1pNTXpkdmMxVXJWMDlCZWtScWJtcGpjMXh1UVhFMVR6ZEhVRWR0V0hWaU5rVkNVVUpRU21oS1VERjNlVFIyTDBzeFpraG5MMFkwTjNFMFpqUXdURUZLVDJ0aFdWSktSRFZJZWtKQmIwZENRVTVWYUZ4dWJrbENVRXB4Y1RSSlRYWlJObU5ET1djNFFpc3hlRmxFWldFNUwxbHJNWGNyVTIxUVIzWjNja1ZZZVROSFMzZzBTemRzUzNCaVVIbzNiVFJZTXpOemVGeHVjMFZWTHl0Wk1sWlJiWGRTWVRGNFVXMHZOVE55U3pkV01tdzFTbVl2UkRRd01HcFNiVFphWmxOQlQzWm5SRlJ5ZEZwdVZVZEtUWEo2T1VVM2RVNTNOMXh1YkdkVlNETktjbWwyZVVZdlpuaE5TVGh4YzNwUlluZFlVREFyZUc1NWNVRjRSVUZuWkhWTFFrRnZSMEZKVFN0UVUwNVpWME5hV0hoRWNGTklTVEU0WkZ4dWFrdHJiMEZpZHpKTmIzbDNVVWxzYTJWMVFXNHhaRmhHWVdReGVuTllVVWRrVkhKdFdIbDJOMDVRVUNzNFIxaENhMjVDVEdrelkzWjRWR2xzU2tsVGVWeHVkV05PY2tOTmFYRk9RVk51TDJSeE4yTlhSRVpWUVVKbmFsZ3hOa3BJTWtST1Jsb3ZiQzlWVmtZelRrUkJTbXBZUTNNeFdEZDVTVXA1V0VJMmIzZ3ZlbHh1WVZOc2NXeEpUVll6TlVSQ1JEZHhlRko1ZFV0eloyczlYRzR0TFMwdExVVk9SQ0JRVWtsV1FWUkZJRXRGV1MwdExTMHRYRzRpTEFvZ0lDSmpiR2xsYm5SZlpXMWhhV3dpT2lBaWNIVnNiQzF6WldOeVpYUXRkR1Z6ZEdsdVowQmlkV2xzWkMxamNtUXRkR1Z6ZEdsdVp5NXBZVzB1WjNObGNuWnBZMlZoWTJOdmRXNTBMbU52YlNJc0NpQWdJbU5zYVdWdWRGOXBaQ0k2SUNJeE1EYzVNelU0TmpJd016TXdNalV5TlRFek5USWlMQW9nSUNKaGRYUm9YM1Z5YVNJNklDSm9kSFJ3Y3pvdkwyRmpZMjkxYm5SekxtZHZiMmRzWlM1amIyMHZieTl2WVhWMGFESXZZWFYwYUNJc0NpQWdJblJ2YTJWdVgzVnlhU0k2SUNKb2RIUndjem92TDJGalkyOTFiblJ6TG1kdmIyZHNaUzVqYjIwdmJ5OXZZWFYwYURJdmRHOXJaVzRpTEFvZ0lDSmhkWFJvWDNCeWIzWnBaR1Z5WDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2YjJGMWRHZ3lMM1l4TDJObGNuUnpJaXdLSUNBaVkyeHBaVzUwWDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2Y205aWIzUXZkakV2YldWMFlXUmhkR0V2ZURVd09TOXdkV3hzTFhObFkzSmxkQzEwWlhOMGFXNW5KVFF3WW5WcGJHUXRZM0prTFhSbGMzUnBibWN1YVdGdExtZHpaWEoyYVdObFlXTmpiM1Z1ZEM1amIyMGlDbjA9In19
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: serviceaccount
+ namespace: serviceaccount-namespace
+imagePullSecrets:
+- name: serviceaccount-secret
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: serviceaccount
+ namespace: default
+---
+apiVersion: rbac.authorization.k8s.io/v1beta1
+kind: ClusterRoleBinding
+metadata:
+ name: serviceaccount
+subjects:
+ - kind: ServiceAccount
+ name: serviceaccount
+ namespace: default
+roleRef:
+ kind: ClusterRole
+ name: cluster-admin
+ apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: serviceaccount
+ annotations:
+ sidecar.istio.io/inject: "false"
+spec:
+ serviceAccountName: serviceaccount
+ containers:
+ - name: serviceaccount
+ image: github.com/google/go-containerregistry/pkg/authn/k8schain/tests/serviceaccount
+ restartPolicy: Never
diff --git a/pkg/authn/keychain.go b/pkg/authn/keychain.go
new file mode 100644
index 0000000..a4a88b3
--- /dev/null
+++ b/pkg/authn/keychain.go
@@ -0,0 +1,180 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+import (
+ "os"
+ "path/filepath"
+ "sync"
+
+ "github.com/docker/cli/cli/config"
+ "github.com/docker/cli/cli/config/configfile"
+ "github.com/docker/cli/cli/config/types"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/mitchellh/go-homedir"
+)
+
+// Resource represents a registry or repository that can be authenticated against.
+type Resource interface {
+ // String returns the full string representation of the target, e.g.
+ // gcr.io/my-project or just gcr.io.
+ String() string
+
+ // RegistryStr returns just the registry portion of the target, e.g. for
+ // gcr.io/my-project, this should just return gcr.io. This is needed to
+ // pull out an appropriate hostname.
+ RegistryStr() string
+}
+
+// Keychain is an interface for resolving an image reference to a credential.
+type Keychain interface {
+ // Resolve looks up the most appropriate credential for the specified target.
+ Resolve(Resource) (Authenticator, error)
+}
+
+// defaultKeychain implements Keychain with the semantics of the standard Docker
+// credential keychain.
+type defaultKeychain struct {
+ mu sync.Mutex
+}
+
+var (
+ // DefaultKeychain implements Keychain by interpreting the docker config file.
+ DefaultKeychain Keychain = &defaultKeychain{}
+)
+
+const (
+ // DefaultAuthKey is the key used for dockerhub in config files, which
+ // is hardcoded for historical reasons.
+ DefaultAuthKey = "https://" + name.DefaultRegistry + "/v1/"
+)
+
+// Resolve implements Keychain.
+func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
+ dk.mu.Lock()
+ defer dk.mu.Unlock()
+
+ // Podman users may have their container registry auth configured in a
+ // different location, that Docker packages aren't aware of.
+ // If the Docker config file isn't found, we'll fallback to look where
+ // Podman configures it, and parse that as a Docker auth config instead.
+
+ // First, check $HOME/.docker/config.json
+ foundDockerConfig := false
+ home, err := homedir.Dir()
+ if err == nil {
+ foundDockerConfig = fileExists(filepath.Join(home, ".docker/config.json"))
+ }
+ // If $HOME/.docker/config.json isn't found, check $DOCKER_CONFIG (if set)
+ if !foundDockerConfig && os.Getenv("DOCKER_CONFIG") != "" {
+ foundDockerConfig = fileExists(filepath.Join(os.Getenv("DOCKER_CONFIG"), "config.json"))
+ }
+ // If either of those locations are found, load it using Docker's
+ // config.Load, which may fail if the config can't be parsed.
+ //
+ // If neither was found, look for Podman's auth at
+ // $XDG_RUNTIME_DIR/containers/auth.json and attempt to load it as a
+ // Docker config.
+ //
+ // If neither are found, fallback to Anonymous.
+ var cf *configfile.ConfigFile
+ if foundDockerConfig {
+ cf, err = config.Load(os.Getenv("DOCKER_CONFIG"))
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ f, err := os.Open(filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers/auth.json"))
+ if err != nil {
+ return Anonymous, nil
+ }
+ defer f.Close()
+ cf, err = config.LoadFromReader(f)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // See:
+ // https://github.com/google/ko/issues/90
+ // https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404
+ var cfg, empty types.AuthConfig
+ for _, key := range []string{
+ target.String(),
+ target.RegistryStr(),
+ } {
+ if key == name.DefaultRegistry {
+ key = DefaultAuthKey
+ }
+
+ cfg, err = cf.GetAuthConfig(key)
+ if err != nil {
+ return nil, err
+ }
+ // cf.GetAuthConfig automatically sets the ServerAddress attribute. Since
+ // we don't make use of it, clear the value for a proper "is-empty" test.
+ // See: https://github.com/google/go-containerregistry/issues/1510
+ cfg.ServerAddress = ""
+ if cfg != empty {
+ break
+ }
+ }
+ if cfg == empty {
+ return Anonymous, nil
+ }
+
+ return FromConfig(AuthConfig{
+ Username: cfg.Username,
+ Password: cfg.Password,
+ Auth: cfg.Auth,
+ IdentityToken: cfg.IdentityToken,
+ RegistryToken: cfg.RegistryToken,
+ }), nil
+}
+
+// fileExists returns true if the given path exists and is not a directory.
+func fileExists(path string) bool {
+ fi, err := os.Stat(path)
+ return err == nil && !fi.IsDir()
+}
+
+// Helper is a subset of the Docker credential helper credentials.Helper
+// interface used by NewKeychainFromHelper.
+//
+// See:
+// https://pkg.go.dev/github.com/docker/docker-credential-helpers/credentials#Helper
+type Helper interface {
+ Get(serverURL string) (string, string, error)
+}
+
+// NewKeychainFromHelper returns a Keychain based on a Docker credential helper
+// implementation that can Get username and password credentials for a given
+// server URL.
+func NewKeychainFromHelper(h Helper) Keychain { return wrapper{h} }
+
+type wrapper struct{ h Helper }
+
+func (w wrapper) Resolve(r Resource) (Authenticator, error) {
+ u, p, err := w.h.Get(r.RegistryStr())
+ if err != nil {
+ return Anonymous, nil
+ }
+ // If the secret being stored is an identity token, the Username should be set to <token>
+ // ref: https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol
+ if u == "<token>" {
+ return FromConfig(AuthConfig{Username: u, IdentityToken: p}), nil
+ }
+ return FromConfig(AuthConfig{Username: u, Password: p}), nil
+}
diff --git a/pkg/authn/keychain_test.go b/pkg/authn/keychain_test.go
new file mode 100644
index 0000000..9dfbad1
--- /dev/null
+++ b/pkg/authn/keychain_test.go
@@ -0,0 +1,392 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+import (
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+var (
+ fresh = 0
+ testRegistry, _ = name.NewRegistry("test.io", name.WeakValidation)
+ testRepo, _ = name.NewRepository("test.io/my-repo", name.WeakValidation)
+ defaultRegistry, _ = name.NewRegistry(name.DefaultRegistry, name.WeakValidation)
+)
+
+func TestMain(m *testing.M) {
+ // Set $HOME to a temp empty dir, to ensure $HOME/.docker/config.json
+ // isn't unexpectedly found.
+ tmp, err := os.MkdirTemp("", "keychain_test_home")
+ if err != nil {
+ log.Fatal(err)
+ }
+ os.Setenv("HOME", tmp)
+ os.Exit(func() int {
+ defer os.RemoveAll(tmp)
+ return m.Run()
+ }())
+}
+
+// setupConfigDir sets up an isolated configDir() for this test.
+func setupConfigDir(t *testing.T) string {
+ tmpdir := os.Getenv("TEST_TMPDIR")
+ if tmpdir == "" {
+ tmpdir = t.TempDir()
+ }
+
+ fresh++
+ p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh))
+ t.Logf("DOCKER_CONFIG=%s", p)
+ t.Setenv("DOCKER_CONFIG", p)
+ if err := os.Mkdir(p, 0777); err != nil {
+ t.Fatalf("mkdir %q: %v", p, err)
+ }
+ return p
+}
+
+func setupConfigFile(t *testing.T, content string) string {
+ cd := setupConfigDir(t)
+ p := filepath.Join(cd, "config.json")
+ if err := os.WriteFile(p, []byte(content), 0600); err != nil {
+ t.Fatalf("write %q: %v", p, err)
+ }
+
+ // return the config dir so we can clean up
+ return cd
+}
+
+func TestNoConfig(t *testing.T) {
+ cd := setupConfigDir(t)
+ defer os.RemoveAll(filepath.Dir(cd))
+
+ auth, err := DefaultKeychain.Resolve(testRegistry)
+ if err != nil {
+ t.Fatalf("Resolve() = %v", err)
+ }
+
+ if auth != Anonymous {
+ t.Errorf("expected Anonymous, got %v", auth)
+ }
+}
+
+func TestPodmanConfig(t *testing.T) {
+ tmpdir := os.Getenv("TEST_TMPDIR")
+ if tmpdir == "" {
+ tmpdir = t.TempDir()
+ }
+ fresh++
+ p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh))
+ t.Setenv("XDG_RUNTIME_DIR", p)
+ os.Unsetenv("DOCKER_CONFIG")
+ if err := os.MkdirAll(filepath.Join(p, "containers"), 0777); err != nil {
+ t.Fatalf("mkdir %s/containers: %v", p, err)
+ }
+ cfg := filepath.Join(p, "containers/auth.json")
+ content := fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar"))
+ if err := os.WriteFile(cfg, []byte(content), 0600); err != nil {
+ t.Fatalf("write %q: %v", cfg, err)
+ }
+
+ // At first, $DOCKER_CONFIG is unset and $HOME/.docker/config.json isn't
+ // found, but Podman auth is configured. This should return Podman's
+ // auth.
+ auth, err := DefaultKeychain.Resolve(testRegistry)
+ if err != nil {
+ t.Fatalf("Resolve() = %v", err)
+ }
+ got, err := auth.Authorization()
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := &AuthConfig{
+ Username: "foo",
+ Password: "bar",
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %+v, want %+v", got, want)
+ }
+
+ // Now, configure $HOME/.docker/config.json, which should override
+ // Podman auth and be used.
+ if err := os.MkdirAll(filepath.Join(os.Getenv("HOME"), ".docker"), 0777); err != nil {
+ t.Fatalf("mkdir $HOME/.docker: %v", err)
+ }
+ cfg = filepath.Join(os.Getenv("HOME"), ".docker/config.json")
+ content = fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("home-foo", "home-bar"))
+ if err := os.WriteFile(cfg, []byte(content), 0600); err != nil {
+ t.Fatalf("write %q: %v", cfg, err)
+ }
+ defer func() { os.Remove(cfg) }()
+ auth, err = DefaultKeychain.Resolve(testRegistry)
+ if err != nil {
+ t.Fatalf("Resolve() = %v", err)
+ }
+ got, err = auth.Authorization()
+ if err != nil {
+ t.Fatal(err)
+ }
+ want = &AuthConfig{
+ Username: "home-foo",
+ Password: "home-bar",
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %+v, want %+v", got, want)
+ }
+
+ // Then, configure DOCKER_CONFIG with a valid config file with different
+ // auth configured.
+ // This demonstrates that DOCKER_CONFIG is preferred over Podman auth
+ // and $HOME/.docker/config.json.
+ content = fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("another-foo", "another-bar"))
+ cd := setupConfigFile(t, content)
+ defer os.RemoveAll(filepath.Dir(cd))
+
+ auth, err = DefaultKeychain.Resolve(testRegistry)
+ if err != nil {
+ t.Fatalf("Resolve() = %v", err)
+ }
+ got, err = auth.Authorization()
+ if err != nil {
+ t.Fatal(err)
+ }
+ want = &AuthConfig{
+ Username: "another-foo",
+ Password: "another-bar",
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %+v, want %+v", got, want)
+ }
+}
+
+func encode(user, pass string) string {
+ delimited := fmt.Sprintf("%s:%s", user, pass)
+ return base64.StdEncoding.EncodeToString([]byte(delimited))
+}
+
+func TestVariousPaths(t *testing.T) {
+ tests := []struct {
+ desc string
+ content string
+ wantErr bool
+ target Resource
+ cfg *AuthConfig
+ anonymous bool
+ }{{
+ desc: "invalid config file",
+ target: testRegistry,
+ content: `}{`,
+ wantErr: true,
+ }, {
+ desc: "creds store does not exist",
+ target: testRegistry,
+ content: `{"credsStore":"#definitely-does-not-exist"}`,
+ wantErr: true,
+ }, {
+ desc: "valid config file",
+ target: testRegistry,
+ content: fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar")),
+ cfg: &AuthConfig{
+ Username: "foo",
+ Password: "bar",
+ },
+ }, {
+ desc: "valid config file; default registry",
+ target: defaultRegistry,
+ content: fmt.Sprintf(`{"auths": {"%s": {"auth": %q}}}`, DefaultAuthKey, encode("foo", "bar")),
+ cfg: &AuthConfig{
+ Username: "foo",
+ Password: "bar",
+ },
+ }, {
+ desc: "valid config file; matches registry w/ v1",
+ target: testRegistry,
+ content: fmt.Sprintf(`{
+ "auths": {
+ "http://test.io/v1/": {"auth": %q}
+ }
+ }`, encode("baz", "quux")),
+ cfg: &AuthConfig{
+ Username: "baz",
+ Password: "quux",
+ },
+ }, {
+ desc: "valid config file; matches registry w/ v2",
+ target: testRegistry,
+ content: fmt.Sprintf(`{
+ "auths": {
+ "http://test.io/v2/": {"auth": %q}
+ }
+ }`, encode("baz", "quux")),
+ cfg: &AuthConfig{
+ Username: "baz",
+ Password: "quux",
+ },
+ }, {
+ desc: "valid config file; matches repo",
+ target: testRepo,
+ content: fmt.Sprintf(`{
+ "auths": {
+ "test.io/my-repo": {"auth": %q},
+ "test.io/another-repo": {"auth": %q},
+ "test.io": {"auth": %q}
+ }
+}`, encode("foo", "bar"), encode("bar", "baz"), encode("baz", "quux")),
+ cfg: &AuthConfig{
+ Username: "foo",
+ Password: "bar",
+ },
+ }, {
+ desc: "ignore unrelated repo",
+ target: testRepo,
+ content: fmt.Sprintf(`{
+ "auths": {
+ "test.io/another-repo": {"auth": %q},
+ "test.io": {}
+ }
+}`, encode("bar", "baz")),
+ cfg: &AuthConfig{},
+ anonymous: true,
+ }}
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ cd := setupConfigFile(t, test.content)
+ // For some reason, these tempdirs don't get cleaned up.
+ defer os.RemoveAll(filepath.Dir(cd))
+
+ auth, err := DefaultKeychain.Resolve(test.target)
+ if test.wantErr {
+ if err == nil {
+ t.Fatal("wanted err, got nil")
+ } else if err != nil {
+ // success
+ return
+ }
+ }
+ if err != nil {
+ t.Fatalf("wanted nil, got err: %v", err)
+ }
+ cfg, err := auth.Authorization()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !reflect.DeepEqual(cfg, test.cfg) {
+ t.Errorf("got %+v, want %+v", cfg, test.cfg)
+ }
+
+ if test.anonymous != (auth == Anonymous) {
+ t.Fatalf("unexpected anonymous authenticator")
+ }
+ })
+ }
+}
+
+type helper struct {
+ u, p string
+ err error
+}
+
+func (h helper) Get(serverURL string) (string, string, error) {
+ if serverURL != "example.com" {
+ return "", "", fmt.Errorf("unexpected serverURL: %s", serverURL)
+ }
+ return h.u, h.p, h.err
+}
+
+func TestNewKeychainFromHelper(t *testing.T) {
+ var repo = name.MustParseReference("example.com/my/repo").Context()
+
+ t.Run("success", func(t *testing.T) {
+ kc := NewKeychainFromHelper(helper{"username", "password", nil})
+ auth, err := kc.Resolve(repo)
+ if err != nil {
+ t.Fatalf("Resolve(%q): %v", repo, err)
+ }
+ cfg, err := auth.Authorization()
+ if err != nil {
+ t.Fatalf("Authorization: %v", err)
+ }
+ if got, want := cfg.Username, "username"; got != want {
+ t.Errorf("Username: got %q, want %q", got, want)
+ }
+ if got, want := cfg.IdentityToken, ""; got != want {
+ t.Errorf("IdentityToken: got %q, want %q", got, want)
+ }
+ if got, want := cfg.Password, "password"; got != want {
+ t.Errorf("Password: got %q, want %q", got, want)
+ }
+ })
+
+ t.Run("success; identity token", func(t *testing.T) {
+ kc := NewKeychainFromHelper(helper{"<token>", "idtoken", nil})
+ auth, err := kc.Resolve(repo)
+ if err != nil {
+ t.Fatalf("Resolve(%q): %v", repo, err)
+ }
+ cfg, err := auth.Authorization()
+ if err != nil {
+ t.Fatalf("Authorization: %v", err)
+ }
+ if got, want := cfg.Username, "<token>"; got != want {
+ t.Errorf("Username: got %q, want %q", got, want)
+ }
+ if got, want := cfg.IdentityToken, "idtoken"; got != want {
+ t.Errorf("IdentityToken: got %q, want %q", got, want)
+ }
+ if got, want := cfg.Password, ""; got != want {
+ t.Errorf("Password: got %q, want %q", got, want)
+ }
+ })
+
+ t.Run("failure", func(t *testing.T) {
+ kc := NewKeychainFromHelper(helper{"", "", errors.New("oh no bad")})
+ auth, err := kc.Resolve(repo)
+ if err != nil {
+ t.Fatalf("Resolve(%q): %v", repo, err)
+ }
+ if auth != Anonymous {
+ t.Errorf("Resolve: got %v, want %v", auth, Anonymous)
+ }
+ })
+}
+
+func TestConfigFileIsADir(t *testing.T) {
+ tmpdir := setupConfigDir(t)
+ // Create "config.json" as a directory, not a file to simulate optional
+ // secrets in Kubernetes.
+ err := os.Mkdir(path.Join(tmpdir, "config.json"), 0777)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ auth, err := DefaultKeychain.Resolve(testRegistry)
+ if err != nil {
+ t.Fatalf("Resolve() = %v", err)
+ }
+ if auth != Anonymous {
+ t.Errorf("expected Anonymous, got %v", auth)
+ }
+}
diff --git a/pkg/authn/kubernetes/go.mod b/pkg/authn/kubernetes/go.mod
new file mode 100644
index 0000000..ba2f45c
--- /dev/null
+++ b/pkg/authn/kubernetes/go.mod
@@ -0,0 +1,59 @@
+module github.com/google/go-containerregistry/pkg/authn/kubernetes
+
+go 1.18
+
+replace github.com/google/go-containerregistry => ../../../
+
+require (
+ github.com/google/go-cmp v0.5.9
+ github.com/google/go-containerregistry v0.13.0
+ k8s.io/api v0.26.2
+ k8s.io/apimachinery v0.26.2
+ k8s.io/client-go v0.26.2
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/docker/cli v23.0.1+incompatible // indirect
+ github.com/docker/docker v23.0.1+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.7.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.10.2 // indirect
+ github.com/evanphx/json-patch v4.12.0+incompatible // indirect
+ github.com/go-logr/logr v1.2.3 // indirect
+ github.com/go-openapi/jsonpointer v0.19.6 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
+ github.com/go-openapi/swag v0.22.3 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/gnostic v0.6.9 // indirect
+ github.com/google/gofuzz v1.2.0 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/sirupsen/logrus v1.9.0 // indirect
+ golang.org/x/net v0.8.0 // indirect
+ golang.org/x/oauth2 v0.6.0 // indirect
+ golang.org/x/sys v0.6.0 // indirect
+ golang.org/x/term v0.6.0 // indirect
+ golang.org/x/text v0.8.0 // indirect
+ golang.org/x/time v0.3.0 // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/protobuf v1.29.0 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ gotest.tools/v3 v3.1.0 // indirect
+ k8s.io/klog/v2 v2.90.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect
+ k8s.io/utils v0.0.0-20230308161112-d77c459e9343 // indirect
+ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
+ sigs.k8s.io/yaml v1.3.0 // indirect
+)
diff --git a/pkg/authn/kubernetes/go.sum b/pkg/authn/kubernetes/go.sum
new file mode 100644
index 0000000..df8143e
--- /dev/null
+++ b/pkg/authn/kubernetes/go.sum
@@ -0,0 +1,276 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM=
+github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY=
+github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
+github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE=
+github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
+github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
+github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
+github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs=
+github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
+google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk=
+gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.26.2 h1:dM3cinp3PGB6asOySalOZxEG4CZ0IAdJsrYZXE/ovGQ=
+k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU=
+k8s.io/apimachinery v0.26.2 h1:da1u3D5wfR5u2RpLhE/ZtZS2P7QvDgLZTi9wrNZl/tQ=
+k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I=
+k8s.io/client-go v0.26.2 h1:s1WkVujHX3kTp4Zn4yGNFK+dlDXy1bAAkIl+cFAiuYI=
+k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU=
+k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
+k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg=
+k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
+k8s.io/utils v0.0.0-20230308161112-d77c459e9343 h1:m7tbIjXGcGIAtpmQr7/NAi7RsWoW3E7Zcm4jI1HicTc=
+k8s.io/utils v0.0.0-20230308161112-d77c459e9343/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
+sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/pkg/authn/kubernetes/keychain.go b/pkg/authn/kubernetes/keychain.go
new file mode 100644
index 0000000..368d829
--- /dev/null
+++ b/pkg/authn/kubernetes/keychain.go
@@ -0,0 +1,331 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package kubernetes
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/url"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/logs"
+ corev1 "k8s.io/api/core/v1"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+)
+
+const (
+ // NoServiceAccount is a constant that can be passed via ServiceAccountName
+ // to tell the keychain that looking up the service account is unnecessary.
+ // This value cannot collide with an actual service account name because
+ // service accounts do not allow spaces.
+ NoServiceAccount = "no service account"
+)
+
+// Options holds configuration data for guiding credential resolution.
+type Options struct {
+ // Namespace holds the namespace inside of which we are resolving service
+ // account and pull secret references to access the image.
+ // If empty, "default" is assumed.
+ Namespace string
+
+ // ServiceAccountName holds the serviceaccount (within Namespace) as which a
+ // Pod might access the image. Service accounts may have image pull secrets
+ // attached, so we lookup the service account to complete the keychain.
+ // If empty, "default" is assumed. To avoid a service account lookup, pass
+ // NoServiceAccount explicitly.
+ ServiceAccountName string
+
+ // ImagePullSecrets holds the names of the Kubernetes secrets (scoped to
+ // Namespace) containing credential data to use for the image pull.
+ ImagePullSecrets []string
+
+ // UseMountSecrets determines whether or not mount secrets in the ServiceAccount
+ // should be considered. Mount secrets are those listed under the `.secrets`
+ // attribute of the ServiceAccount resource. Ignored if ServiceAccountName is set
+ // to NoServiceAccount.
+ UseMountSecrets bool
+}
+
+// New returns a new authn.Keychain suitable for resolving image references as
+// scoped by the provided Options. It speaks to Kubernetes through the provided
+// client interface.
+func New(ctx context.Context, client kubernetes.Interface, opt Options) (authn.Keychain, error) {
+ if opt.Namespace == "" {
+ opt.Namespace = "default"
+ }
+ if opt.ServiceAccountName == "" {
+ opt.ServiceAccountName = "default"
+ }
+
+ // Implement a Kubernetes-style authentication keychain.
+ // This needs to support roughly the following kinds of authentication:
+ // 1) The explicit authentication from imagePullSecrets on Pod
+ // 2) The semi-implicit authentication where imagePullSecrets are on the
+ // Pod's service account.
+
+ // First, fetch all of the explicitly declared pull secrets
+ var pullSecrets []corev1.Secret
+ for _, name := range opt.ImagePullSecrets {
+ ps, err := client.CoreV1().Secrets(opt.Namespace).Get(ctx, name, metav1.GetOptions{})
+ if k8serrors.IsNotFound(err) {
+ logs.Warn.Printf("secret %s/%s not found; ignoring", opt.Namespace, name)
+ continue
+ } else if err != nil {
+ return nil, err
+ }
+ pullSecrets = append(pullSecrets, *ps)
+ }
+
+ // Second, fetch all of the pull secrets attached to our service account,
+ // unless the user has explicitly specified that no service account lookup
+ // is desired.
+ if opt.ServiceAccountName != NoServiceAccount {
+ sa, err := client.CoreV1().ServiceAccounts(opt.Namespace).Get(ctx, opt.ServiceAccountName, metav1.GetOptions{})
+ if k8serrors.IsNotFound(err) {
+ logs.Warn.Printf("serviceaccount %s/%s not found; ignoring", opt.Namespace, opt.ServiceAccountName)
+ } else if err != nil {
+ return nil, err
+ }
+ if sa != nil {
+ for _, localObj := range sa.ImagePullSecrets {
+ ps, err := client.CoreV1().Secrets(opt.Namespace).Get(ctx, localObj.Name, metav1.GetOptions{})
+ if k8serrors.IsNotFound(err) {
+ logs.Warn.Printf("secret %s/%s not found; ignoring", opt.Namespace, localObj.Name)
+ continue
+ } else if err != nil {
+ return nil, err
+ }
+ pullSecrets = append(pullSecrets, *ps)
+ }
+
+ if opt.UseMountSecrets {
+ for _, obj := range sa.Secrets {
+ s, err := client.CoreV1().Secrets(opt.Namespace).Get(ctx, obj.Name, metav1.GetOptions{})
+ if k8serrors.IsNotFound(err) {
+ logs.Warn.Printf("secret %s/%s not found; ignoring", opt.Namespace, obj.Name)
+ continue
+ } else if err != nil {
+ return nil, err
+ }
+ pullSecrets = append(pullSecrets, *s)
+ }
+ }
+ }
+ }
+
+ return NewFromPullSecrets(ctx, pullSecrets)
+}
+
+// NewInCluster returns a new authn.Keychain suitable for resolving image references as
+// scoped by the provided Options, constructing a kubernetes.Interface based on in-cluster
+// authentication.
+func NewInCluster(ctx context.Context, opt Options) (authn.Keychain, error) {
+ clusterConfig, err := rest.InClusterConfig()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := kubernetes.NewForConfig(clusterConfig)
+ if err != nil {
+ return nil, err
+ }
+ return New(ctx, client, opt)
+}
+
+type dockerConfigJSON struct {
+ Auths map[string]authn.AuthConfig
+}
+
+// NewFromPullSecrets returns a new authn.Keychain suitable for resolving image references as
+// scoped by the pull secrets.
+func NewFromPullSecrets(ctx context.Context, secrets []corev1.Secret) (authn.Keychain, error) {
+ keyring := &keyring{
+ index: make([]string, 0),
+ creds: make(map[string][]authn.AuthConfig),
+ }
+
+ var cfg dockerConfigJSON
+
+ // From: https://github.com/kubernetes/kubernetes/blob/0dcafb1f37ee522be3c045753623138e5b907001/pkg/credentialprovider/keyring.go
+ for _, secret := range secrets {
+ if b, exists := secret.Data[corev1.DockerConfigJsonKey]; secret.Type == corev1.SecretTypeDockerConfigJson && exists && len(b) > 0 {
+ if err := json.Unmarshal(b, &cfg); err != nil {
+ return nil, err
+ }
+ }
+ if b, exists := secret.Data[corev1.DockerConfigKey]; secret.Type == corev1.SecretTypeDockercfg && exists && len(b) > 0 {
+ if err := json.Unmarshal(b, &cfg.Auths); err != nil {
+ return nil, err
+ }
+ }
+
+ for registry, v := range cfg.Auths {
+ value := registry
+ if !strings.HasPrefix(value, "https://") && !strings.HasPrefix(value, "http://") {
+ value = "https://" + value
+ }
+ parsed, err := url.Parse(value)
+ if err != nil {
+ return nil, fmt.Errorf("Entry %q in dockercfg invalid (%w)", value, err)
+ }
+
+ // The docker client allows exact matches:
+ // foo.bar.com/namespace
+ // Or hostname matches:
+ // foo.bar.com
+ // It also considers /v2/ and /v1/ equivalent to the hostname
+ // See ResolveAuthConfig in docker/registry/auth.go.
+ effectivePath := parsed.Path
+ if strings.HasPrefix(effectivePath, "/v2/") || strings.HasPrefix(effectivePath, "/v1/") {
+ effectivePath = effectivePath[3:]
+ }
+ var key string
+ if (len(effectivePath) > 0) && (effectivePath != "/") {
+ key = parsed.Host + effectivePath
+ } else {
+ key = parsed.Host
+ }
+
+ if _, ok := keyring.creds[key]; !ok {
+ keyring.index = append(keyring.index, key)
+ }
+
+ keyring.creds[key] = append(keyring.creds[key], v)
+
+ }
+
+ // We reverse sort in to give more specific (aka longer) keys priority
+ // when matching for creds
+ sort.Sort(sort.Reverse(sort.StringSlice(keyring.index)))
+ }
+ return keyring, nil
+}
+
+type keyring struct {
+ index []string
+ creds map[string][]authn.AuthConfig
+}
+
+func (keyring *keyring) Resolve(target authn.Resource) (authn.Authenticator, error) {
+ image := target.String()
+ auths := []authn.AuthConfig{}
+
+ for _, k := range keyring.index {
+ // both k and image are schemeless URLs because even though schemes are allowed
+ // in the credential configurations, we remove them when constructing the keyring
+ if matched, _ := urlsMatchStr(k, image); matched {
+ auths = append(auths, keyring.creds[k]...)
+ }
+ }
+
+ if len(auths) == 0 {
+ return authn.Anonymous, nil
+ }
+
+ return toAuthenticator(auths)
+}
+
+// urlsMatchStr is wrapper for URLsMatch, operating on strings instead of URLs.
+func urlsMatchStr(glob string, target string) (bool, error) {
+ globURL, err := parseSchemelessURL(glob)
+ if err != nil {
+ return false, err
+ }
+ targetURL, err := parseSchemelessURL(target)
+ if err != nil {
+ return false, err
+ }
+ return urlsMatch(globURL, targetURL)
+}
+
+// parseSchemelessURL parses a schemeless url and returns a url.URL
+// url.Parse require a scheme, but ours don't have schemes. Adding a
+// scheme to make url.Parse happy, then clear out the resulting scheme.
+func parseSchemelessURL(schemelessURL string) (*url.URL, error) {
+ parsed, err := url.Parse("https://" + schemelessURL)
+ if err != nil {
+ return nil, err
+ }
+ // clear out the resulting scheme
+ parsed.Scheme = ""
+ return parsed, nil
+}
+
+// splitURL splits the host name into parts, as well as the port
+func splitURL(url *url.URL) (parts []string, port string) {
+ host, port, err := net.SplitHostPort(url.Host)
+ if err != nil {
+ // could not parse port
+ host, port = url.Host, ""
+ }
+ return strings.Split(host, "."), port
+}
+
+// urlsMatch checks whether the given target url matches the glob url, which may have
+// glob wild cards in the host name.
+//
+// Examples:
+//
+// globURL=*.docker.io, targetURL=blah.docker.io => match
+// globURL=*.docker.io, targetURL=not.right.io => no match
+//
+// Note that we don't support wildcards in ports and paths yet.
+func urlsMatch(globURL *url.URL, targetURL *url.URL) (bool, error) {
+ globURLParts, globPort := splitURL(globURL)
+ targetURLParts, targetPort := splitURL(targetURL)
+ if globPort != targetPort {
+ // port doesn't match
+ return false, nil
+ }
+ if len(globURLParts) != len(targetURLParts) {
+ // host name does not have the same number of parts
+ return false, nil
+ }
+ if !strings.HasPrefix(targetURL.Path, globURL.Path) {
+ // the path of the credential must be a prefix
+ return false, nil
+ }
+ for k, globURLPart := range globURLParts {
+ targetURLPart := targetURLParts[k]
+ matched, err := filepath.Match(globURLPart, targetURLPart)
+ if err != nil {
+ return false, err
+ }
+ if !matched {
+ // glob mismatch for some part
+ return false, nil
+ }
+ }
+ // everything matches
+ return true, nil
+}
+
+func toAuthenticator(configs []authn.AuthConfig) (authn.Authenticator, error) {
+ cfg := configs[0]
+
+ if cfg.Auth != "" {
+ cfg.Auth = ""
+ }
+
+ return authn.FromConfig(cfg), nil
+}
diff --git a/pkg/authn/kubernetes/keychain_test.go b/pkg/authn/kubernetes/keychain_test.go
new file mode 100644
index 0000000..e015771
--- /dev/null
+++ b/pkg/authn/kubernetes/keychain_test.go
@@ -0,0 +1,586 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package kubernetes
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ fakeclient "k8s.io/client-go/kubernetes/fake"
+)
+
+var dockerSecretTypes = []secretType{
+ dockerConfigJSONSecretType,
+ dockerCfgSecretType,
+}
+
+type secretType struct {
+ name corev1.SecretType
+ key string
+ marshal func(t *testing.T, registry string, auth authn.AuthConfig) []byte
+}
+
+func (s *secretType) Create(t *testing.T, namespace, name string, registry string, auth authn.AuthConfig) *corev1.Secret {
+ return &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Namespace: namespace,
+ },
+ Type: s.name,
+ Data: map[string][]byte{
+ s.key: s.marshal(t, registry, auth),
+ },
+ }
+}
+
+var dockerConfigJSONSecretType = secretType{
+ name: corev1.SecretTypeDockerConfigJson,
+ key: corev1.DockerConfigJsonKey,
+ marshal: func(t *testing.T, target string, auth authn.AuthConfig) []byte {
+ return toJSON(t, dockerConfigJSON{
+ Auths: map[string]authn.AuthConfig{target: auth},
+ })
+ },
+}
+
+var dockerCfgSecretType = secretType{
+ name: corev1.SecretTypeDockercfg,
+ key: corev1.DockerConfigKey,
+ marshal: func(t *testing.T, target string, auth authn.AuthConfig) []byte {
+ return toJSON(t, map[string]authn.AuthConfig{target: auth})
+ },
+}
+
+func TestAnonymousFallback(t *testing.T) {
+ client := fakeclient.NewSimpleClientset(&corev1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ Namespace: "default",
+ },
+ })
+
+ kc, err := New(context.Background(), client, Options{})
+ if err != nil {
+ t.Errorf("New() = %v", err)
+ }
+
+ testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous)
+}
+
+func TestAnonymousFallbackNoServiceAccount(t *testing.T) {
+ kc, err := New(context.Background(), nil, Options{
+ ServiceAccountName: NoServiceAccount,
+ })
+ if err != nil {
+ t.Errorf("New() = %v", err)
+ }
+
+ testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous)
+}
+
+func TestSecretNotFound(t *testing.T) {
+ client := fakeclient.NewSimpleClientset()
+
+ kc, err := New(context.Background(), client, Options{
+ ServiceAccountName: NoServiceAccount,
+ ImagePullSecrets: []string{"not-found"},
+ })
+ if err != nil {
+ t.Errorf("New() = %v", err)
+ }
+
+ testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous)
+}
+
+func TestServiceAccountNotFound(t *testing.T) {
+ client := fakeclient.NewSimpleClientset(&corev1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ Namespace: "default",
+ },
+ })
+ kc, err := New(context.Background(), client, Options{
+ ServiceAccountName: "not-found",
+ })
+ if err != nil {
+ t.Errorf("New() = %v", err)
+ }
+
+ testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous)
+}
+
+func TestImagePullSecretAttachedServiceAccount(t *testing.T) {
+ username, password := "foo", "bar"
+ client := fakeclient.NewSimpleClientset(&corev1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "svcacct",
+ Namespace: "ns",
+ },
+ ImagePullSecrets: []corev1.LocalObjectReference{{
+ Name: "secret",
+ }},
+ },
+ dockerCfgSecretType.Create(t, "ns", "secret", "fake.registry.io", authn.AuthConfig{
+ Username: username,
+ Password: password,
+ }),
+ )
+
+ kc, err := New(context.Background(), client, Options{
+ Namespace: "ns",
+ ServiceAccountName: "svcacct",
+ })
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+
+ testResolve(t, kc, registry(t, "fake.registry.io"),
+ &authn.Basic{Username: username, Password: password})
+}
+
+func TestSecretAttachedServiceAccount(t *testing.T) {
+ username, password := "foo", "bar"
+
+ cases := []struct {
+ name string
+ createSecret bool
+ useMountSecrets bool
+ expected authn.Authenticator
+ }{
+ {
+ name: "resolved successfully",
+ createSecret: true,
+ useMountSecrets: true,
+ expected: &authn.Basic{Username: username, Password: password},
+ },
+ {
+ name: "missing secret skipped",
+ createSecret: false,
+ useMountSecrets: true,
+ expected: &authn.Basic{},
+ },
+ {
+ name: "skip option",
+ createSecret: true,
+ useMountSecrets: false,
+ expected: &authn.Basic{},
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+
+ objs := []runtime.Object{
+ &corev1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "svcacct",
+ Namespace: "ns",
+ },
+ Secrets: []corev1.ObjectReference{{
+ Name: "secret",
+ }},
+ },
+ }
+ if c.createSecret {
+ objs = append(objs, dockerCfgSecretType.Create(
+ t, "ns", "secret", "fake.registry.io", authn.AuthConfig{
+ Username: username,
+ Password: password,
+ }))
+ }
+ client := fakeclient.NewSimpleClientset(objs...)
+
+ kc, err := New(context.Background(), client, Options{
+ Namespace: "ns",
+ ServiceAccountName: "svcacct",
+ UseMountSecrets: c.useMountSecrets,
+ })
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+
+ testResolve(t, kc, registry(t, "fake.registry.io"), c.expected)
+ })
+ }
+
+}
+
+// Prioritze picking the first secret
+func TestSecretPriority(t *testing.T) {
+ secrets := []corev1.Secret{
+ *dockerCfgSecretType.Create(t, "ns", "secret", "fake.registry.io", authn.AuthConfig{
+ Username: "user", Password: "pass",
+ }),
+ *dockerCfgSecretType.Create(t, "ns", "secret-2", "fake.registry.io", authn.AuthConfig{
+ Username: "anotherUser", Password: "anotherPass",
+ }),
+ }
+
+ kc, err := NewFromPullSecrets(context.Background(), secrets)
+ if err != nil {
+ t.Fatalf("NewFromPullSecrets() = %v", err)
+ }
+
+ expectedAuth := &authn.Basic{Username: "user", Password: "pass"}
+ testResolve(t, kc, registry(t, "fake.registry.io"), expectedAuth)
+}
+
+func TestResolveTargets(t *testing.T) {
+ // Iterate over target types
+ targetTypes := []authn.Resource{
+ registry(t, "fake.registry.io"),
+ repo(t, "fake.registry.io/repo"),
+ }
+
+ for _, secretType := range dockerSecretTypes {
+ for _, target := range targetTypes {
+ // Drop the .
+ testName := secretType.key[1:] + "_" + target.String()
+
+ t.Run(testName, func(t *testing.T) {
+ auth := authn.AuthConfig{
+ Password: fmt.Sprintf("%x", md5.Sum([]byte(t.Name()))),
+ Username: "user" + fmt.Sprintf("%x", md5.Sum([]byte(t.Name()))),
+ }
+
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{
+ *secretType.Create(t, "ns", "secret", target.String(), auth),
+ })
+
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+ authenticator := &authn.Basic{Username: auth.Username, Password: auth.Password}
+ testResolve(t, kc, target, authenticator)
+ })
+ }
+ }
+}
+
+func TestAuthWithScheme(t *testing.T) {
+ auth := authn.AuthConfig{
+ Password: "password",
+ Username: "username",
+ }
+
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret", "https://fake.registry.io", auth),
+ })
+
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+ authenticator := &authn.Basic{Username: auth.Username, Password: auth.Password}
+ testResolve(t, kc, registry(t, "fake.registry.io"), authenticator)
+ testResolve(t, kc, repo(t, "fake.registry.io/repo"), authenticator)
+}
+
+func TestAuthWithPorts(t *testing.T) {
+ auth := authn.AuthConfig{
+ Password: "password",
+ Username: "username",
+ }
+
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret", "fake.registry.io:5000", auth),
+ })
+
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+ authenticator := &authn.Basic{Username: auth.Username, Password: auth.Password}
+ testResolve(t, kc, registry(t, "fake.registry.io:5000"), authenticator)
+ testResolve(t, kc, repo(t, "fake.registry.io:5000/repo"), authenticator)
+
+ // Non-matching ports should return Anonymous
+ testResolve(t, kc, registry(t, "fake.registry.io:1000"), authn.Anonymous)
+ testResolve(t, kc, repo(t, "fake.registry.io:1000/repo"), authn.Anonymous)
+}
+
+func TestAuthPathMatching(t *testing.T) {
+ rootAuth := authn.AuthConfig{Username: "root", Password: "root"}
+ nestedAuth := authn.AuthConfig{Username: "nested", Password: "nested"}
+ leafAuth := authn.AuthConfig{Username: "leaf", Password: "leaf"}
+ partialAuth := authn.AuthConfig{Username: "partial", Password: "partial"}
+
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret-1", "fake.registry.io", rootAuth),
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret-2", "fake.registry.io/nested", nestedAuth),
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret-3", "fake.registry.io/nested/repo", leafAuth),
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret-4", "fake.registry.io/par", partialAuth),
+ })
+
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+ testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(rootAuth))
+ testResolve(t, kc, repo(t, "fake.registry.io/nested"), authn.FromConfig(nestedAuth))
+ testResolve(t, kc, repo(t, "fake.registry.io/nested/repo"), authn.FromConfig(leafAuth))
+ testResolve(t, kc, repo(t, "fake.registry.io/nested/repo/dirt"), authn.FromConfig(leafAuth))
+ testResolve(t, kc, repo(t, "fake.registry.io/partial"), authn.FromConfig(partialAuth))
+}
+
+func TestAuthHostNameVariations(t *testing.T) {
+ rootAuth := authn.AuthConfig{Username: "root", Password: "root"}
+ subdomainAuth := authn.AuthConfig{Username: "sub", Password: "sub"}
+
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret-1", "fake.registry.io", rootAuth),
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret-2", "1.fake.registry.io", subdomainAuth),
+ })
+
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+
+ testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(rootAuth))
+ testResolve(t, kc, registry(t, "1.fake.registry.io"), authn.FromConfig(subdomainAuth))
+
+ // Unrecognized subdomain uses Anonymous
+ testResolve(t, kc, registry(t, "2.fake.registry.io"), authn.Anonymous)
+}
+
+func TestAuthSpecialPathsIgnored(t *testing.T) {
+ auth := authn.AuthConfig{Username: "root", Password: "root"}
+ auth2 := authn.AuthConfig{Username: "root2", Password: "root2"}
+
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{
+ // Note the paths need a trailing '/'
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret-1", "https://fake.registry.io/v1/", auth),
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret-2", "https://fake2.registry.io/v2/", auth2),
+ })
+
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+
+ testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(auth))
+ testResolve(t, kc, repo(t, "fake.registry.io/repo"), authn.FromConfig(auth))
+ testResolve(t, kc, registry(t, "fake2.registry.io"), authn.FromConfig(auth2))
+ testResolve(t, kc, repo(t, "fake2.registry.io/repo"), authn.FromConfig(auth2))
+}
+
+func TestAuthDockerRegistry(t *testing.T) {
+ auth := authn.AuthConfig{Username: "root", Password: "root"}
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret", "index.docker.io", auth),
+ })
+
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+
+ testResolve(t, kc, repo(t, "ubuntu"), authn.FromConfig(auth))
+ testResolve(t, kc, repo(t, "knative/serving"), authn.FromConfig(auth))
+}
+
+func TestAuthWithGlobs(t *testing.T) {
+ auth := authn.AuthConfig{Username: "root", Password: "root"}
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{
+ *dockerConfigJSONSecretType.Create(t, "ns", "secret", "*.registry.io", auth),
+ })
+
+ if err != nil {
+ t.Fatalf("New() = %v", err)
+ }
+
+ testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(auth))
+ testResolve(t, kc, repo(t, "fake.registry.io/repo"), authn.FromConfig(auth))
+ testResolve(t, kc, registry(t, "blah.registry.io"), authn.FromConfig(auth))
+ testResolve(t, kc, repo(t, "blah.registry.io/repo"), authn.FromConfig(auth))
+}
+
+func testResolve(t *testing.T, kc authn.Keychain, target authn.Resource, expectedAuth authn.Authenticator) {
+ t.Helper()
+
+ auth, err := kc.Resolve(target)
+ if err != nil {
+ t.Errorf("Resolve(%v) = %v", target, err)
+ }
+ got, err := auth.Authorization()
+ if err != nil {
+ t.Errorf("Authorization() = %v", err)
+ }
+ want, err := expectedAuth.Authorization()
+ if err != nil {
+ t.Errorf("Authorization() = %v", err)
+ }
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Error("Resolve() diff (-want, +got)\n", diff)
+ }
+}
+
+func toJSON(t *testing.T, obj any) []byte {
+ t.Helper()
+
+ bites, err := json.Marshal(obj)
+
+ if err != nil {
+ t.Fatal("unable to json marshal", err)
+ }
+ return bites
+}
+
+func registry(t *testing.T, registry string) authn.Resource {
+ t.Helper()
+
+ reg, err := name.NewRegistry(registry, name.WeakValidation)
+ if err != nil {
+ t.Fatal("failed to create registry", err)
+ }
+ return reg
+}
+
+func repo(t *testing.T, repository string) authn.Resource {
+ t.Helper()
+
+ repo, err := name.NewRepository(repository, name.WeakValidation)
+ if err != nil {
+ t.Fatal("failed to create repo", err)
+ }
+ return repo
+}
+
+// TestDockerConfigJSON tests using secrets using the .dockerconfigjson form,
+// like you might get from running:
+// kubectl create secret docker-registry secret -n ns --docker-server="fake.registry.io" --docker-username="foo" --docker-password="bar"
+func TestDockerConfigJSON(t *testing.T) {
+ username, password := "foo", "bar"
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "secret",
+ Namespace: "ns",
+ },
+ Type: corev1.SecretTypeDockerConfigJson,
+ Data: map[string][]byte{
+ corev1.DockerConfigJsonKey: []byte(
+ fmt.Sprintf(`{"auths":{"fake.registry.io":{"username":%q,"password":%q,"auth":%q}}}`,
+ username, password,
+ base64.StdEncoding.EncodeToString([]byte(username+":"+password))),
+ ),
+ },
+ }})
+ if err != nil {
+ t.Fatalf("NewFromPullSecrets() = %v", err)
+ }
+
+ reg, err := name.NewRegistry("fake.registry.io", name.WeakValidation)
+ if err != nil {
+ t.Errorf("NewRegistry() = %v", err)
+ }
+
+ auth, err := kc.Resolve(reg)
+ if err != nil {
+ t.Errorf("Resolve(%v) = %v", reg, err)
+ }
+ got, err := auth.Authorization()
+ if err != nil {
+ t.Errorf("Authorization() = %v", err)
+ }
+ want, err := (&authn.Basic{Username: username, Password: password}).Authorization()
+ if err != nil {
+ t.Errorf("Authorization() = %v", err)
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("Resolve() = %v, want %v", got, want)
+ }
+}
+
+func TestKubernetesAuth(t *testing.T) {
+ // From https://github.com/knative/serving/issues/12761#issuecomment-1097441770
+ // All of these should work with K8s' docker auth parsing.
+ for k, ss := range map[string][]string{
+ "registry.gitlab.com/dprotaso/test/nginx": {
+ "registry.gitlab.com",
+ "http://registry.gitlab.com",
+ "https://registry.gitlab.com",
+ "registry.gitlab.com/dprotaso",
+ "http://registry.gitlab.com/dprotaso",
+ "https://registry.gitlab.com/dprotaso",
+ "registry.gitlab.com/dprotaso/test",
+ "http://registry.gitlab.com/dprotaso/test",
+ "https://registry.gitlab.com/dprotaso/test",
+ "registry.gitlab.com/dprotaso/test/nginx",
+ "http://registry.gitlab.com/dprotaso/test/nginx",
+ "https://registry.gitlab.com/dprotaso/test/nginx",
+ },
+ "dtestcontainer.azurecr.io/dave/nginx": {
+ "dtestcontainer.azurecr.io",
+ "http://dtestcontainer.azurecr.io",
+ "https://dtestcontainer.azurecr.io",
+ "dtestcontainer.azurecr.io/dave",
+ "http://dtestcontainer.azurecr.io/dave",
+ "https://dtestcontainer.azurecr.io/dave",
+ "dtestcontainer.azurecr.io/dave/nginx",
+ "http://dtestcontainer.azurecr.io/dave/nginx",
+ "https://dtestcontainer.azurecr.io/dave/nginx",
+ }} {
+ repo, err := name.NewRepository(k)
+ if err != nil {
+ t.Errorf("parsing %q: %v", k, err)
+ continue
+ }
+
+ for _, s := range ss {
+ t.Run(fmt.Sprintf("%s - %s", k, s), func(t *testing.T) {
+ username, password := "foo", "bar"
+ kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "secret",
+ Namespace: "ns",
+ },
+ Type: corev1.SecretTypeDockerConfigJson,
+ Data: map[string][]byte{
+ corev1.DockerConfigJsonKey: []byte(
+ fmt.Sprintf(`{"auths":{%q:{"username":%q,"password":%q,"auth":%q}}}`,
+ s,
+ username, password,
+ base64.StdEncoding.EncodeToString([]byte(username+":"+password))),
+ ),
+ },
+ }})
+ if err != nil {
+ t.Fatalf("NewFromPullSecrets() = %v", err)
+ }
+ auth, err := kc.Resolve(repo)
+ if err != nil {
+ t.Errorf("Resolve(%v) = %v", repo, err)
+ }
+ got, err := auth.Authorization()
+ if err != nil {
+ t.Errorf("Authorization() = %v", err)
+ }
+ want, err := (&authn.Basic{Username: username, Password: password}).Authorization()
+ if err != nil {
+ t.Errorf("Authorization() = %v", err)
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("Resolve() = %v, want %v", got, want)
+ }
+ })
+ }
+ }
+}
diff --git a/pkg/authn/multikeychain.go b/pkg/authn/multikeychain.go
new file mode 100644
index 0000000..3b1804f
--- /dev/null
+++ b/pkg/authn/multikeychain.go
@@ -0,0 +1,41 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+type multiKeychain struct {
+ keychains []Keychain
+}
+
+// Assert that our multi-keychain implements Keychain.
+var _ (Keychain) = (*multiKeychain)(nil)
+
+// NewMultiKeychain composes a list of keychains into one new keychain.
+func NewMultiKeychain(kcs ...Keychain) Keychain {
+ return &multiKeychain{keychains: kcs}
+}
+
+// Resolve implements Keychain.
+func (mk *multiKeychain) Resolve(target Resource) (Authenticator, error) {
+ for _, kc := range mk.keychains {
+ auth, err := kc.Resolve(target)
+ if err != nil {
+ return nil, err
+ }
+ if auth != Anonymous {
+ return auth, nil
+ }
+ }
+ return Anonymous, nil
+}
diff --git a/pkg/authn/multikeychain_test.go b/pkg/authn/multikeychain_test.go
new file mode 100644
index 0000000..3dff0c9
--- /dev/null
+++ b/pkg/authn/multikeychain_test.go
@@ -0,0 +1,98 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authn
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+func TestMultiKeychain(t *testing.T) {
+ one := &Basic{Username: "one", Password: "secret"}
+ two := &Basic{Username: "two", Password: "secret"}
+ three := &Basic{Username: "three", Password: "secret"}
+
+ regOne, _ := name.NewRegistry("one.gcr.io", name.StrictValidation)
+ regTwo, _ := name.NewRegistry("two.gcr.io", name.StrictValidation)
+ regThree, _ := name.NewRegistry("three.gcr.io", name.StrictValidation)
+
+ tests := []struct {
+ name string
+ reg name.Registry
+ kc Keychain
+ want Authenticator
+ }{{
+ // Make sure our test keychain WAI
+ name: "simple fixed test (match)",
+ reg: regOne,
+ kc: fixedKeychain{regOne: one},
+ want: one,
+ }, {
+ // Make sure our test keychain WAI
+ name: "simple fixed test (no match)",
+ reg: regTwo,
+ kc: fixedKeychain{regOne: one},
+ want: Anonymous,
+ }, {
+ name: "match first keychain",
+ reg: regOne,
+ kc: NewMultiKeychain(
+ fixedKeychain{regOne: one},
+ fixedKeychain{regOne: three, regTwo: two},
+ ),
+ want: one,
+ }, {
+ name: "match second keychain",
+ reg: regTwo,
+ kc: NewMultiKeychain(
+ fixedKeychain{regOne: one},
+ fixedKeychain{regOne: three, regTwo: two},
+ ),
+ want: two,
+ }, {
+ name: "match no keychain",
+ reg: regThree,
+ kc: NewMultiKeychain(
+ fixedKeychain{regOne: one},
+ fixedKeychain{regOne: three, regTwo: two},
+ ),
+ want: Anonymous,
+ }}
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got, err := test.kc.Resolve(test.reg)
+ if err != nil {
+ t.Errorf("Resolve() = %v", err)
+ }
+ if got != test.want {
+ t.Errorf("Resolve() = %v, wanted %v", got, test.want)
+ }
+ })
+ }
+}
+
+type fixedKeychain map[Resource]Authenticator
+
+var _ Keychain = (fixedKeychain)(nil)
+
+// Resolve implements Keychain.
+func (fk fixedKeychain) Resolve(target Resource) (Authenticator, error) {
+ if auth, ok := fk[target]; ok {
+ return auth, nil
+ }
+ return Anonymous, nil
+}
diff --git a/pkg/compression/compression.go b/pkg/compression/compression.go
new file mode 100644
index 0000000..6686c2d
--- /dev/null
+++ b/pkg/compression/compression.go
@@ -0,0 +1,26 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package compression abstracts over gzip and zstd.
+package compression
+
+// Compression is an enumeration of the supported compression algorithms
+type Compression string
+
+// The collection of known MediaType values.
+const (
+ None Compression = "none"
+ GZip Compression = "gzip"
+ ZStd Compression = "zstd"
+)
diff --git a/pkg/crane/append.go b/pkg/crane/append.go
new file mode 100644
index 0000000..f1c2ef6
--- /dev/null
+++ b/pkg/crane/append.go
@@ -0,0 +1,114 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/google/go-containerregistry/internal/windows"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func isWindows(img v1.Image) (bool, error) {
+ cfg, err := img.ConfigFile()
+ if err != nil {
+ return false, err
+ }
+ return cfg != nil && cfg.OS == "windows", nil
+}
+
+// Append reads a layer from path and appends it the the v1.Image base.
+//
+// If the base image is a Windows base image (i.e., its config.OS is
+// "windows"), the contents of the tarballs will be modified to be suitable for
+// a Windows container image.`,
+func Append(base v1.Image, paths ...string) (v1.Image, error) {
+ if base == nil {
+ return nil, fmt.Errorf("invalid argument: base")
+ }
+
+ win, err := isWindows(base)
+ if err != nil {
+ return nil, fmt.Errorf("getting base image: %w", err)
+ }
+
+ baseMediaType, err := base.MediaType()
+
+ if err != nil {
+ return nil, fmt.Errorf("getting base image media type: %w", err)
+ }
+
+ layerType := types.DockerLayer
+
+ if baseMediaType == types.OCIManifestSchema1 {
+ layerType = types.OCILayer
+ }
+
+ layers := make([]v1.Layer, 0, len(paths))
+ for _, path := range paths {
+ layer, err := getLayer(path, layerType)
+ if err != nil {
+ return nil, fmt.Errorf("reading layer %q: %w", path, err)
+ }
+
+ if win {
+ layer, err = windows.Windows(layer)
+ if err != nil {
+ return nil, fmt.Errorf("converting %q for Windows: %w", path, err)
+ }
+ }
+
+ layers = append(layers, layer)
+ }
+
+ return mutate.AppendLayers(base, layers...)
+}
+
+func getLayer(path string, layerType types.MediaType) (v1.Layer, error) {
+ f, err := streamFile(path)
+ if err != nil {
+ return nil, err
+ }
+ if f != nil {
+ return stream.NewLayer(f, stream.WithMediaType(layerType)), nil
+ }
+
+ return tarball.LayerFromFile(path, tarball.WithMediaType(layerType))
+}
+
+// If we're dealing with a named pipe, trying to open it multiple times will
+// fail, so we need to do a streaming upload.
+//
+// returns nil, nil for non-streaming files
+func streamFile(path string) (*os.File, error) {
+ if path == "-" {
+ return os.Stdin, nil
+ }
+ fi, err := os.Stat(path)
+ if err != nil {
+ return nil, err
+ }
+
+ if !fi.Mode().IsRegular() {
+ return os.Open(path)
+ }
+
+ return nil, nil
+}
diff --git a/pkg/crane/append_test.go b/pkg/crane/append_test.go
new file mode 100644
index 0000000..d894a9d
--- /dev/null
+++ b/pkg/crane/append_test.go
@@ -0,0 +1,73 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane_test
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestAppendWithOCIBaseImage(t *testing.T) {
+ base := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
+ img, err := crane.Append(base, "testdata/content.tar")
+
+ if err != nil {
+ t.Fatalf("crane.Append(): %v", err)
+ }
+
+ layers, err := img.Layers()
+
+ if err != nil {
+ t.Fatalf("img.Layers(): %v", err)
+ }
+
+ mediaType, err := layers[0].MediaType()
+
+ if err != nil {
+ t.Fatalf("layers[0].MediaType(): %v", err)
+ }
+
+ if got, want := mediaType, types.OCILayer; got != want {
+ t.Errorf("MediaType(): want %q, got %q", want, got)
+ }
+}
+
+func TestAppendWithDockerBaseImage(t *testing.T) {
+ img, err := crane.Append(empty.Image, "testdata/content.tar")
+
+ if err != nil {
+ t.Fatalf("crane.Append(): %v", err)
+ }
+
+ layers, err := img.Layers()
+
+ if err != nil {
+ t.Fatalf("img.Layers(): %v", err)
+ }
+
+ mediaType, err := layers[0].MediaType()
+
+ if err != nil {
+ t.Fatalf("layers[0].MediaType(): %v", err)
+ }
+
+ if got, want := mediaType, types.DockerLayer; got != want {
+ t.Errorf("MediaType(): want %q, got %q", want, got)
+ }
+}
diff --git a/pkg/crane/catalog.go b/pkg/crane/catalog.go
new file mode 100644
index 0000000..f30800c
--- /dev/null
+++ b/pkg/crane/catalog.go
@@ -0,0 +1,35 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "context"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// Catalog returns the repositories in a registry's catalog.
+func Catalog(src string, opt ...Option) (res []string, err error) {
+ o := makeOptions(opt...)
+ reg, err := name.NewRegistry(src, o.Name...)
+ if err != nil {
+ return nil, err
+ }
+
+ // This context gets overridden by remote.WithContext, which is set by
+ // crane.WithContext.
+ return remote.Catalog(context.Background(), reg, o.Remote...)
+}
diff --git a/pkg/crane/config.go b/pkg/crane/config.go
new file mode 100644
index 0000000..3e55cc9
--- /dev/null
+++ b/pkg/crane/config.go
@@ -0,0 +1,24 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+// Config returns the config file for the remote image ref.
+func Config(ref string, opt ...Option) ([]byte, error) {
+ i, _, err := getImage(ref, opt...)
+ if err != nil {
+ return nil, err
+ }
+ return i.RawConfigFile()
+}
diff --git a/pkg/crane/copy.go b/pkg/crane/copy.go
new file mode 100644
index 0000000..a606f96
--- /dev/null
+++ b/pkg/crane/copy.go
@@ -0,0 +1,88 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/internal/legacy"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Copy copies a remote image or index from src to dst.
+func Copy(src, dst string, opt ...Option) error {
+ o := makeOptions(opt...)
+ srcRef, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing reference %q: %w", src, err)
+ }
+
+ dstRef, err := name.ParseReference(dst, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing reference for %q: %w", dst, err)
+ }
+
+ logs.Progress.Printf("Copying from %v to %v", srcRef, dstRef)
+ desc, err := remote.Get(srcRef, o.Remote...)
+ if err != nil {
+ return fmt.Errorf("fetching %q: %w", src, err)
+ }
+
+ switch desc.MediaType {
+ case types.OCIImageIndex, types.DockerManifestList:
+ // Handle indexes separately.
+ if o.Platform != nil {
+ // If platform is explicitly set, don't copy the whole index, just the appropriate image.
+ if err := copyImage(desc, dstRef, o); err != nil {
+ return fmt.Errorf("failed to copy image: %w", err)
+ }
+ } else {
+ if err := copyIndex(desc, dstRef, o); err != nil {
+ return fmt.Errorf("failed to copy index: %w", err)
+ }
+ }
+ case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
+ // Handle schema 1 images separately.
+ if err := legacy.CopySchema1(desc, srcRef, dstRef, o.Remote...); err != nil {
+ return fmt.Errorf("failed to copy schema 1 image: %w", err)
+ }
+ default:
+ // Assume anything else is an image, since some registries don't set mediaTypes properly.
+ if err := copyImage(desc, dstRef, o); err != nil {
+ return fmt.Errorf("failed to copy image: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func copyImage(desc *remote.Descriptor, dstRef name.Reference, o Options) error {
+ img, err := desc.Image()
+ if err != nil {
+ return err
+ }
+ return remote.Write(dstRef, img, o.Remote...)
+}
+
+func copyIndex(desc *remote.Descriptor, dstRef name.Reference, o Options) error {
+ idx, err := desc.ImageIndex()
+ if err != nil {
+ return err
+ }
+ return remote.WriteIndex(dstRef, idx, o.Remote...)
+}
diff --git a/pkg/crane/crane_test.go b/pkg/crane/crane_test.go
new file mode 100644
index 0000000..9f4d124
--- /dev/null
+++ b/pkg/crane/crane_test.go
@@ -0,0 +1,574 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane_test
+
+import (
+ "archive/tar"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/compare"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// TODO(jonjohnsonjr): Test crane.Copy failures.
+func TestCraneRegistry(t *testing.T) {
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ src := fmt.Sprintf("%s/test/crane", u.Host)
+ dst := fmt.Sprintf("%s/test/crane/copy", u.Host)
+
+ // Expected values.
+ img, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest, err := img.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ rawManifest, err := img.RawManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ manifest, err := img.Manifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ config, err := img.RawConfigFile()
+ if err != nil {
+ t.Fatal(err)
+ }
+ layer, err := img.LayerByDigest(manifest.Layers[0].Digest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Load up the registry.
+ if err := crane.Push(img, src); err != nil {
+ t.Fatal(err)
+ }
+
+ // Test that `crane.Foo` returns expected values.
+ d, err := crane.Digest(src)
+ if err != nil {
+ t.Error(err)
+ } else if d != digest.String() {
+ t.Errorf("Digest(): %v != %v", d, digest)
+ }
+
+ m, err := crane.Manifest(src)
+ if err != nil {
+ t.Error(err)
+ } else if string(m) != string(rawManifest) {
+ t.Errorf("Manifest(): %v != %v", m, rawManifest)
+ }
+
+ c, err := crane.Config(src)
+ if err != nil {
+ t.Error(err)
+ } else if string(c) != string(config) {
+ t.Errorf("Config(): %v != %v", c, config)
+ }
+
+ // Make sure we pull what we pushed.
+ pulled, err := crane.Pull(src)
+ if err != nil {
+ t.Error(err)
+ }
+ if err := compare.Images(img, pulled); err != nil {
+ t.Fatal(err)
+ }
+
+ // Test that the copied image is the same as the source.
+ if err := crane.Copy(src, dst); err != nil {
+ t.Fatal(err)
+ }
+
+ // Make sure what we copied is equivalent.
+ // Also, get options coverage in a dumb way.
+ copied, err := crane.Pull(dst, crane.Insecure, crane.WithTransport(http.DefaultTransport), crane.WithAuth(authn.Anonymous), crane.WithAuthFromKeychain(authn.DefaultKeychain), crane.WithUserAgent("crane/tests"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := compare.Images(pulled, copied); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := crane.Tag(dst, "crane-tag"); err != nil {
+ t.Fatal(err)
+ }
+
+ // Make sure what we tagged is equivalent.
+ tagged, err := crane.Pull(fmt.Sprintf("%s:%s", dst, "crane-tag"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := compare.Images(pulled, tagged); err != nil {
+ t.Fatal(err)
+ }
+
+ layerRef := fmt.Sprintf("%s/test/crane@%s", u.Host, manifest.Layers[0].Digest)
+ pulledLayer, err := crane.PullLayer(layerRef)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := compare.Layers(pulledLayer, layer); err != nil {
+ t.Fatal(err)
+ }
+
+ // List Tags
+ // dst variable have: latest and crane-tag
+ tags, err := crane.ListTags(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(tags) != 2 {
+ t.Fatalf("wanted 2 tags, got %d", len(tags))
+ }
+
+ // create 4 tags for dst
+ for i := 1; i < 5; i++ {
+ if err := crane.Tag(dst, fmt.Sprintf("honk-tag-%d", i)); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ tags, err = crane.ListTags(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(tags) != 6 {
+ t.Fatalf("wanted 6 tags, got %d", len(tags))
+ }
+
+ // Delete the non existing image
+ if err := crane.Delete(dst + ":honk-image"); err == nil {
+ t.Fatal("wanted err, got nil")
+ }
+
+ // Delete the image
+ if err := crane.Delete(src); err != nil {
+ t.Fatal(err)
+ }
+
+ // check if the image was really deleted
+ if _, err := crane.Pull(src); err == nil {
+ t.Fatal("wanted err, got nil")
+ }
+
+ // check if the copied image still exist
+ dstPulled, err := crane.Pull(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := compare.Images(dstPulled, copied); err != nil {
+ t.Fatal(err)
+ }
+
+ // List Catalog
+ repos, err := crane.Catalog(u.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(repos) != 2 {
+ t.Fatalf("wanted 2 repos, got %d", len(repos))
+ }
+
+ // Test pushing layer
+ layer, err = img.LayerByDigest(manifest.Layers[1].Digest)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := crane.Upload(layer, dst); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCraneCopyIndex(t *testing.T) {
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ src := fmt.Sprintf("%s/test/crane", u.Host)
+ dst := fmt.Sprintf("%s/test/crane/copy", u.Host)
+
+ // Load up the registry.
+ idx, err := random.Index(1024, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ref, err := name.ParseReference(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := remote.WriteIndex(ref, idx); err != nil {
+ t.Fatal(err)
+ }
+
+ // Test that the copied index is the same as the source.
+ if err := crane.Copy(src, dst); err != nil {
+ t.Fatal(err)
+ }
+
+ d, err := crane.Digest(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cp, err := crane.Digest(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if d != cp {
+ t.Errorf("Copied Digest(): %v != %v", d, cp)
+ }
+}
+
+func TestWithPlatform(t *testing.T) {
+ // Set up a fake registry with a platform-specific image.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ imgs := []mutate.IndexAddendum{}
+ for _, plat := range []string{
+ "linux/amd64",
+ "linux/arm",
+ } {
+ img, err := crane.Image(map[string][]byte{
+ "platform.txt": []byte(plat),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ parts := strings.Split(plat, "/")
+ imgs = append(imgs, mutate.IndexAddendum{
+ Add: img,
+ Descriptor: v1.Descriptor{
+ Platform: &v1.Platform{
+ OS: parts[0],
+ Architecture: parts[1],
+ },
+ },
+ })
+ }
+
+ idx := mutate.AppendManifests(empty.Index, imgs...)
+
+ src := path.Join(u.Host, "src")
+ dst := path.Join(u.Host, "dst")
+
+ ref, err := name.ParseReference(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Populate registry so we can copy from it.
+ if err := remote.WriteIndex(ref, idx); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := crane.Copy(src, dst, crane.WithPlatform(imgs[1].Platform)); err != nil {
+ t.Fatal(err)
+ }
+
+ want, err := crane.Manifest(src, crane.WithPlatform(imgs[1].Platform))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := crane.Manifest(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(got) != string(want) {
+ t.Errorf("Manifest(%q) != Manifest(%q): (\n\n%s\n\n!=\n\n%s\n\n)", dst, src, string(got), string(want))
+ }
+
+ arch := "real fake doors"
+
+ // Now do a fake platform, should fail
+ if _, err := crane.Manifest(src, crane.WithPlatform(&v1.Platform{
+ OS: "does-not-exist",
+ Architecture: arch,
+ })); err == nil {
+ t.Error("crane.Manifest(fake platform): got nil want err")
+ } else if !strings.Contains(err.Error(), arch) {
+ t.Errorf("crane.Manifest(fake platform): expected %q in error, got: %v", arch, err)
+ }
+}
+
+func TestCraneTarball(t *testing.T) {
+ t.Parallel()
+ // Write an image as a tarball.
+ tmp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmp.Name())
+
+ img, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest, err := img.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ src := fmt.Sprintf("test/crane@%s", digest)
+
+ if err := crane.Save(img, src, tmp.Name()); err != nil {
+ t.Errorf("Save: %v", err)
+ }
+
+ // Make sure the image we load has a matching digest.
+ img, err = crane.Load(tmp.Name())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ d, err := img.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if d != digest {
+ t.Errorf("digest mismatch: %v != %v", d, digest)
+ }
+}
+
+func TestCraneSaveLegacy(t *testing.T) {
+ t.Parallel()
+ // Write an image as a legacy tarball.
+ tmp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmp.Name())
+
+ img, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := crane.SaveLegacy(img, "test/crane", tmp.Name()); err != nil {
+ t.Errorf("SaveOCI: %v", err)
+ }
+}
+
+func TestCraneSaveOCI(t *testing.T) {
+ t.Parallel()
+ // Write an image as an OCI image layout.
+ tmp := t.TempDir()
+
+ img, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := crane.SaveOCI(img, tmp); err != nil {
+ t.Errorf("SaveLegacy: %v", err)
+ }
+}
+
+func TestCraneFilesystem(t *testing.T) {
+ t.Parallel()
+ tmp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ img, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ name := "/some/file"
+ content := []byte("sentinel")
+
+ tw := tar.NewWriter(tmp)
+ if err := tw.WriteHeader(&tar.Header{
+ Size: int64(len(content)),
+ Name: name,
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := tw.Write(content); err != nil {
+ t.Fatal(err)
+ }
+ tw.Flush()
+ tw.Close()
+
+ img, err = crane.Append(img, tmp.Name())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var buf bytes.Buffer
+ if err := crane.Export(img, &buf); err != nil {
+ t.Fatal(err)
+ }
+
+ tr := tar.NewReader(&buf)
+ for {
+ header, err := tr.Next()
+ if errors.Is(err, io.EOF) {
+ t.Fatalf("didn't find find")
+ } else if err != nil {
+ t.Fatal(err)
+ }
+ if header.Name == name {
+ b, err := io.ReadAll(tr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(b) != string(content) {
+ t.Fatalf("got back wrong content: %v != %v", string(b), string(content))
+ }
+ break
+ }
+ }
+}
+
+func TestStreamingAppend(t *testing.T) {
+ // Stdin will be an uncompressed layer.
+ layer, err := crane.Layer(map[string][]byte{
+ "hello": []byte(`world`),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ rc, err := layer.Uncompressed()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tmp, err := os.CreateTemp("", "crane-append")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmp.Name())
+
+ if _, err := io.Copy(tmp, rc); err != nil {
+ t.Fatal(err)
+ }
+
+ stdin := os.Stdin
+ defer func() {
+ os.Stdin = stdin
+ }()
+
+ os.Stdin = tmp
+
+ img, err := crane.Append(empty.Image, "-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ ll, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want, got := 1, len(ll); want != got {
+ t.Errorf("crane.Append(stdin) - len(layers): want %d != got %d", want, got)
+ }
+}
+
+func TestBadInputs(t *testing.T) {
+ t.Parallel()
+ invalid := "/dev/null/@@@@@@"
+
+ // Create a valid image reference that will fail with not found.
+ s := httptest.NewServer(http.NotFoundHandler())
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ valid404 := fmt.Sprintf("%s/some/image", u.Host)
+
+ // e drops the first parameter so we can use the result of a function
+ // that returns two values as an expression above. This is a bit of a go quirk.
+ e := func(_ any, err error) error {
+ return err
+ }
+
+ for _, tc := range []struct {
+ desc string
+ err error
+ }{
+ {"Push(_, invalid)", crane.Push(nil, invalid)},
+ {"Upload(_, invalid)", crane.Upload(nil, invalid)},
+ {"Delete(invalid)", crane.Delete(invalid)},
+ {"Delete: 404", crane.Delete(valid404)},
+ {"Save(_, invalid)", crane.Save(nil, invalid, "")},
+ {"SaveLegacy(_, invalid)", crane.SaveLegacy(nil, invalid, "")},
+ {"SaveLegacy(_, invalid)", crane.SaveLegacy(nil, valid404, invalid)},
+ {"SaveOCI(_, invalid)", crane.SaveOCI(nil, "")},
+ {"Copy(invalid, invalid)", crane.Copy(invalid, invalid)},
+ {"Copy(404, invalid)", crane.Copy(valid404, invalid)},
+ {"Copy(404, 404)", crane.Copy(valid404, valid404)},
+ {"Tag(invalid, invalid)", crane.Tag(invalid, invalid)},
+ {"Tag(404, invalid)", crane.Tag(valid404, invalid)},
+ {"Tag(404, 404)", crane.Tag(valid404, valid404)},
+ {"Optimize(invalid, invalid)", crane.Optimize(invalid, invalid, []string{})},
+ {"Optimize(404, invalid)", crane.Optimize(valid404, invalid, []string{})},
+ {"Optimize(404, 404)", crane.Optimize(valid404, valid404, []string{})},
+ // These return multiple values, which are hard to use as expressions.
+ {"Pull(invalid)", e(crane.Pull(invalid))},
+ {"Digest(invalid)", e(crane.Digest(invalid))},
+ {"Manifest(invalid)", e(crane.Manifest(invalid))},
+ {"Config(invalid)", e(crane.Config(invalid))},
+ {"Config(404)", e(crane.Config(valid404))},
+ {"ListTags(invalid)", e(crane.ListTags(invalid))},
+ {"ListTags(404)", e(crane.ListTags(valid404))},
+ {"Append(_, invalid)", e(crane.Append(nil, invalid))},
+ {"Catalog(invalid)", e(crane.Catalog(invalid))},
+ {"Catalog(404)", e(crane.Catalog(u.Host))},
+ {"PullLayer(invalid)", e(crane.PullLayer(invalid))},
+ {"LoadTag(_, invalid)", e(crane.LoadTag("", invalid))},
+ {"LoadTag(invalid, 404)", e(crane.LoadTag(invalid, valid404))},
+ } {
+ if tc.err == nil {
+ t.Errorf("%s: expected err, got nil", tc.desc)
+ }
+ }
+}
diff --git a/pkg/crane/delete.go b/pkg/crane/delete.go
new file mode 100644
index 0000000..58a8be1
--- /dev/null
+++ b/pkg/crane/delete.go
@@ -0,0 +1,33 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// Delete deletes the remote reference at src.
+func Delete(src string, opt ...Option) error {
+ o := makeOptions(opt...)
+ ref, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing reference %q: %w", src, err)
+ }
+
+ return remote.Delete(ref, o.Remote...)
+}
diff --git a/pkg/crane/digest.go b/pkg/crane/digest.go
new file mode 100644
index 0000000..868a570
--- /dev/null
+++ b/pkg/crane/digest.go
@@ -0,0 +1,52 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import "github.com/google/go-containerregistry/pkg/logs"
+
+// Digest returns the sha256 hash of the remote image at ref.
+func Digest(ref string, opt ...Option) (string, error) {
+ o := makeOptions(opt...)
+ if o.Platform != nil {
+ desc, err := getManifest(ref, opt...)
+ if err != nil {
+ return "", err
+ }
+ if !desc.MediaType.IsIndex() {
+ return desc.Digest.String(), nil
+ }
+
+ // TODO: does not work for indexes which contain schema v1 manifests
+ img, err := desc.Image()
+ if err != nil {
+ return "", err
+ }
+ digest, err := img.Digest()
+ if err != nil {
+ return "", err
+ }
+ return digest.String(), nil
+ }
+ desc, err := Head(ref, opt...)
+ if err != nil {
+ logs.Warn.Printf("HEAD request failed, falling back on GET: %v", err)
+ rdesc, err := getManifest(ref, opt...)
+ if err != nil {
+ return "", err
+ }
+ return rdesc.Digest.String(), nil
+ }
+ return desc.Digest.String(), nil
+}
diff --git a/pkg/crane/digest_test.go b/pkg/crane/digest_test.go
new file mode 100644
index 0000000..ac215d3
--- /dev/null
+++ b/pkg/crane/digest_test.go
@@ -0,0 +1,61 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestDigest_MissingDigest(t *testing.T) {
+ response := []byte("doesn't matter")
+ digest := "sha256:477c34d98f9e090a4441cf82d2f1f03e64c8eb730e8c1ef39a8595e685d4df65" // Digest of "doesn't matter"
+ getCalled := false
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/v2/" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ w.Header().Set("Content-Type", string(types.DockerManifestSchema2))
+ if r.Method == http.MethodGet {
+ getCalled = true
+ w.Header().Set("Docker-Content-Digest", digest)
+ }
+ // This will automatically set the Content-Length header.
+ w.Write(response)
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ got, err := Digest(fmt.Sprintf("%s/repo:latest", u.Host))
+ if err != nil {
+ t.Fatalf("Digest: %v", err)
+ }
+ if got != digest {
+ t.Errorf("Digest: got %q, want %q", got, digest)
+ }
+ if !getCalled {
+ t.Errorf("Digest: expected GET to be called")
+ }
+}
diff --git a/pkg/crane/doc.go b/pkg/crane/doc.go
new file mode 100644
index 0000000..7602d79
--- /dev/null
+++ b/pkg/crane/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package crane holds libraries used to implement the crane CLI.
+package crane
diff --git a/pkg/crane/example_test.go b/pkg/crane/example_test.go
new file mode 100644
index 0000000..3a6c182
--- /dev/null
+++ b/pkg/crane/example_test.go
@@ -0,0 +1,31 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane_test
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+)
+
+func Example() {
+ c := map[string][]byte{
+ "/binary": []byte("binary contents"),
+ }
+ i, _ := crane.Image(c)
+ d, _ := i.Digest()
+ fmt.Println(d)
+ // Output: sha256:09fb0c6289cefaad8c74c7e5fd6758ad6906ab8f57f1350d9f4eb5a7df45ff8b
+}
diff --git a/pkg/crane/export.go b/pkg/crane/export.go
new file mode 100644
index 0000000..5d6da1d
--- /dev/null
+++ b/pkg/crane/export.go
@@ -0,0 +1,47 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "io"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+)
+
+// Export writes the filesystem contents (as a tarball) of img to w.
+// If img has a single layer, just write the (uncompressed) contents to w so
+// that this "just works" for images that just wrap a single blob.
+func Export(img v1.Image, w io.Writer) error {
+ layers, err := img.Layers()
+ if err != nil {
+ return err
+ }
+ if len(layers) == 1 {
+ // If it's a single layer, we don't have to flatten the filesystem.
+ // An added perk of skipping mutate.Extract here is that this works
+ // for non-tarball layers.
+ l := layers[0]
+ rc, err := l.Uncompressed()
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, rc)
+ return err
+ }
+ fs := mutate.Extract(img)
+ _, err = io.Copy(w, fs)
+ return err
+}
diff --git a/pkg/crane/export_test.go b/pkg/crane/export_test.go
new file mode 100644
index 0000000..e60e941
--- /dev/null
+++ b/pkg/crane/export_test.go
@@ -0,0 +1,41 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/static"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestExport(t *testing.T) {
+ want := []byte(`{"foo":"bar"}`)
+ layer := static.NewLayer(want, types.MediaType("application/json"))
+ img, err := mutate.AppendLayers(empty.Image, layer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var buf bytes.Buffer
+ if err := Export(img, &buf); err != nil {
+ t.Fatal(err)
+ }
+ if got := buf.Bytes(); !bytes.Equal(got, want) {
+ t.Errorf("got: %s\nwant: %s", got, want)
+ }
+}
diff --git a/pkg/crane/filemap.go b/pkg/crane/filemap.go
new file mode 100644
index 0000000..36dfc2a
--- /dev/null
+++ b/pkg/crane/filemap.go
@@ -0,0 +1,72 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "archive/tar"
+ "bytes"
+ "io"
+ "sort"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+// Layer creates a layer from a single file map. These layers are reproducible and consistent.
+// A filemap is a path -> file content map representing a file system.
+func Layer(filemap map[string][]byte) (v1.Layer, error) {
+ b := &bytes.Buffer{}
+ w := tar.NewWriter(b)
+
+ fn := []string{}
+ for f := range filemap {
+ fn = append(fn, f)
+ }
+ sort.Strings(fn)
+
+ for _, f := range fn {
+ c := filemap[f]
+ if err := w.WriteHeader(&tar.Header{
+ Name: f,
+ Size: int64(len(c)),
+ }); err != nil {
+ return nil, err
+ }
+ if _, err := w.Write(c); err != nil {
+ return nil, err
+ }
+ }
+ if err := w.Close(); err != nil {
+ return nil, err
+ }
+
+ // Return a new copy of the buffer each time it's opened.
+ return tarball.LayerFromOpener(func() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewBuffer(b.Bytes())), nil
+ })
+}
+
+// Image creates a image with the given filemaps as its contents. These images are reproducible and consistent.
+// A filemap is a path -> file content map representing a file system.
+func Image(filemap map[string][]byte) (v1.Image, error) {
+ y, err := Layer(filemap)
+ if err != nil {
+ return nil, err
+ }
+
+ return mutate.AppendLayers(empty.Image, y)
+}
diff --git a/pkg/crane/filemap_test.go b/pkg/crane/filemap_test.go
new file mode 100644
index 0000000..21d8d54
--- /dev/null
+++ b/pkg/crane/filemap_test.go
@@ -0,0 +1,187 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane_test
+
+import (
+ "archive/tar"
+ "errors"
+ "io"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+)
+
+func TestLayer(t *testing.T) {
+ tcs := []struct {
+ Name string
+ FileMap map[string][]byte
+ Digest string
+ }{{
+ Name: "Empty contents",
+ Digest: "sha256:89732bc7504122601f40269fc9ddfb70982e633ea9caf641ae45736f2846b004",
+ }, {
+ Name: "One file",
+ FileMap: map[string][]byte{
+ "/test": []byte("testy"),
+ },
+ Digest: "sha256:ec3ff19f471b99a76fb1c339c1dfdaa944a4fba25be6bcdc99fe7e772103079e",
+ }, {
+ Name: "Two files",
+ FileMap: map[string][]byte{
+ "/test": []byte("testy"),
+ "/testalt": []byte("footesty"),
+ },
+ Digest: "sha256:a48bcb7be3ab3ec608ee56eb80901224e19e31dc096cc06a8fd3a8dae1aa8947",
+ }, {
+ Name: "Many files",
+ FileMap: map[string][]byte{
+ "/1": []byte("1"),
+ "/2": []byte("2"),
+ "/3": []byte("3"),
+ "/4": []byte("4"),
+ "/5": []byte("5"),
+ "/6": []byte("6"),
+ "/7": []byte("7"),
+ "/8": []byte("8"),
+ "/9": []byte("9"),
+ },
+ Digest: "sha256:1e637602abbcab2dcedcc24e0b7c19763454a47261f1658b57569530b369ccb9",
+ }}
+
+ for _, tc := range tcs {
+ t.Run(tc.Name, func(t *testing.T) {
+ l, err := crane.Layer(tc.FileMap)
+ if err != nil {
+ t.Fatalf("Error calling layer: %v", err)
+ }
+
+ d, err := l.Digest()
+ if err != nil {
+ t.Fatalf("Error calling digest: %v", err)
+ }
+ if d.String() != tc.Digest {
+ t.Errorf("Incorrect digest, want %q, got %q", tc.Digest, d.String())
+ }
+
+ // Check contents match.
+ rc, err := l.Uncompressed()
+ if err != nil {
+ t.Fatalf("Uncompressed: %v", err)
+ }
+ defer rc.Close()
+ tr := tar.NewReader(rc)
+ saw := map[string]struct{}{}
+ for {
+ th, err := tr.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ t.Fatalf("Next: %v", err)
+ }
+ saw[th.Name] = struct{}{}
+ want, found := tc.FileMap[th.Name]
+ if !found {
+ t.Errorf("found %q, not in original map", th.Name)
+ continue
+ }
+ got, err := io.ReadAll(tr)
+ if err != nil {
+ t.Fatalf("ReadAll(%q): %v", th.Name, err)
+ }
+ if string(want) != string(got) {
+ t.Errorf("File %q: got %v, want %v", th.Name, string(got), string(want))
+ }
+ }
+ for k := range saw {
+ delete(tc.FileMap, k)
+ }
+ for k := range tc.FileMap {
+ t.Errorf("Layer did not contain %q", k)
+ }
+ })
+ t.Run(tc.Name+" is reproducible", func(t *testing.T) {
+ l1, _ := crane.Layer(tc.FileMap)
+ l2, _ := crane.Layer(tc.FileMap)
+ d1, _ := l1.Digest()
+ d2, _ := l2.Digest()
+ if d1 != d2 {
+ t.Fatalf("Non matching digests, want %q, got %q", d1, d2)
+ }
+ })
+ }
+}
+
+func TestImage(t *testing.T) {
+ tcs := []struct {
+ Name string
+ FileMap map[string][]byte
+ Digest string
+ }{{
+ Name: "Empty contents",
+ Digest: "sha256:98132f58b523c391a5788997327cac95e114e3a6609d01163189774510705399",
+ }, {
+ Name: "One file",
+ FileMap: map[string][]byte{
+ "/test": []byte("testy"),
+ },
+ Digest: "sha256:d905c03ac635172a96c12b8af6c90cfd028e3edaa3114b31a9e196ab38c16963",
+ }, {
+ Name: "Two files",
+ FileMap: map[string][]byte{
+ "/test": []byte("testy"),
+ "/bar": []byte("not useful"),
+ },
+ Digest: "sha256:20e7e4800e5eb167f170970936c08d9e1bcbe91372420eeb6ab8d1a07752c3a3",
+ }, {
+ Name: "Many files",
+ FileMap: map[string][]byte{
+ "/1": []byte("1"),
+ "/2": []byte("2"),
+ "/3": []byte("3"),
+ "/4": []byte("4"),
+ "/5": []byte("5"),
+ "/6": []byte("6"),
+ "/7": []byte("7"),
+ "/8": []byte("8"),
+ "/9": []byte("9"),
+ },
+ Digest: "sha256:dfca2803510c8e3b83a3151f7c035c60cfa2a8a52465b802e18b85014de361f1",
+ }}
+ for _, tc := range tcs {
+ t.Run(tc.Name, func(t *testing.T) {
+ i, err := crane.Image(tc.FileMap)
+ if err != nil {
+ t.Fatalf("Error calling image: %v", err)
+ }
+ d, err := i.Digest()
+ if err != nil {
+ t.Fatalf("Error calling digest: %v", err)
+ }
+ if d.String() != tc.Digest {
+ t.Fatalf("Incorrect digest, want %q, got %q", tc.Digest, d.String())
+ }
+ })
+ t.Run(tc.Name+" is reproducible", func(t *testing.T) {
+ i1, _ := crane.Image(tc.FileMap)
+ i2, _ := crane.Image(tc.FileMap)
+ d1, _ := i1.Digest()
+ d2, _ := i2.Digest()
+ if d1 != d2 {
+ t.Fatalf("Non matching digests, want %q, got %q", d1, d2)
+ }
+ })
+ }
+}
diff --git a/pkg/crane/get.go b/pkg/crane/get.go
new file mode 100644
index 0000000..1f12f01
--- /dev/null
+++ b/pkg/crane/get.go
@@ -0,0 +1,56 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func getImage(r string, opt ...Option) (v1.Image, name.Reference, error) {
+ o := makeOptions(opt...)
+ ref, err := name.ParseReference(r, o.Name...)
+ if err != nil {
+ return nil, nil, fmt.Errorf("parsing reference %q: %w", r, err)
+ }
+ img, err := remote.Image(ref, o.Remote...)
+ if err != nil {
+ return nil, nil, fmt.Errorf("reading image %q: %w", ref, err)
+ }
+ return img, ref, nil
+}
+
+func getManifest(r string, opt ...Option) (*remote.Descriptor, error) {
+ o := makeOptions(opt...)
+ ref, err := name.ParseReference(r, o.Name...)
+ if err != nil {
+ return nil, fmt.Errorf("parsing reference %q: %w", r, err)
+ }
+ return remote.Get(ref, o.Remote...)
+}
+
+// Head performs a HEAD request for a manifest and returns a content descriptor
+// based on the registry's response.
+func Head(r string, opt ...Option) (*v1.Descriptor, error) {
+ o := makeOptions(opt...)
+ ref, err := name.ParseReference(r, o.Name...)
+ if err != nil {
+ return nil, err
+ }
+ return remote.Head(ref, o.Remote...)
+}
diff --git a/pkg/crane/list.go b/pkg/crane/list.go
new file mode 100644
index 0000000..3835215
--- /dev/null
+++ b/pkg/crane/list.go
@@ -0,0 +1,33 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// ListTags returns the tags in repository src.
+func ListTags(src string, opt ...Option) ([]string, error) {
+ o := makeOptions(opt...)
+ repo, err := name.NewRepository(src, o.Name...)
+ if err != nil {
+ return nil, fmt.Errorf("parsing repo %q: %w", src, err)
+ }
+
+ return remote.List(repo, o.Remote...)
+}
diff --git a/pkg/crane/manifest.go b/pkg/crane/manifest.go
new file mode 100644
index 0000000..a54926a
--- /dev/null
+++ b/pkg/crane/manifest.go
@@ -0,0 +1,32 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+// Manifest returns the manifest for the remote image or index ref.
+func Manifest(ref string, opt ...Option) ([]byte, error) {
+ desc, err := getManifest(ref, opt...)
+ if err != nil {
+ return nil, err
+ }
+ o := makeOptions(opt...)
+ if o.Platform != nil {
+ img, err := desc.Image()
+ if err != nil {
+ return nil, err
+ }
+ return img.RawManifest()
+ }
+ return desc.Manifest, nil
+}
diff --git a/pkg/crane/optimize.go b/pkg/crane/optimize.go
new file mode 100644
index 0000000..74c665d
--- /dev/null
+++ b/pkg/crane/optimize.go
@@ -0,0 +1,237 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/containerd/stargz-snapshotter/estargz"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Optimize optimizes a remote image or index from src to dst.
+// THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE WITHOUT WARNING.
+func Optimize(src, dst string, prioritize []string, opt ...Option) error {
+ pset := newStringSet(prioritize)
+ o := makeOptions(opt...)
+ srcRef, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing reference %q: %w", src, err)
+ }
+
+ dstRef, err := name.ParseReference(dst, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing reference for %q: %w", dst, err)
+ }
+
+ logs.Progress.Printf("Optimizing from %v to %v", srcRef, dstRef)
+ desc, err := remote.Get(srcRef, o.Remote...)
+ if err != nil {
+ return fmt.Errorf("fetching %q: %w", src, err)
+ }
+
+ switch desc.MediaType {
+ case types.OCIImageIndex, types.DockerManifestList:
+ // Handle indexes separately.
+ if o.Platform != nil {
+ // If platform is explicitly set, don't optimize the whole index, just the appropriate image.
+ if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil {
+ return fmt.Errorf("failed to optimize image: %w", err)
+ }
+ } else {
+ if err := optimizeAndPushIndex(desc, dstRef, pset, o); err != nil {
+ return fmt.Errorf("failed to optimize index: %w", err)
+ }
+ }
+
+ case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
+ return errors.New("docker schema 1 images are not supported")
+
+ default:
+ // Assume anything else is an image, since some registries don't set mediaTypes properly.
+ if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil {
+ return fmt.Errorf("failed to optimize image: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func optimizeAndPushImage(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error {
+ img, err := desc.Image()
+ if err != nil {
+ return err
+ }
+
+ missing, oimg, err := optimizeImage(img, prioritize)
+ if err != nil {
+ return err
+ }
+
+ if len(missing) > 0 {
+ return fmt.Errorf("the following prioritized files were missing from image: %v", missing.List())
+ }
+
+ return remote.Write(dstRef, oimg, o.Remote...)
+}
+
+func optimizeImage(img v1.Image, prioritize stringSet) (stringSet, v1.Image, error) {
+ cfg, err := img.ConfigFile()
+ if err != nil {
+ return nil, nil, err
+ }
+ ocfg := cfg.DeepCopy()
+ ocfg.History = nil
+ ocfg.RootFS.DiffIDs = nil
+
+ oimg, err := mutate.ConfigFile(empty.Image, ocfg)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ layers, err := img.Layers()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ missingFromImage := newStringSet(prioritize.List())
+ olayers := make([]mutate.Addendum, 0, len(layers))
+ for _, layer := range layers {
+ missingFromLayer := []string{}
+ olayer, err := tarball.LayerFromOpener(layer.Uncompressed,
+ tarball.WithEstargz,
+ tarball.WithEstargzOptions(
+ estargz.WithPrioritizedFiles(prioritize.List()),
+ estargz.WithAllowPrioritizeNotFound(&missingFromLayer),
+ ))
+ if err != nil {
+ return nil, nil, err
+ }
+ missingFromImage = missingFromImage.Intersection(newStringSet(missingFromLayer))
+
+ olayers = append(olayers, mutate.Addendum{
+ Layer: olayer,
+ MediaType: types.DockerLayer,
+ })
+ }
+
+ oimg, err = mutate.Append(oimg, olayers...)
+ if err != nil {
+ return nil, nil, err
+ }
+ return missingFromImage, oimg, nil
+}
+
+func optimizeAndPushIndex(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error {
+ idx, err := desc.ImageIndex()
+ if err != nil {
+ return err
+ }
+
+ missing, oidx, err := optimizeIndex(idx, prioritize)
+ if err != nil {
+ return err
+ }
+
+ if len(missing) > 0 {
+ return fmt.Errorf("the following prioritized files were missing from all images: %v", missing.List())
+ }
+
+ return remote.WriteIndex(dstRef, oidx, o.Remote...)
+}
+
+func optimizeIndex(idx v1.ImageIndex, prioritize stringSet) (stringSet, v1.ImageIndex, error) {
+ im, err := idx.IndexManifest()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ missingFromIndex := newStringSet(prioritize.List())
+
+ // Build an image for each child from the base and append it to a new index to produce the result.
+ adds := make([]mutate.IndexAddendum, 0, len(im.Manifests))
+ for _, desc := range im.Manifests {
+ img, err := idx.Image(desc.Digest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ missingFromImage, oimg, err := optimizeImage(img, prioritize)
+ if err != nil {
+ return nil, nil, err
+ }
+ missingFromIndex = missingFromIndex.Intersection(missingFromImage)
+ adds = append(adds, mutate.IndexAddendum{
+ Add: oimg,
+ Descriptor: v1.Descriptor{
+ URLs: desc.URLs,
+ MediaType: desc.MediaType,
+ Annotations: desc.Annotations,
+ Platform: desc.Platform,
+ },
+ })
+ }
+
+ idxType, err := idx.MediaType()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return missingFromIndex, mutate.IndexMediaType(mutate.AppendManifests(empty.Index, adds...), idxType), nil
+}
+
+type stringSet map[string]struct{}
+
+func newStringSet(in []string) stringSet {
+ ss := stringSet{}
+ for _, s := range in {
+ ss[s] = struct{}{}
+ }
+ return ss
+}
+
+func (s stringSet) List() []string {
+ result := make([]string, 0, len(s))
+ for k := range s {
+ result = append(result, k)
+ }
+ return result
+}
+
+func (s stringSet) Intersection(rhs stringSet) stringSet {
+ // To appease ST1016
+ lhs := s
+
+ // Make sure len(lhs) >= len(rhs)
+ if len(lhs) < len(rhs) {
+ return rhs.Intersection(lhs)
+ }
+
+ result := stringSet{}
+ for k := range lhs {
+ if _, ok := rhs[k]; ok {
+ result[k] = struct{}{}
+ }
+ }
+ return result
+}
diff --git a/pkg/crane/optimize_test.go b/pkg/crane/optimize_test.go
new file mode 100644
index 0000000..11aaf57
--- /dev/null
+++ b/pkg/crane/optimize_test.go
@@ -0,0 +1,179 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "net/http/httptest"
+ "net/url"
+ "path"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func TestStringSet(t *testing.T) {
+ for _, tc := range []struct {
+ lhs []string
+ rhs []string
+ result []string
+ }{{
+ lhs: []string{},
+ rhs: []string{},
+ result: []string{},
+ }, {
+ lhs: []string{"a"},
+ rhs: []string{},
+ result: []string{},
+ }, {
+ lhs: []string{},
+ rhs: []string{"a"},
+ result: []string{},
+ }, {
+ lhs: []string{"a", "b", "c"},
+ rhs: []string{"a", "b", "c"},
+ result: []string{"a", "b", "c"},
+ }, {
+ lhs: []string{"a", "b"},
+ rhs: []string{"a"},
+ result: []string{"a"},
+ }, {
+ lhs: []string{"a"},
+ rhs: []string{"a", "b"},
+ result: []string{"a"},
+ }} {
+ got := newStringSet(tc.lhs).Intersection(newStringSet(tc.rhs))
+ want := newStringSet(tc.result)
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("%v.intersect(%v) (-want +got): %s", tc.lhs, tc.rhs, diff)
+ }
+
+ less := func(a, b string) bool {
+ return strings.Compare(a, b) <= -1
+ }
+ if diff := cmp.Diff(tc.result, got.List(), cmpopts.SortSlices(less)); diff != "" {
+ t.Errorf("%v.List() (-want +got): = %v", tc.result, diff)
+ }
+ }
+}
+
+func TestOptimize(t *testing.T) {
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ imgs := []mutate.IndexAddendum{}
+ for _, plat := range []string{
+ "linux/amd64",
+ "linux/arm",
+ } {
+ img, err := Image(map[string][]byte{
+ "unimportant": []byte(strings.Repeat("deadbeef", 128)),
+ "important": []byte(`abc`),
+ "platform.txt": []byte(plat),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ parts := strings.Split(plat, "/")
+ imgs = append(imgs, mutate.IndexAddendum{
+ Add: img,
+ Descriptor: v1.Descriptor{
+ Platform: &v1.Platform{
+ OS: parts[0],
+ Architecture: parts[1],
+ },
+ },
+ })
+ }
+
+ idx := mutate.AppendManifests(empty.Index, imgs...)
+
+ slow := path.Join(u.Host, "slow")
+ fast := path.Join(u.Host, "fast")
+
+ ref, err := name.ParseReference(slow)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := remote.WriteIndex(ref, idx); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Optimize(slow, fast, []string{"important"}); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Optimize(slow, fast, []string{"important"}, WithPlatform(imgs[1].Platform)); err != nil {
+ t.Fatal(err)
+ }
+
+ // Compare optimize WithPlatform path to optimizing just an image.
+ got, err := Digest(fast)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dig, err := Digest(slow, WithPlatform(imgs[1].Platform))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ slowImgRef := slow + "@" + dig
+ if err := Optimize(slowImgRef, fast, []string{"important"}, WithPlatform(imgs[1].Platform)); err != nil {
+ t.Fatal(err)
+ }
+
+ want, err := Digest(fast)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got != want {
+ t.Errorf("Optimize(WithPlatform) != Optimize(bydigest): %q != %q", got, want)
+ }
+
+ for i, ref := range []string{slow, slow, slowImgRef} {
+ opts := []Option{}
+ // Silly, but use WithPlatform to get some more coverage.
+ if i != 0 {
+ opts = []Option{WithPlatform(imgs[1].Platform)}
+ }
+ dig, err := Digest(ref, opts...)
+ if err != nil {
+ t.Errorf("Digest(%q): %v", ref, err)
+ continue
+ }
+ // Make sure we fail if there's a missing file in the optimize set
+ // Use the image digest because it's ~impossible to exist in img.
+ if err := Optimize(ref, fast, []string{dig}, opts...); err == nil {
+ t.Errorf("Optimize(%q, prioritize=%q): got nil, want err", ref, dig)
+ } else if !strings.Contains(err.Error(), dig) {
+ // Make sure this contains the missing file (dig)
+ t.Errorf("Optimize(%q) error should contain %q, got: %v", ref, dig, err)
+ }
+ }
+}
diff --git a/pkg/crane/options.go b/pkg/crane/options.go
new file mode 100644
index 0000000..5d2e0e4
--- /dev/null
+++ b/pkg/crane/options.go
@@ -0,0 +1,149 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "context"
+ "crypto/tls"
+ "net/http"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// Options hold the options that crane uses when calling other packages.
+type Options struct {
+ Name []name.Option
+ Remote []remote.Option
+ Platform *v1.Platform
+ Keychain authn.Keychain
+
+ transport http.RoundTripper
+ insecure bool
+}
+
+// GetOptions exposes the underlying []remote.Option, []name.Option, and
+// platform, based on the passed Option. Generally, you shouldn't need to use
+// this unless you've painted yourself into a dependency corner as we have
+// with the crane and gcrane cli packages.
+func GetOptions(opts ...Option) Options {
+ return makeOptions(opts...)
+}
+
+func makeOptions(opts ...Option) Options {
+ opt := Options{
+ Remote: []remote.Option{
+ remote.WithAuthFromKeychain(authn.DefaultKeychain),
+ },
+ Keychain: authn.DefaultKeychain,
+ }
+
+ for _, o := range opts {
+ o(&opt)
+ }
+
+ // Allow for untrusted certificates if the user
+ // passed Insecure but no custom transport.
+ if opt.insecure && opt.transport == nil {
+ transport := remote.DefaultTransport.(*http.Transport).Clone()
+ transport.TLSClientConfig = &tls.Config{
+ InsecureSkipVerify: true, //nolint: gosec
+ }
+
+ WithTransport(transport)(&opt)
+ }
+
+ return opt
+}
+
+// Option is a functional option for crane.
+type Option func(*Options)
+
+// WithTransport is a functional option for overriding the default transport
+// for remote operations. Setting a transport will override the Insecure option's
+// configuration allowing for image registries to use untrusted certificates.
+func WithTransport(t http.RoundTripper) Option {
+ return func(o *Options) {
+ o.Remote = append(o.Remote, remote.WithTransport(t))
+ o.transport = t
+ }
+}
+
+// Insecure is an Option that allows image references to be fetched without TLS.
+// This will also allow for untrusted (e.g. self-signed) certificates in cases where
+// the default transport is used (i.e. when WithTransport is not used).
+func Insecure(o *Options) {
+ o.Name = append(o.Name, name.Insecure)
+ o.insecure = true
+}
+
+// WithPlatform is an Option to specify the platform.
+func WithPlatform(platform *v1.Platform) Option {
+ return func(o *Options) {
+ if platform != nil {
+ o.Remote = append(o.Remote, remote.WithPlatform(*platform))
+ }
+ o.Platform = platform
+ }
+}
+
+// WithAuthFromKeychain is a functional option for overriding the default
+// authenticator for remote operations, using an authn.Keychain to find
+// credentials.
+//
+// By default, crane will use authn.DefaultKeychain.
+func WithAuthFromKeychain(keys authn.Keychain) Option {
+ return func(o *Options) {
+ // Replace the default keychain at position 0.
+ o.Remote[0] = remote.WithAuthFromKeychain(keys)
+ o.Keychain = keys
+ }
+}
+
+// WithAuth is a functional option for overriding the default authenticator
+// for remote operations.
+//
+// By default, crane will use authn.DefaultKeychain.
+func WithAuth(auth authn.Authenticator) Option {
+ return func(o *Options) {
+ // Replace the default keychain at position 0.
+ o.Remote[0] = remote.WithAuth(auth)
+ }
+}
+
+// WithUserAgent adds the given string to the User-Agent header for any HTTP
+// requests.
+func WithUserAgent(ua string) Option {
+ return func(o *Options) {
+ o.Remote = append(o.Remote, remote.WithUserAgent(ua))
+ }
+}
+
+// WithNondistributable is an option that allows pushing non-distributable
+// layers.
+func WithNondistributable() Option {
+ return func(o *Options) {
+ o.Remote = append(o.Remote, remote.WithNondistributable)
+ }
+}
+
+// WithContext is a functional option for setting the context.
+func WithContext(ctx context.Context) Option {
+ return func(o *Options) {
+ o.Remote = append(o.Remote, remote.WithContext(ctx))
+ }
+}
diff --git a/pkg/crane/options_test.go b/pkg/crane/options_test.go
new file mode 100644
index 0000000..98d7396
--- /dev/null
+++ b/pkg/crane/options_test.go
@@ -0,0 +1,58 @@
+// Copyright 2023 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "errors"
+ "net/http"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func TestInsecureOptionTracking(t *testing.T) {
+ want := true
+ opts := GetOptions(Insecure)
+
+ if got := opts.insecure; got != want {
+ t.Errorf("got %t\nwant: %t", got, want)
+ }
+}
+
+func TestTransportSetting(t *testing.T) {
+ opts := GetOptions(WithTransport(remote.DefaultTransport))
+
+ if opts.transport == nil {
+ t.Error("expected crane transport to be set when user passes WithTransport")
+ }
+}
+
+func TestInsecureTransport(t *testing.T) {
+ want := true
+ opts := GetOptions(Insecure)
+ var transport *http.Transport
+ var ok bool
+ if transport, ok = opts.transport.(*http.Transport); !ok {
+ t.Fatal("Unable to successfully assert default transport")
+ }
+
+ if transport.TLSClientConfig == nil {
+ t.Fatal(errors.New("TLSClientConfig was nil and should be set"))
+ }
+
+ if got := transport.TLSClientConfig.InsecureSkipVerify; got != want {
+ t.Errorf("got: %t\nwant: %t", got, want)
+ }
+}
diff --git a/pkg/crane/pull.go b/pkg/crane/pull.go
new file mode 100644
index 0000000..7e6e5b7
--- /dev/null
+++ b/pkg/crane/pull.go
@@ -0,0 +1,142 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "fmt"
+ "os"
+
+ legacy "github.com/google/go-containerregistry/pkg/legacy/tarball"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/layout"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+// Tag applied to images that were pulled by digest. This denotes that the
+// image was (probably) never tagged with this, but lets us avoid applying the
+// ":latest" tag which might be misleading.
+const iWasADigestTag = "i-was-a-digest"
+
+// Pull returns a v1.Image of the remote image src.
+func Pull(src string, opt ...Option) (v1.Image, error) {
+ o := makeOptions(opt...)
+ ref, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ return nil, fmt.Errorf("parsing reference %q: %w", src, err)
+ }
+
+ return remote.Image(ref, o.Remote...)
+}
+
+// Save writes the v1.Image img as a tarball at path with tag src.
+func Save(img v1.Image, src, path string) error {
+ imgMap := map[string]v1.Image{src: img}
+ return MultiSave(imgMap, path)
+}
+
+// MultiSave writes collection of v1.Image img with tag as a tarball.
+func MultiSave(imgMap map[string]v1.Image, path string, opt ...Option) error {
+ o := makeOptions(opt...)
+ tagToImage := map[name.Tag]v1.Image{}
+
+ for src, img := range imgMap {
+ ref, err := name.ParseReference(src, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing ref %q: %w", src, err)
+ }
+
+ // WriteToFile wants a tag to write to the tarball, but we might have
+ // been given a digest.
+ // If the original ref was a tag, use that. Otherwise, if it was a
+ // digest, tag the image with :i-was-a-digest instead.
+ tag, ok := ref.(name.Tag)
+ if !ok {
+ d, ok := ref.(name.Digest)
+ if !ok {
+ return fmt.Errorf("ref wasn't a tag or digest")
+ }
+ tag = d.Repository.Tag(iWasADigestTag)
+ }
+ tagToImage[tag] = img
+ }
+ // no progress channel (for now)
+ return tarball.MultiWriteToFile(path, tagToImage)
+}
+
+// PullLayer returns the given layer from a registry.
+func PullLayer(ref string, opt ...Option) (v1.Layer, error) {
+ o := makeOptions(opt...)
+ digest, err := name.NewDigest(ref, o.Name...)
+ if err != nil {
+ return nil, err
+ }
+
+ return remote.Layer(digest, o.Remote...)
+}
+
+// SaveLegacy writes the v1.Image img as a legacy tarball at path with tag src.
+func SaveLegacy(img v1.Image, src, path string) error {
+ imgMap := map[string]v1.Image{src: img}
+ return MultiSave(imgMap, path)
+}
+
+// MultiSaveLegacy writes collection of v1.Image img with tag as a legacy tarball.
+func MultiSaveLegacy(imgMap map[string]v1.Image, path string) error {
+ refToImage := map[name.Reference]v1.Image{}
+
+ for src, img := range imgMap {
+ ref, err := name.ParseReference(src)
+ if err != nil {
+ return fmt.Errorf("parsing ref %q: %w", src, err)
+ }
+ refToImage[ref] = img
+ }
+
+ w, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer w.Close()
+
+ return legacy.MultiWrite(refToImage, w)
+}
+
+// SaveOCI writes the v1.Image img as an OCI Image Layout at path. If a layout
+// already exists at that path, it will add the image to the index.
+func SaveOCI(img v1.Image, path string) error {
+ imgMap := map[string]v1.Image{"": img}
+ return MultiSaveOCI(imgMap, path)
+}
+
+// MultiSaveOCI writes collection of v1.Image img as an OCI Image Layout at path. If a layout
+// already exists at that path, it will add the image to the index.
+func MultiSaveOCI(imgMap map[string]v1.Image, path string) error {
+ p, err := layout.FromPath(path)
+ if err != nil {
+ p, err = layout.Write(path, empty.Index)
+ if err != nil {
+ return err
+ }
+ }
+ for _, img := range imgMap {
+ if err = p.AppendImage(img); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/pkg/crane/push.go b/pkg/crane/push.go
new file mode 100644
index 0000000..6d1fbd6
--- /dev/null
+++ b/pkg/crane/push.go
@@ -0,0 +1,65 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+// Load reads the tarball at path as a v1.Image.
+func Load(path string, opt ...Option) (v1.Image, error) {
+ return LoadTag(path, "")
+}
+
+// LoadTag reads a tag from the tarball at path as a v1.Image.
+// If tag is "", will attempt to read the tarball as a single image.
+func LoadTag(path, tag string, opt ...Option) (v1.Image, error) {
+ if tag == "" {
+ return tarball.ImageFromPath(path, nil)
+ }
+
+ o := makeOptions(opt...)
+ t, err := name.NewTag(tag, o.Name...)
+ if err != nil {
+ return nil, fmt.Errorf("parsing tag %q: %w", tag, err)
+ }
+ return tarball.ImageFromPath(path, &t)
+}
+
+// Push pushes the v1.Image img to a registry as dst.
+func Push(img v1.Image, dst string, opt ...Option) error {
+ o := makeOptions(opt...)
+ tag, err := name.ParseReference(dst, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing reference %q: %w", dst, err)
+ }
+ return remote.Write(tag, img, o.Remote...)
+}
+
+// Upload pushes the v1.Layer to a given repo.
+func Upload(layer v1.Layer, repo string, opt ...Option) error {
+ o := makeOptions(opt...)
+ ref, err := name.NewRepository(repo, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing repo %q: %w", repo, err)
+ }
+
+ return remote.WriteLayer(ref, layer, o.Remote...)
+}
diff --git a/pkg/crane/tag.go b/pkg/crane/tag.go
new file mode 100644
index 0000000..13bc395
--- /dev/null
+++ b/pkg/crane/tag.go
@@ -0,0 +1,39 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package crane
+
+import (
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// Tag adds tag to the remote img.
+func Tag(img, tag string, opt ...Option) error {
+ o := makeOptions(opt...)
+ ref, err := name.ParseReference(img, o.Name...)
+ if err != nil {
+ return fmt.Errorf("parsing reference %q: %w", img, err)
+ }
+ desc, err := remote.Get(ref, o.Remote...)
+ if err != nil {
+ return fmt.Errorf("fetching %q: %w", img, err)
+ }
+
+ dst := ref.Context().Tag(tag)
+
+ return remote.Tag(dst, desc, o.Remote...)
+}
diff --git a/pkg/crane/testdata/content.tar b/pkg/crane/testdata/content.tar
new file mode 100755
index 0000000..55f4d1d
--- /dev/null
+++ b/pkg/crane/testdata/content.tar
Binary files differ
diff --git a/pkg/gcrane/copy.go b/pkg/gcrane/copy.go
new file mode 100644
index 0000000..a3362c4
--- /dev/null
+++ b/pkg/gcrane/copy.go
@@ -0,0 +1,347 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gcrane
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/google/go-containerregistry/internal/retry"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/google"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+ "golang.org/x/sync/errgroup"
+)
+
+// Keychain tries to use google-specific credential sources, falling back to
+// the DefaultKeychain (config-file based).
+var Keychain = authn.NewMultiKeychain(google.Keychain, authn.DefaultKeychain)
+
+// GCRBackoff returns a retry.Backoff that is suitable for use with gcr.io.
+//
+// These numbers are based on GCR's posted quotas:
+// https://cloud.google.com/container-registry/quotas
+// - 30k requests per 10 minutes.
+// - 500k requests per 24 hours.
+//
+// On error, we will wait for:
+// - 6 seconds (in case of very short term 429s from GCS), then
+// - 1 minute (in case of temporary network issues), then
+// - 10 minutes (to get around GCR 10 minute quotas), then fail.
+//
+// TODO: In theory, we could keep retrying until the next day to get around the 500k limit.
+func GCRBackoff() retry.Backoff {
+ return retry.Backoff{
+ Duration: 6 * time.Second,
+ Factor: 10.0,
+ Jitter: 0.1,
+ Steps: 3,
+ Cap: 1 * time.Hour,
+ }
+}
+
+// Copy copies a remote image or index from src to dst.
+func Copy(src, dst string, opts ...Option) error {
+ o := makeOptions(opts...)
+ // Just reuse crane's copy logic with gcrane's credential logic.
+ return crane.Copy(src, dst, o.crane...)
+}
+
+// CopyRepository copies everything from the src GCR repository to the
+// dst GCR repository.
+func CopyRepository(ctx context.Context, src, dst string, opts ...Option) error {
+ o := makeOptions(opts...)
+ return recursiveCopy(ctx, src, dst, o)
+}
+
+type task struct {
+ digest string
+ manifest google.ManifestInfo
+ oldRepo name.Repository
+ newRepo name.Repository
+}
+
+type copier struct {
+ srcRepo name.Repository
+ dstRepo name.Repository
+
+ tasks chan task
+ opt *options
+}
+
+func newCopier(src, dst string, o *options) (*copier, error) {
+ srcRepo, err := name.NewRepository(src)
+ if err != nil {
+ return nil, fmt.Errorf("parsing repo %q: %w", src, err)
+ }
+
+ dstRepo, err := name.NewRepository(dst)
+ if err != nil {
+ return nil, fmt.Errorf("parsing repo %q: %w", dst, err)
+ }
+
+ // A queue of size 2*jobs should keep each goroutine busy.
+ tasks := make(chan task, o.jobs*2)
+
+ return &copier{srcRepo, dstRepo, tasks, o}, nil
+}
+
+// recursiveCopy copies images from repo src to repo dst.
+func recursiveCopy(ctx context.Context, src, dst string, o *options) error {
+ c, err := newCopier(src, dst, o)
+ if err != nil {
+ return err
+ }
+
+ g, ctx := errgroup.WithContext(ctx)
+ walkFn := func(repo name.Repository, tags *google.Tags, err error) error {
+ if err != nil {
+ logs.Warn.Printf("failed walkFn for repo %s: %v", repo, err)
+ // If we hit an error when listing the repo, try re-listing with backoff.
+ if err := backoffErrors(GCRBackoff(), func() error {
+ tags, err = google.List(repo, o.google...)
+ return err
+ }); err != nil {
+ return fmt.Errorf("failed List for repo %s: %w", repo, err)
+ }
+ }
+
+ // If we hit an error when trying to diff the repo, re-diff with backoff.
+ if err := backoffErrors(GCRBackoff(), func() error {
+ return c.copyRepo(ctx, repo, tags)
+ }); err != nil {
+ return fmt.Errorf("failed to copy repo %q: %w", repo, err)
+ }
+
+ return nil
+ }
+
+ // Start walking the repo, enqueuing items in c.tasks.
+ g.Go(func() error {
+ defer close(c.tasks)
+ if err := google.Walk(c.srcRepo, walkFn, o.google...); err != nil {
+ return fmt.Errorf("failed to Walk: %w", err)
+ }
+ return nil
+ })
+
+ // Pull items off of c.tasks and copy the images.
+ for i := 0; i < o.jobs; i++ {
+ g.Go(func() error {
+ for task := range c.tasks {
+ // If we hit an error when trying to copy the images,
+ // retry with backoff.
+ if err := backoffErrors(GCRBackoff(), func() error {
+ return c.copyImages(ctx, task)
+ }); err != nil {
+ return fmt.Errorf("failed to copy %q: %w", task.digest, err)
+ }
+ }
+ return nil
+ })
+ }
+
+ return g.Wait()
+}
+
+// copyRepo figures out the name for our destination repo (newRepo), lists the
+// contents of newRepo, calculates the diff of what needs to be copied, then
+// starts a goroutine to copy each image we need, and waits for them to finish.
+func (c *copier) copyRepo(ctx context.Context, oldRepo name.Repository, tags *google.Tags) error {
+ newRepo, err := c.rename(oldRepo)
+ if err != nil {
+ return fmt.Errorf("rename failed: %w", err)
+ }
+
+ // Figure out what we actually need to copy.
+ want := tags.Manifests
+ have := make(map[string]google.ManifestInfo)
+ haveTags, err := google.List(newRepo, c.opt.google...)
+ if err != nil {
+ if !hasStatusCode(err, http.StatusNotFound) {
+ return err
+ }
+ // This is a 404 code, so we just need to copy everything.
+ logs.Warn.Printf("failed to list %s: %v", newRepo, err)
+ } else {
+ have = haveTags.Manifests
+ }
+ need := diffImages(want, have)
+
+ // Queue up every image as a task.
+ for digest, manifest := range need {
+ t := task{
+ digest: digest,
+ manifest: manifest,
+ oldRepo: oldRepo,
+ newRepo: newRepo,
+ }
+ select {
+ case c.tasks <- t:
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+
+ return nil
+}
+
+// copyImages starts a goroutine for each tag that points to the image
+// oldRepo@digest, or just copies the image by digest if there are no tags.
+func (c *copier) copyImages(_ context.Context, t task) error {
+ // We only have to explicitly copy by digest if there are no tags pointing to this manifest.
+ if len(t.manifest.Tags) == 0 {
+ srcImg := fmt.Sprintf("%s@%s", t.oldRepo, t.digest)
+ dstImg := fmt.Sprintf("%s@%s", t.newRepo, t.digest)
+
+ return crane.Copy(srcImg, dstImg, c.opt.crane...)
+ }
+
+ // We only need to push the whole image once.
+ tag := t.manifest.Tags[0]
+ srcImg := fmt.Sprintf("%s:%s", t.oldRepo, tag)
+ dstImg := fmt.Sprintf("%s:%s", t.newRepo, tag)
+
+ if err := crane.Copy(srcImg, dstImg, c.opt.crane...); err != nil {
+ return err
+ }
+
+ if len(t.manifest.Tags) <= 1 {
+ // If there's only one tag, we're done.
+ return nil
+ }
+
+ // Add the rest of the tags.
+ srcRef, err := name.ParseReference(srcImg)
+ if err != nil {
+ return err
+ }
+ desc, err := remote.Get(srcRef, c.opt.remote...)
+ if err != nil {
+ return err
+ }
+
+ for _, tag := range t.manifest.Tags[1:] {
+ dstImg := t.newRepo.Tag(tag)
+
+ if err := remote.Tag(dstImg, desc, c.opt.remote...); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Retry temporary errors, 429, and 500+ with backoff.
+func backoffErrors(bo retry.Backoff, f func() error) error {
+ p := func(err error) bool {
+ b := retry.IsTemporary(err) || hasStatusCode(err, http.StatusTooManyRequests) || isServerError(err)
+ if b {
+ logs.Warn.Printf("Retrying %v", err)
+ }
+ return b
+ }
+ return retry.Retry(f, p, bo)
+}
+
+func hasStatusCode(err error, code int) bool {
+ if err == nil {
+ return false
+ }
+ var terr *transport.Error
+ if errors.As(err, &terr) {
+ if terr.StatusCode == code {
+ return true
+ }
+ }
+ return false
+}
+
+func isServerError(err error) bool {
+ if err == nil {
+ return false
+ }
+ var terr *transport.Error
+ if errors.As(err, &terr) {
+ return terr.StatusCode >= 500
+ }
+ return false
+}
+
+// rename figures out the name of the new repository to copy to, e.g.:
+//
+// $ gcrane cp -r gcr.io/foo gcr.io/baz
+//
+// rename("gcr.io/foo/bar") == "gcr.io/baz/bar"
+func (c *copier) rename(repo name.Repository) (name.Repository, error) {
+ replaced := strings.Replace(repo.String(), c.srcRepo.String(), c.dstRepo.String(), 1)
+ return name.NewRepository(replaced, name.StrictValidation)
+}
+
+// diffImages returns a map of digests to google.ManifestInfos for images or
+// tags that are present in "want" but not in "have".
+func diffImages(want, have map[string]google.ManifestInfo) map[string]google.ManifestInfo {
+ need := make(map[string]google.ManifestInfo)
+
+ for digest, wantManifest := range want {
+ if haveManifest, ok := have[digest]; !ok {
+ // Missing the whole image, we need to copy everything.
+ need[digest] = wantManifest
+ } else {
+ missingTags := subtractStringLists(wantManifest.Tags, haveManifest.Tags)
+ if len(missingTags) == 0 {
+ continue
+ }
+
+ // Missing just some tags, add the ones we need to copy.
+ todo := wantManifest
+ todo.Tags = missingTags
+ need[digest] = todo
+ }
+ }
+
+ return need
+}
+
+// subtractStringLists returns a list of strings that are in minuend and not
+// in subtrahend; order is unimportant.
+func subtractStringLists(minuend, subtrahend []string) []string {
+ bSet := toStringSet(subtrahend)
+ difference := []string{}
+
+ for _, a := range minuend {
+ if _, ok := bSet[a]; !ok {
+ difference = append(difference, a)
+ }
+ }
+
+ return difference
+}
+
+func toStringSet(slice []string) map[string]struct{} {
+ set := make(map[string]struct{}, len(slice))
+ for _, s := range slice {
+ set[s] = struct{}{}
+ }
+ return set
+}
diff --git a/pkg/gcrane/copy_test.go b/pkg/gcrane/copy_test.go
new file mode 100644
index 0000000..e50564a
--- /dev/null
+++ b/pkg/gcrane/copy_test.go
@@ -0,0 +1,428 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gcrane
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/internal/retry"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ "github.com/google/go-containerregistry/pkg/v1/google"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type fakeXCR struct {
+ h http.Handler
+ repos map[string]google.Tags
+ t *testing.T
+}
+
+func (xcr *fakeXCR) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ xcr.t.Logf("%s %s", r.Method, r.URL)
+ if strings.HasPrefix(r.URL.Path, "/v2/") && strings.HasSuffix(r.URL.Path, "/tags/list") {
+ repo := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v2/"), "/tags/list")
+ if tags, ok := xcr.repos[repo]; !ok {
+ w.WriteHeader(http.StatusNotFound)
+ } else {
+ xcr.t.Logf("%+v", tags)
+ if err := json.NewEncoder(w).Encode(tags); err != nil {
+ xcr.t.Fatal(err)
+ }
+ }
+ } else {
+ xcr.h.ServeHTTP(w, r)
+ }
+}
+
+func newFakeXCR(t *testing.T) *fakeXCR {
+ h := registry.New()
+ return &fakeXCR{h: h, t: t}
+}
+
+func (xcr *fakeXCR) setRefs(stuff map[name.Reference]partial.Describable) error {
+ repos := make(map[string]google.Tags)
+
+ for ref, thing := range stuff {
+ repo := ref.Context().RepositoryStr()
+ tags, ok := repos[repo]
+ if !ok {
+ tags = google.Tags{
+ Name: repo,
+ Children: []string{},
+ }
+ }
+
+ // Populate the "child" field.
+ for parentPath := repo; parentPath != "."; parentPath = path.Dir(parentPath) {
+ child, parent := path.Base(parentPath), path.Dir(parentPath)
+ tags, ok := repos[parent]
+ if !ok {
+ tags = google.Tags{}
+ }
+ for _, c := range repos[parent].Children {
+ if c == child {
+ break
+ }
+ }
+ tags.Children = append(tags.Children, child)
+ repos[parent] = tags
+ }
+
+ // Populate the "manifests" and "tags" field.
+ d, err := thing.Digest()
+ if err != nil {
+ return err
+ }
+ mt, err := thing.MediaType()
+ if err != nil {
+ return err
+ }
+ if tags.Manifests == nil {
+ tags.Manifests = make(map[string]google.ManifestInfo)
+ }
+ mi, ok := tags.Manifests[d.String()]
+ if !ok {
+ mi = google.ManifestInfo{
+ MediaType: string(mt),
+ Tags: []string{},
+ }
+ }
+ if tag, ok := ref.(name.Tag); ok {
+ tags.Tags = append(tags.Tags, tag.Identifier())
+ mi.Tags = append(mi.Tags, tag.Identifier())
+ }
+ tags.Manifests[d.String()] = mi
+ repos[repo] = tags
+ }
+ xcr.repos = repos
+ return nil
+}
+
+func TestCopy(t *testing.T) {
+ logs.Warn.SetOutput(os.Stderr)
+ xcr := newFakeXCR(t)
+ s := httptest.NewServer(xcr)
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+ src := path.Join(u.Host, "test/gcrane")
+ dst := path.Join(u.Host, "test/gcrane/copy")
+
+ oneTag, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ twoTags, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ noTags, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ latestRef, err := name.ParseReference(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ oneTagRef := latestRef.Context().Tag("bar")
+
+ d, err := noTags.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ noTagsRef := latestRef.Context().Digest(d.String())
+ fooRef := latestRef.Context().Tag("foo")
+
+ // Populate this after we create it so we know the hostname.
+ if err := xcr.setRefs(map[name.Reference]partial.Describable{
+ oneTagRef: oneTag,
+ latestRef: twoTags,
+ fooRef: twoTags,
+ noTagsRef: noTags,
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := remote.Write(latestRef, twoTags); err != nil {
+ t.Fatal(err)
+ }
+ if err := remote.Write(fooRef, twoTags); err != nil {
+ t.Fatal(err)
+ }
+ if err := remote.Write(oneTagRef, oneTag); err != nil {
+ t.Fatal(err)
+ }
+ if err := remote.Write(noTagsRef, noTags); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Copy(src, dst); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := CopyRepository(context.Background(), src, dst); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRename(t *testing.T) {
+ c := copier{
+ srcRepo: name.MustParseReference("registry.example.com/foo").Context(),
+ dstRepo: name.MustParseReference("registry.example.com/bar").Context(),
+ }
+
+ got, err := c.rename(name.MustParseReference("registry.example.com/foo/sub/repo").Context())
+ if err != nil {
+ t.Fatalf("unexpected err: %v", err)
+ }
+ want := name.MustParseReference("registry.example.com/bar/sub/repo").Context()
+
+ if want.String() != got.String() {
+ t.Errorf("%s != %s", want, got)
+ }
+}
+
+func TestSubtractStringLists(t *testing.T) {
+ cases := []struct {
+ minuend []string
+ subtrahend []string
+ result []string
+ }{{
+ minuend: []string{"a", "b", "c"},
+ subtrahend: []string{"a"},
+ result: []string{"b", "c"},
+ }, {
+ minuend: []string{"a", "a", "a"},
+ subtrahend: []string{"a", "b"},
+ result: []string{},
+ }, {
+ minuend: []string{},
+ subtrahend: []string{"a", "b"},
+ result: []string{},
+ }, {
+ minuend: []string{"a", "b"},
+ subtrahend: []string{},
+ result: []string{"a", "b"},
+ }}
+
+ for _, tc := range cases {
+ want, got := tc.result, subtractStringLists(tc.minuend, tc.subtrahend)
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("subtracting string lists: %v - %v: (-want +got)\n%s", tc.minuend, tc.subtrahend, diff)
+ }
+ }
+}
+
+func TestDiffImages(t *testing.T) {
+ cases := []struct {
+ want map[string]google.ManifestInfo
+ have map[string]google.ManifestInfo
+ need map[string]google.ManifestInfo
+ }{{
+ // Have everything we need.
+ want: map[string]google.ManifestInfo{
+ "a": {
+ Tags: []string{"b", "c"},
+ },
+ },
+ have: map[string]google.ManifestInfo{
+ "a": {
+ Tags: []string{"b", "c"},
+ },
+ },
+ need: map[string]google.ManifestInfo{},
+ }, {
+ // Missing image a.
+ want: map[string]google.ManifestInfo{
+ "a": {
+ Tags: []string{"b", "c", "d"},
+ },
+ },
+ have: map[string]google.ManifestInfo{},
+ need: map[string]google.ManifestInfo{
+ "a": {
+ Tags: []string{"b", "c", "d"},
+ },
+ },
+ }, {
+ // Missing tags "b" and "d"
+ want: map[string]google.ManifestInfo{
+ "a": {
+ Tags: []string{"b", "c", "d"},
+ },
+ },
+ have: map[string]google.ManifestInfo{
+ "a": {
+ Tags: []string{"c"},
+ },
+ },
+ need: map[string]google.ManifestInfo{
+ "a": {
+ Tags: []string{"b", "d"},
+ },
+ },
+ }, {
+ // Make sure all properties get copied over.
+ want: map[string]google.ManifestInfo{
+ "a": {
+ Size: 123,
+ MediaType: string(types.DockerManifestSchema2),
+ Created: time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC),
+ Uploaded: time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC),
+ Tags: []string{"b", "c", "d"},
+ },
+ },
+ have: map[string]google.ManifestInfo{},
+ need: map[string]google.ManifestInfo{
+ "a": {
+ Size: 123,
+ MediaType: string(types.DockerManifestSchema2),
+ Created: time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC),
+ Uploaded: time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC),
+ Tags: []string{"b", "c", "d"},
+ },
+ },
+ }}
+
+ for _, tc := range cases {
+ want, got := tc.need, diffImages(tc.want, tc.have)
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("diffing images: %v - %v: (-want +got)\n%s", tc.want, tc.have, diff)
+ }
+ }
+}
+
+// Test that our backoff works the way we expect.
+func TestBackoff(t *testing.T) {
+ backoff := GCRBackoff()
+
+ if d := backoff.Step(); d > 10*time.Second {
+ t.Errorf("Duration too long: %v", d)
+ }
+ if d := backoff.Step(); d > 100*time.Second {
+ t.Errorf("Duration too long: %v", d)
+ }
+ if d := backoff.Step(); d > 1000*time.Second {
+ t.Errorf("Duration too long: %v", d)
+ }
+ if s := backoff.Steps; s != 0 {
+ t.Errorf("backoff.Steps should be 0, got %d", s)
+ }
+}
+
+func TestErrors(t *testing.T) {
+ if hasStatusCode(nil, http.StatusOK) {
+ t.Fatal("nil error should not have any status code")
+ }
+ if !hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusOK) {
+ t.Fatal("200 should be 200")
+ }
+ if hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusNotFound) {
+ t.Fatal("200 should not be 404")
+ }
+
+ if isServerError(nil) {
+ t.Fatal("nil should not be server error")
+ }
+ if isServerError(fmt.Errorf("i am a string")) {
+ t.Fatal("string should not be server error")
+ }
+ if !isServerError(&transport.Error{StatusCode: http.StatusServiceUnavailable}) {
+ t.Fatal("503 should be server error")
+ }
+ if isServerError(&transport.Error{StatusCode: http.StatusTooManyRequests}) {
+ t.Fatal("429 should not be server error")
+ }
+}
+
+func TestRetryErrors(t *testing.T) {
+ // We log a warning during retries, so we can tell if something retried by checking logs.Warn.
+ var b bytes.Buffer
+ logs.Warn.SetOutput(&b)
+
+ err := backoffErrors(retry.Backoff{
+ Duration: 1 * time.Millisecond,
+ Steps: 3,
+ }, func() error {
+ return &transport.Error{StatusCode: http.StatusTooManyRequests}
+ })
+
+ if err == nil {
+ t.Fatal("backoffErrors should return internal err, got nil")
+ }
+ var terr *transport.Error
+ if !errors.As(err, &terr) {
+ t.Fatalf("backoffErrors should return internal err, got different error: %v", err)
+ } else if terr.StatusCode != http.StatusTooManyRequests {
+ t.Fatalf("backoffErrors should return internal err, got different status code: %v", terr.StatusCode)
+ }
+
+ if b.Len() == 0 {
+ t.Fatal("backoffErrors didn't log to logs.Warn")
+ }
+}
+
+func TestBadInputs(t *testing.T) {
+ t.Parallel()
+ invalid := "@@@@@@"
+
+ // Create a valid image reference that will fail with not found.
+ s := httptest.NewServer(http.NotFoundHandler())
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ valid404 := fmt.Sprintf("%s/some/image", u.Host)
+
+ ctx := context.Background()
+
+ for _, tc := range []struct {
+ desc string
+ err error
+ }{
+ {"Copy(invalid, invalid)", Copy(invalid, invalid)},
+ {"Copy(404, invalid)", Copy(valid404, invalid)},
+ {"Copy(404, 404)", Copy(valid404, valid404)},
+ {"CopyRepository(invalid, invalid)", CopyRepository(ctx, invalid, invalid)},
+ {"CopyRepository(404, invalid)", CopyRepository(ctx, valid404, invalid)},
+ {"CopyRepository(404, 404)", CopyRepository(ctx, valid404, valid404, WithJobs(1))},
+ } {
+ if tc.err == nil {
+ t.Errorf("%s: expected err, got nil", tc.desc)
+ }
+ }
+}
diff --git a/pkg/gcrane/doc.go b/pkg/gcrane/doc.go
new file mode 100644
index 0000000..63a1bbb
--- /dev/null
+++ b/pkg/gcrane/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package gcrane holds libraries used to implement the gcrane CLI.
+package gcrane
diff --git a/pkg/gcrane/options.go b/pkg/gcrane/options.go
new file mode 100644
index 0000000..9d34c7d
--- /dev/null
+++ b/pkg/gcrane/options.go
@@ -0,0 +1,122 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gcrane
+
+import (
+ "context"
+ "net/http"
+ "runtime"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/crane"
+ "github.com/google/go-containerregistry/pkg/v1/google"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// Option is a functional option for gcrane operations.
+type Option func(*options)
+
+type options struct {
+ jobs int
+ remote []remote.Option
+ google []google.Option
+ crane []crane.Option
+}
+
+func makeOptions(opts ...Option) *options {
+ o := &options{
+ jobs: runtime.GOMAXPROCS(0),
+ remote: []remote.Option{
+ remote.WithAuthFromKeychain(Keychain),
+ },
+ google: []google.Option{
+ google.WithAuthFromKeychain(Keychain),
+ },
+ crane: []crane.Option{
+ crane.WithAuthFromKeychain(Keychain),
+ },
+ }
+
+ for _, option := range opts {
+ option(o)
+ }
+
+ return o
+}
+
+// WithJobs sets the number of concurrent jobs to run.
+//
+// The default number of jobs is GOMAXPROCS.
+func WithJobs(jobs int) Option {
+ return func(o *options) {
+ o.jobs = jobs
+ }
+}
+
+// WithTransport is a functional option for overriding the default transport
+// for remote operations.
+func WithTransport(t http.RoundTripper) Option {
+ return func(o *options) {
+ o.remote = append(o.remote, remote.WithTransport(t))
+ o.google = append(o.google, google.WithTransport(t))
+ o.crane = append(o.crane, crane.WithTransport(t))
+ }
+}
+
+// WithUserAgent adds the given string to the User-Agent header for any HTTP
+// requests.
+func WithUserAgent(ua string) Option {
+ return func(o *options) {
+ o.remote = append(o.remote, remote.WithUserAgent(ua))
+ o.google = append(o.google, google.WithUserAgent(ua))
+ o.crane = append(o.crane, crane.WithUserAgent(ua))
+ }
+}
+
+// WithContext is a functional option for setting the context.
+func WithContext(ctx context.Context) Option {
+ return func(o *options) {
+ o.remote = append(o.remote, remote.WithContext(ctx))
+ o.google = append(o.google, google.WithContext(ctx))
+ o.crane = append(o.crane, crane.WithContext(ctx))
+ }
+}
+
+// WithKeychain is a functional option for overriding the default
+// authenticator for remote operations, using an authn.Keychain to find
+// credentials.
+//
+// By default, gcrane will use gcrane.Keychain.
+func WithKeychain(keys authn.Keychain) Option {
+ return func(o *options) {
+ // Replace the default keychain at position 0.
+ o.remote[0] = remote.WithAuthFromKeychain(keys)
+ o.google[0] = google.WithAuthFromKeychain(keys)
+ o.crane[0] = crane.WithAuthFromKeychain(keys)
+ }
+}
+
+// WithAuth is a functional option for overriding the default authenticator
+// for remote operations.
+//
+// By default, gcrane will use gcrane.Keychain.
+func WithAuth(auth authn.Authenticator) Option {
+ return func(o *options) {
+ // Replace the default keychain at position 0.
+ o.remote[0] = remote.WithAuth(auth)
+ o.google[0] = google.WithAuth(auth)
+ o.crane[0] = crane.WithAuth(auth)
+ }
+}
diff --git a/pkg/gcrane/options_test.go b/pkg/gcrane/options_test.go
new file mode 100644
index 0000000..c2ce2f9
--- /dev/null
+++ b/pkg/gcrane/options_test.go
@@ -0,0 +1,58 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gcrane
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+)
+
+func TestOptions(t *testing.T) {
+ o := makeOptions()
+ if len(o.remote) != 1 {
+ t.Errorf("remote should default to Keychain")
+ }
+ if len(o.crane) != 1 {
+ t.Errorf("crane should default to Keychain")
+ }
+ if len(o.google) != 1 {
+ t.Errorf("google should default to Keychain")
+ }
+
+ o = makeOptions(WithAuth(authn.Anonymous), WithKeychain(authn.DefaultKeychain))
+ if len(o.remote) != 1 {
+ t.Errorf("WithKeychain should replace remote[0]")
+ }
+ if len(o.crane) != 1 {
+ t.Errorf("WithKeychain should replace crane[0]")
+ }
+ if len(o.google) != 1 {
+ t.Errorf("WithKeychain should replace google[0]")
+ }
+
+ o = makeOptions(WithTransport(http.DefaultTransport), WithUserAgent("hi"), WithContext(context.TODO()))
+ if len(o.remote) != 4 {
+ t.Errorf("wrong number of options: %d", len(o.remote))
+ }
+ if len(o.crane) != 4 {
+ t.Errorf("wrong number of options: %d", len(o.crane))
+ }
+ if len(o.google) != 4 {
+ t.Errorf("wrong number of options: %d", len(o.google))
+ }
+}
diff --git a/pkg/legacy/config.go b/pkg/legacy/config.go
new file mode 100644
index 0000000..3364bec
--- /dev/null
+++ b/pkg/legacy/config.go
@@ -0,0 +1,33 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package legacy
+
+import (
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// LayerConfigFile is the configuration file that holds the metadata describing
+// a v1 layer. See:
+// https://github.com/moby/moby/blob/master/image/spec/v1.md
+type LayerConfigFile struct {
+ v1.ConfigFile
+
+ ContainerConfig v1.Config `json:"container_config,omitempty"`
+
+ ID string `json:"id,omitempty"`
+ Parent string `json:"parent,omitempty"`
+ Throwaway bool `json:"throwaway,omitempty"`
+ Comment string `json:"comment,omitempty"`
+}
diff --git a/pkg/legacy/doc.go b/pkg/legacy/doc.go
new file mode 100644
index 0000000..1d16688
--- /dev/null
+++ b/pkg/legacy/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package legacy provides functionality to work with docker images in the v1
+// format.
+// See: https://github.com/moby/moby/blob/master/image/spec/v1.md
+package legacy
diff --git a/pkg/legacy/tarball/README.md b/pkg/legacy/tarball/README.md
new file mode 100644
index 0000000..90b88c7
--- /dev/null
+++ b/pkg/legacy/tarball/README.md
@@ -0,0 +1,6 @@
+# `legacy/tarball`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball)
+
+This package implements support for writing legacy tarballs, as described
+[here](https://github.com/moby/moby/blob/749d90e10f989802638ae542daf54257f3bf71f2/image/spec/v1.2.md#combined-image-json--filesystem-changeset-format).
diff --git a/pkg/legacy/tarball/doc.go b/pkg/legacy/tarball/doc.go
new file mode 100644
index 0000000..62684d6
--- /dev/null
+++ b/pkg/legacy/tarball/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package tarball provides facilities for writing v1 docker images
+// (https://github.com/moby/moby/blob/master/image/spec/v1.md) from/to a tarball
+// on-disk.
+package tarball
diff --git a/pkg/legacy/tarball/write.go b/pkg/legacy/tarball/write.go
new file mode 100644
index 0000000..e3f173c
--- /dev/null
+++ b/pkg/legacy/tarball/write.go
@@ -0,0 +1,374 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tarball
+
+import (
+ "archive/tar"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/legacy"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+// repositoriesTarDescriptor represents the repositories file inside a `docker save` tarball.
+type repositoriesTarDescriptor map[string]map[string]string
+
+// v1Layer represents a layer with metadata needed by the v1 image spec https://github.com/moby/moby/blob/master/image/spec/v1.md.
+type v1Layer struct {
+ // config is the layer metadata.
+ config *legacy.LayerConfigFile
+ // layer is the v1.Layer object this v1Layer represents.
+ layer v1.Layer
+}
+
+// json returns the raw bytes of the json metadata of the given v1Layer.
+func (l *v1Layer) json() ([]byte, error) {
+ return json.Marshal(l.config)
+}
+
+// version returns the raw bytes of the "VERSION" file of the given v1Layer.
+func (l *v1Layer) version() []byte {
+ return []byte("1.0")
+}
+
+// v1LayerID computes the v1 image format layer id for the given v1.Layer with the given v1 parent ID and raw image config.
+func v1LayerID(layer v1.Layer, parentID string, rawConfig []byte) (string, error) {
+ d, err := layer.Digest()
+ if err != nil {
+ return "", fmt.Errorf("unable to get layer digest to generate v1 layer ID: %w", err)
+ }
+ s := fmt.Sprintf("%s %s", d.Hex, parentID)
+ if len(rawConfig) != 0 {
+ s = fmt.Sprintf("%s %s", s, string(rawConfig))
+ }
+
+ h, _, _ := v1.SHA256(strings.NewReader(s))
+ return h.Hex, nil
+}
+
+// newTopV1Layer creates a new v1Layer for a layer other than the top layer in a v1 image tarball.
+func newV1Layer(layer v1.Layer, parent *v1Layer, history v1.History) (*v1Layer, error) {
+ parentID := ""
+ if parent != nil {
+ parentID = parent.config.ID
+ }
+ id, err := v1LayerID(layer, parentID, nil)
+ if err != nil {
+ return nil, fmt.Errorf("unable to generate v1 layer ID: %w", err)
+ }
+ result := &v1Layer{
+ layer: layer,
+ config: &legacy.LayerConfigFile{
+ ConfigFile: v1.ConfigFile{
+ Created: history.Created,
+ Author: history.Author,
+ },
+ ContainerConfig: v1.Config{
+ Cmd: []string{history.CreatedBy},
+ },
+ ID: id,
+ Parent: parentID,
+ Throwaway: history.EmptyLayer,
+ Comment: history.Comment,
+ },
+ }
+ return result, nil
+}
+
+// newTopV1Layer creates a new v1Layer for the top layer in a v1 image tarball.
+func newTopV1Layer(layer v1.Layer, parent *v1Layer, history v1.History, imgConfig *v1.ConfigFile, rawConfig []byte) (*v1Layer, error) {
+ result, err := newV1Layer(layer, parent, history)
+ if err != nil {
+ return nil, err
+ }
+ id, err := v1LayerID(layer, result.config.Parent, rawConfig)
+ if err != nil {
+ return nil, fmt.Errorf("unable to generate v1 layer ID for top layer: %w", err)
+ }
+ result.config.ID = id
+ result.config.Architecture = imgConfig.Architecture
+ result.config.Container = imgConfig.Container
+ result.config.DockerVersion = imgConfig.DockerVersion
+ result.config.OS = imgConfig.OS
+ result.config.Config = imgConfig.Config
+ result.config.Created = imgConfig.Created
+ return result, nil
+}
+
+// splitTag splits the given tagged image name <registry>/<repository>:<tag>
+// into <registry>/<repository> and <tag>.
+func splitTag(name string) (string, string) {
+ // Split on ":"
+ parts := strings.Split(name, ":")
+ // Verify that we aren't confusing a tag for a hostname w/ port for the purposes of weak validation.
+ if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") {
+ base := strings.Join(parts[:len(parts)-1], ":")
+ tag := parts[len(parts)-1]
+ return base, tag
+ }
+ return name, ""
+}
+
+// addTags adds the given image tags to the given "repositories" file descriptor in a v1 image tarball.
+func addTags(repos repositoriesTarDescriptor, tags []string, topLayerID string) {
+ for _, t := range tags {
+ base, tag := splitTag(t)
+ tagToID, ok := repos[base]
+ if !ok {
+ tagToID = make(map[string]string)
+ repos[base] = tagToID
+ }
+ tagToID[tag] = topLayerID
+ }
+}
+
+// updateLayerSources updates the given layer digest to descriptor map with the descriptor of the given layer in the given image if it's an undistributable layer.
+func updateLayerSources(layerSources map[v1.Hash]v1.Descriptor, layer v1.Layer, img v1.Image) error {
+ d, err := layer.Digest()
+ if err != nil {
+ return err
+ }
+ // Add to LayerSources if it's a foreign layer.
+ desc, err := partial.BlobDescriptor(img, d)
+ if err != nil {
+ return err
+ }
+ if !desc.MediaType.IsDistributable() {
+ diffid, err := partial.BlobToDiffID(img, d)
+ if err != nil {
+ return err
+ }
+ layerSources[diffid] = *desc
+ }
+ return nil
+}
+
+// Write is a wrapper to write a single image in V1 format and tag to a tarball.
+func Write(ref name.Reference, img v1.Image, w io.Writer) error {
+ return MultiWrite(map[name.Reference]v1.Image{ref: img}, w)
+}
+
+// filterEmpty filters out the history corresponding to empty layers from the
+// given history.
+func filterEmpty(h []v1.History) []v1.History {
+ result := []v1.History{}
+ for _, i := range h {
+ if i.EmptyLayer {
+ continue
+ }
+ result = append(result, i)
+ }
+ return result
+}
+
+// MultiWrite writes the contents of each image to the provided reader, in the V1 image tarball format.
+// The contents are written in the following format:
+// One manifest.json file at the top level containing information about several images.
+// One repositories file mapping from the image <registry>/<repo name> to <tag> to the id of the top most layer.
+// For every layer, a directory named with the layer ID is created with the following contents:
+//
+// layer.tar - The uncompressed layer tarball.
+// <layer id>.json- Layer metadata json.
+// VERSION- Schema version string. Always set to "1.0".
+//
+// One file for the config blob, named after its SHA.
+func MultiWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
+ tf := tar.NewWriter(w)
+ defer tf.Close()
+
+ sortedImages, imageToTags := dedupRefToImage(refToImage)
+ var m tarball.Manifest
+ repos := make(repositoriesTarDescriptor)
+
+ seenLayerIDs := make(map[string]struct{})
+ for _, img := range sortedImages {
+ tags := imageToTags[img]
+
+ // Write the config.
+ cfgName, err := img.ConfigName()
+ if err != nil {
+ return err
+ }
+ cfgFileName := fmt.Sprintf("%s.json", cfgName.Hex)
+ cfgBlob, err := img.RawConfigFile()
+ if err != nil {
+ return err
+ }
+ if err := writeTarEntry(tf, cfgFileName, bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil {
+ return err
+ }
+ cfg, err := img.ConfigFile()
+ if err != nil {
+ return err
+ }
+
+ // Store foreign layer info.
+ layerSources := make(map[v1.Hash]v1.Descriptor)
+
+ // Write the layers.
+ layers, err := img.Layers()
+ if err != nil {
+ return err
+ }
+ history := filterEmpty(cfg.History)
+ // Create a blank config history if the config didn't have a history.
+ if len(history) == 0 && len(layers) != 0 {
+ history = make([]v1.History, len(layers))
+ } else if len(layers) != len(history) {
+ return fmt.Errorf("image config had layer history which did not match the number of layers, got len(history)=%d, len(layers)=%d, want len(history)=len(layers)", len(history), len(layers))
+ }
+ layerFiles := make([]string, len(layers))
+ var prev *v1Layer
+ for i, l := range layers {
+ if err := updateLayerSources(layerSources, l, img); err != nil {
+ return fmt.Errorf("unable to update image metadata to include undistributable layer source information: %w", err)
+ }
+ var cur *v1Layer
+ if i < (len(layers) - 1) {
+ cur, err = newV1Layer(l, prev, history[i])
+ } else {
+ cur, err = newTopV1Layer(l, prev, history[i], cfg, cfgBlob)
+ }
+ if err != nil {
+ return err
+ }
+ layerFiles[i] = fmt.Sprintf("%s/layer.tar", cur.config.ID)
+ if _, ok := seenLayerIDs[cur.config.ID]; ok {
+ prev = cur
+ continue
+ }
+ seenLayerIDs[cur.config.ID] = struct{}{}
+
+ // If the v1.Layer implements UncompressedSize efficiently, use that
+ // for the tar header. Otherwise, this iterates over Uncompressed().
+ // NOTE: If using a streaming layer, this may consume the layer.
+ size, err := partial.UncompressedSize(l)
+ if err != nil {
+ return err
+ }
+ u, err := l.Uncompressed()
+ if err != nil {
+ return err
+ }
+ defer u.Close()
+ if err := writeTarEntry(tf, layerFiles[i], u, size); err != nil {
+ return err
+ }
+
+ j, err := cur.json()
+ if err != nil {
+ return err
+ }
+ if err := writeTarEntry(tf, fmt.Sprintf("%s/json", cur.config.ID), bytes.NewReader(j), int64(len(j))); err != nil {
+ return err
+ }
+ v := cur.version()
+ if err := writeTarEntry(tf, fmt.Sprintf("%s/VERSION", cur.config.ID), bytes.NewReader(v), int64(len(v))); err != nil {
+ return err
+ }
+ prev = cur
+ }
+
+ // Generate the tar descriptor and write it.
+ m = append(m, tarball.Descriptor{
+ Config: cfgFileName,
+ RepoTags: tags,
+ Layers: layerFiles,
+ LayerSources: layerSources,
+ })
+ // prev should be the top layer here. Use it to add the image tags
+ // to the tarball repositories file.
+ addTags(repos, tags, prev.config.ID)
+ }
+
+ mBytes, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+
+ if err := writeTarEntry(tf, "manifest.json", bytes.NewReader(mBytes), int64(len(mBytes))); err != nil {
+ return err
+ }
+ reposBytes, err := json.Marshal(&repos)
+ if err != nil {
+ return err
+ }
+ if err := writeTarEntry(tf, "repositories", bytes.NewReader(reposBytes), int64(len(reposBytes))); err != nil {
+ return err
+ }
+ return nil
+}
+
+func dedupRefToImage(refToImage map[name.Reference]v1.Image) ([]v1.Image, map[v1.Image][]string) {
+ imageToTags := make(map[v1.Image][]string)
+
+ for ref, img := range refToImage {
+ if tag, ok := ref.(name.Tag); ok {
+ if tags, ok := imageToTags[img]; ok && tags != nil {
+ imageToTags[img] = append(tags, tag.String())
+ } else {
+ imageToTags[img] = []string{tag.String()}
+ }
+ } else {
+ if _, ok := imageToTags[img]; !ok {
+ imageToTags[img] = nil
+ }
+ }
+ }
+
+ // Force specific order on tags
+ imgs := []v1.Image{}
+ for img, tags := range imageToTags {
+ sort.Strings(tags)
+ imgs = append(imgs, img)
+ }
+
+ sort.Slice(imgs, func(i, j int) bool {
+ cfI, err := imgs[i].ConfigName()
+ if err != nil {
+ return false
+ }
+ cfJ, err := imgs[j].ConfigName()
+ if err != nil {
+ return false
+ }
+ return cfI.Hex < cfJ.Hex
+ })
+
+ return imgs, imageToTags
+}
+
+// Writes a file to the provided writer with a corresponding tar header
+func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error {
+ hdr := &tar.Header{
+ Mode: 0644,
+ Typeflag: tar.TypeReg,
+ Size: size,
+ Name: path,
+ }
+ if err := tf.WriteHeader(hdr); err != nil {
+ return err
+ }
+ _, err := io.Copy(tf, r)
+ return err
+}
diff --git a/pkg/legacy/tarball/write_test.go b/pkg/legacy/tarball/write_test.go
new file mode 100644
index 0000000..33e658c
--- /dev/null
+++ b/pkg/legacy/tarball/write_test.go
@@ -0,0 +1,615 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tarball
+
+import (
+ "archive/tar"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/compare"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestWrite(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file.")
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image
+ randImage, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image: %v", err)
+ }
+ tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag: %v", err)
+ }
+ o, err := os.Create(fp.Name())
+ if err != nil {
+ t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
+ }
+ defer o.Close()
+ if err := Write(tag, randImage, o); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+
+ // Make sure the image is valid and can be loaded.
+ // Load it both by nil and by its name.
+ for _, it := range []*name.Tag{nil, &tag} {
+ tarImage, err := tarball.ImageFromPath(fp.Name(), it)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+ if err := validate.Image(tarImage); err != nil {
+ t.Errorf("validate.Image: %v", err)
+ }
+ if err := compare.Images(randImage, tarImage); err != nil {
+ t.Errorf("compare.Images: %v", err)
+ }
+ }
+
+ // Try loading a different tag, it should error.
+ fakeTag, err := name.NewTag("gcr.io/notthistag:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error generating tag: %v", err)
+ }
+ if _, err := tarball.ImageFromPath(fp.Name(), &fakeTag); err == nil {
+ t.Errorf("Expected error loading tag %v from image", fakeTag)
+ }
+}
+
+func TestMultiWriteSameImage(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file.")
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image
+ randImage, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image.")
+ }
+
+ // Make two tags that point to the random image above.
+ tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag1.")
+ }
+ tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag2.")
+ }
+ dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test dig3.")
+ }
+ refToImage := make(map[name.Reference]v1.Image)
+ refToImage[tag1] = randImage
+ refToImage[tag2] = randImage
+ refToImage[dig3] = randImage
+
+ o, err := os.Create(fp.Name())
+ if err != nil {
+ t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
+ }
+ defer o.Close()
+
+ // Write the images with both tags to the tarball
+ if err := MultiWrite(refToImage, o); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+ for ref := range refToImage {
+ tag, ok := ref.(name.Tag)
+ if !ok {
+ continue
+ }
+
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+ if err := validate.Image(tarImage); err != nil {
+ t.Errorf("validate.Image: %v", err)
+ }
+ if err := compare.Images(randImage, tarImage); err != nil {
+ t.Errorf("compare.Images: %v", err)
+ }
+ }
+}
+
+func TestMultiWriteDifferentImages(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file: %v", err)
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image
+ randImage1, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image 1: %v", err)
+ }
+
+ // Make another random image
+ randImage2, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image 2: %v", err)
+ }
+
+ // Make another random image
+ randImage3, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image 3: %v", err)
+ }
+
+ // Create two tags, one pointing to each image created.
+ tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag1: %v", err)
+ }
+ tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag2: %v", err)
+ }
+ dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test dig3: %v", err)
+ }
+ refToImage := make(map[name.Reference]v1.Image)
+ refToImage[tag1] = randImage1
+ refToImage[tag2] = randImage2
+ refToImage[dig3] = randImage3
+
+ o, err := os.Create(fp.Name())
+ if err != nil {
+ t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
+ }
+ defer o.Close()
+
+ // Write both images to the tarball.
+ if err := MultiWrite(refToImage, o); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+ for ref, img := range refToImage {
+ tag, ok := ref.(name.Tag)
+ if !ok {
+ continue
+ }
+
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+ if err := validate.Image(tarImage); err != nil {
+ t.Errorf("validate.Image: %v", err)
+ }
+ if err := compare.Images(img, tarImage); err != nil {
+ t.Errorf("compare.Images: %v", err)
+ }
+ }
+}
+
+func TestWriteForeignLayers(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file: %v", err)
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image
+ randImage, err := random.Image(256, 1)
+ if err != nil {
+ t.Fatalf("Error creating random image: %v", err)
+ }
+ tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag: %v", err)
+ }
+ randLayer, err := random.Layer(512, types.DockerForeignLayer)
+ if err != nil {
+ t.Fatalf("random.Layer: %v", err)
+ }
+ img, err := mutate.Append(randImage, mutate.Addendum{
+ Layer: randLayer,
+ URLs: []string{
+ "example.com",
+ },
+ })
+ if err != nil {
+ t.Fatalf("Unable to mutate image to add foreign layer: %v", err)
+ }
+ o, err := os.Create(fp.Name())
+ if err != nil {
+ t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
+ }
+ defer o.Close()
+ if err := Write(tag, img, o); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+
+ if err := validate.Image(tarImage); err != nil {
+ t.Fatalf("validate.Image(): %v", err)
+ }
+
+ m, err := tarImage.Manifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if got, want := m.Layers[1].MediaType, types.DockerForeignLayer; got != want {
+ t.Errorf("Wrong MediaType: %s != %s", got, want)
+ }
+ if got, want := m.Layers[1].URLs[0], "example.com"; got != want {
+ t.Errorf("Wrong URLs: %s != %s", got, want)
+ }
+}
+
+func TestMultiWriteNoHistory(t *testing.T) {
+ // Make a random image.
+ img, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image: %v", err)
+ }
+ cfg, err := img.ConfigFile()
+ if err != nil {
+ t.Fatalf("Error getting image config: %v", err)
+ }
+ // Blank out the layer history.
+ cfg.History = nil
+ tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag: %v", err)
+ }
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file: %v", err)
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+ if err := Write(tag, img, fp); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+ if err := validate.Image(tarImage); err != nil {
+ t.Fatalf("validate.Image(): %v", err)
+ }
+}
+
+func TestMultiWriteHistoryEmptyLayers(t *testing.T) {
+ // Build a history for 2 layers that is interspersed with empty layer
+ // history.
+ h := []v1.History{
+ {EmptyLayer: true},
+ {EmptyLayer: false},
+ {EmptyLayer: true},
+ {EmptyLayer: false},
+ {EmptyLayer: true},
+ }
+ // Make a random image with the number of non-empty layers from the history
+ // above.
+ img, err := random.Image(256, int64(len(filterEmpty(h))))
+ if err != nil {
+ t.Fatalf("Error creating random image: %v", err)
+ }
+ cfg, err := img.ConfigFile()
+ if err != nil {
+ t.Fatalf("Error getting image config: %v", err)
+ }
+ // Override the config history with our custom built history that includes
+ // history for empty layers.
+ cfg.History = h
+ tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag: %v", err)
+ }
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file: %v", err)
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+ if err := Write(tag, img, fp); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+ if err := validate.Image(tarImage); err != nil {
+ t.Fatalf("validate.Image(): %v", err)
+ }
+}
+
+func TestMultiWriteMismatchedHistory(t *testing.T) {
+ // Make a random image
+ img, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image: %v", err)
+ }
+ cfg, err := img.ConfigFile()
+ if err != nil {
+ t.Fatalf("Error getting image config: %v", err)
+ }
+
+ // Set the history such that number of history entries != layers. This
+ // should trigger an error during the image write.
+ cfg.History = make([]v1.History, 1)
+ img, err = mutate.ConfigFile(img, cfg)
+ if err != nil {
+ t.Fatalf("mutate.ConfigFile() = %v", err)
+ }
+
+ tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag: %v", err)
+ }
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file: %v", err)
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+ err = Write(tag, img, fp)
+ if err == nil {
+ t.Fatal("Unexpected success writing tarball, got nil, want error.")
+ }
+ want := "image config had layer history which did not match the number of layers"
+ if !strings.Contains(err.Error(), want) {
+ t.Errorf("Got unexpected error when writing image with mismatched history & layer, got %v, want substring %q", err, want)
+ }
+}
+
+type fastSizeLayer struct {
+ v1.Layer
+ size int64
+ called bool
+}
+
+func (l *fastSizeLayer) UncompressedSize() (int64, error) {
+ l.called = true
+ return l.size, nil
+}
+
+func TestUncompressedSize(t *testing.T) {
+ // Make a random image
+ img, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image: %v", err)
+ }
+
+ rand, err := random.Layer(1000, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ size, err := partial.UncompressedSize(rand)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ l := &fastSizeLayer{Layer: rand, size: size}
+
+ img, err = mutate.AppendLayers(img, l)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag: %v", err)
+ }
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file: %v", err)
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+ if err := Write(tag, img, fp); err != nil {
+ t.Fatalf("Write(): %v", err)
+ }
+ if !l.called {
+ t.Errorf("expected UncompressedSize to be called, but it wasn't")
+ }
+}
+
+// TestWriteSharedLayers tests that writing a tarball of multiple images that
+// share some layers only writes those shared layers once.
+func TestWriteSharedLayers(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file: %v", err)
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ const baseImageLayerCount = 8
+
+ // Make a random image
+ baseImage, err := random.Image(256, baseImageLayerCount)
+ if err != nil {
+ t.Fatalf("Error creating base image: %v", err)
+ }
+
+ // Make another random image
+ randLayer, err := random.Layer(256, types.DockerLayer)
+ if err != nil {
+ t.Fatalf("Error creating random layer %v", err)
+ }
+ extendedImage, err := mutate.Append(baseImage, mutate.Addendum{
+ Layer: randLayer,
+ })
+ if err != nil {
+ t.Fatalf("Error mutating base image %v", err)
+ }
+
+ // Create two tags, one pointing to each image created.
+ tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag1: %v", err)
+ }
+ tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag2: %v", err)
+ }
+ refToImage := map[name.Reference]v1.Image{
+ tag1: baseImage,
+ tag2: extendedImage,
+ }
+
+ o, err := os.Create(fp.Name())
+ if err != nil {
+ t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
+ }
+ defer o.Close()
+
+ // Write both images to the tarball.
+ if err := MultiWrite(refToImage, o); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+ for ref, img := range refToImage {
+ tag, ok := ref.(name.Tag)
+ if !ok {
+ continue
+ }
+
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+ if err := validate.Image(tarImage); err != nil {
+ t.Errorf("validate.Image: %v", err)
+ }
+ if err := compare.Images(img, tarImage); err != nil {
+ t.Errorf("compare.Images: %v", err)
+ }
+ }
+
+ wantIDs := make(map[string]struct{})
+ ids, err := v1LayerIDs(baseImage)
+ if err != nil {
+ t.Fatalf("Error getting base image IDs: %v", err)
+ }
+ for _, id := range ids {
+ wantIDs[id] = struct{}{}
+ }
+ ids, err = v1LayerIDs(extendedImage)
+ if err != nil {
+ t.Fatalf("Error getting extended image IDs: %v", err)
+ }
+ for _, id := range ids {
+ wantIDs[id] = struct{}{}
+ }
+
+ // base + extended layer + different top base layer
+ if len(wantIDs) != baseImageLayerCount+2 {
+ t.Errorf("Expected to have %d unique layer IDs but have %d", baseImageLayerCount+2, len(wantIDs))
+ }
+
+ const layerFileName = "layer.tar"
+ r := tar.NewReader(fp)
+ for {
+ hdr, err := r.Next()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ t.Fatalf("Get tar header: %v", err)
+ }
+ if filepath.Base(hdr.Name) == layerFileName {
+ id := filepath.Dir(hdr.Name)
+ if _, ok := wantIDs[id]; ok {
+ delete(wantIDs, id)
+ } else {
+ t.Errorf("Found unwanted layer with ID %q", id)
+ }
+ }
+ }
+ if len(wantIDs) != 0 {
+ for id := range wantIDs {
+ t.Errorf("Expected to find layer with ID %q but it didn't exist", id)
+ }
+ }
+}
+
+func v1LayerIDs(img v1.Image) ([]string, error) {
+ layers, err := img.Layers()
+ if err != nil {
+ return nil, fmt.Errorf("get layers: %w", err)
+ }
+ ids := make([]string, len(layers))
+ parentID := ""
+ for i, layer := range layers {
+ var rawCfg []byte
+ if i == len(layers)-1 {
+ rawCfg, err = img.RawConfigFile()
+ if err != nil {
+ return nil, fmt.Errorf("get raw config file: %w", err)
+ }
+ }
+ id, err := v1LayerID(layer, parentID, rawCfg)
+ if err != nil {
+ return nil, fmt.Errorf("get v1 layer ID: %w", err)
+ }
+
+ ids[i] = id
+ parentID = id
+ }
+ return ids, nil
+}
diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go
new file mode 100644
index 0000000..a5d25b1
--- /dev/null
+++ b/pkg/logs/logs.go
@@ -0,0 +1,39 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package logs exposes the loggers used by this library.
+package logs
+
+import (
+ "io"
+ "log"
+)
+
+var (
+ // Warn is used to log non-fatal errors.
+ Warn = log.New(io.Discard, "", log.LstdFlags)
+
+ // Progress is used to log notable, successful events.
+ Progress = log.New(io.Discard, "", log.LstdFlags)
+
+ // Debug is used to log information that is useful for debugging.
+ Debug = log.New(io.Discard, "", log.LstdFlags)
+)
+
+// Enabled checks to see if the logger's writer is set to something other
+// than io.Discard. This allows callers to avoid expensive operations
+// that will end up in /dev/null anyway.
+func Enabled(l *log.Logger) bool {
+ return l.Writer() != io.Discard
+}
diff --git a/pkg/name/README.md b/pkg/name/README.md
new file mode 100644
index 0000000..4889b84
--- /dev/null
+++ b/pkg/name/README.md
@@ -0,0 +1,3 @@
+# `name`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/name?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/name)
diff --git a/pkg/name/check.go b/pkg/name/check.go
new file mode 100644
index 0000000..e9a240a
--- /dev/null
+++ b/pkg/name/check.go
@@ -0,0 +1,43 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "strings"
+ "unicode/utf8"
+)
+
+// stripRunesFn returns a function which returns -1 (i.e. a value which
+// signals deletion in strings.Map) for runes in 'runes', and the rune otherwise.
+func stripRunesFn(runes string) func(rune) rune {
+ return func(r rune) rune {
+ if strings.ContainsRune(runes, r) {
+ return -1
+ }
+ return r
+ }
+}
+
+// checkElement checks a given named element matches character and length restrictions.
+// Returns true if the given element adheres to the given restrictions, false otherwise.
+func checkElement(name, element, allowedRunes string, minRunes, maxRunes int) error {
+ numRunes := utf8.RuneCountInString(element)
+ if (numRunes < minRunes) || (maxRunes < numRunes) {
+ return newErrBadName("%s must be between %d and %d characters in length: %s", name, minRunes, maxRunes, element)
+ } else if len(strings.Map(stripRunesFn(allowedRunes), element)) != 0 {
+ return newErrBadName("%s can only contain the characters `%s`: %s", name, allowedRunes, element)
+ }
+ return nil
+}
diff --git a/pkg/name/digest.go b/pkg/name/digest.go
new file mode 100644
index 0000000..c049c1e
--- /dev/null
+++ b/pkg/name/digest.go
@@ -0,0 +1,94 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ // nolint: depguard
+ _ "crypto/sha256" // Recommended by go-digest.
+ "strings"
+
+ "github.com/opencontainers/go-digest"
+)
+
+const digestDelim = "@"
+
+// Digest stores a digest name in a structured form.
+type Digest struct {
+ Repository
+ digest string
+ original string
+}
+
+// Ensure Digest implements Reference
+var _ Reference = (*Digest)(nil)
+
+// Context implements Reference.
+func (d Digest) Context() Repository {
+ return d.Repository
+}
+
+// Identifier implements Reference.
+func (d Digest) Identifier() string {
+ return d.DigestStr()
+}
+
+// DigestStr returns the digest component of the Digest.
+func (d Digest) DigestStr() string {
+ return d.digest
+}
+
+// Name returns the name from which the Digest was derived.
+func (d Digest) Name() string {
+ return d.Repository.Name() + digestDelim + d.DigestStr()
+}
+
+// String returns the original input string.
+func (d Digest) String() string {
+ return d.original
+}
+
+// NewDigest returns a new Digest representing the given name.
+func NewDigest(name string, opts ...Option) (Digest, error) {
+ // Split on "@"
+ parts := strings.Split(name, digestDelim)
+ if len(parts) != 2 {
+ return Digest{}, newErrBadName("a digest must contain exactly one '@' separator (e.g. registry/repository@digest) saw: %s", name)
+ }
+ base := parts[0]
+ dig := parts[1]
+ prefix := digest.Canonical.String() + ":"
+ if !strings.HasPrefix(dig, prefix) {
+ return Digest{}, newErrBadName("unsupported digest algorithm: %s", dig)
+ }
+ hex := strings.TrimPrefix(dig, prefix)
+ if err := digest.Canonical.Validate(hex); err != nil {
+ return Digest{}, err
+ }
+
+ tag, err := NewTag(base, opts...)
+ if err == nil {
+ base = tag.Repository.Name()
+ }
+
+ repo, err := NewRepository(base, opts...)
+ if err != nil {
+ return Digest{}, err
+ }
+ return Digest{
+ Repository: repo,
+ digest: dig,
+ original: name,
+ }, nil
+}
diff --git a/pkg/name/digest_test.go b/pkg/name/digest_test.go
new file mode 100644
index 0000000..85775cc
--- /dev/null
+++ b/pkg/name/digest_test.go
@@ -0,0 +1,152 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "path"
+ "strings"
+ "testing"
+)
+
+const validDigest = "sha256:deadb33fdeadb33fdeadb33fdeadb33fdeadb33fdeadb33fdeadb33fdeadb33f"
+
+var goodStrictValidationDigestNames = []string{
+ "gcr.io/g-convoy/hello-world@" + validDigest,
+ "gcr.io/google.com/project-id/hello-world@" + validDigest,
+ "us.gcr.io/project-id/sub-repo@" + validDigest,
+ "example.text/foo/bar@" + validDigest,
+}
+
+var goodStrictValidationTagDigestNames = []string{
+ "example.text/foo/bar:latest@" + validDigest,
+ "example.text:8443/foo/bar:latest@" + validDigest,
+ "example.text/foo/bar:v1.0.0-alpine@" + validDigest,
+}
+
+var goodWeakValidationDigestNames = []string{
+ "namespace/pathcomponent/image@" + validDigest,
+ "library/ubuntu@" + validDigest,
+}
+
+var goodWeakValidationTagDigestNames = []string{
+ "nginx:latest@" + validDigest,
+ "library/nginx:latest@" + validDigest,
+}
+
+var badDigestNames = []string{
+ "gcr.io/project-id/unknown-alg@unknown:abc123",
+ "gcr.io/project-id/wrong-length@sha256:d34db33fd34db33f",
+ "gcr.io/project-id/missing-digest@",
+ // https://github.com/google/go-containerregistry/issues/1394
+ "repo@sha256:" + strings.Repeat(":", 64),
+ "repo@sha256:" + strings.Repeat("sh", 32),
+ "repo@sha256:" + validDigest + "@" + validDigest,
+}
+
+func TestNewDigestStrictValidation(t *testing.T) {
+ t.Parallel()
+
+ for _, name := range goodStrictValidationDigestNames {
+ if digest, err := NewDigest(name, StrictValidation); err != nil {
+ t.Errorf("`%s` should be a valid Digest name, got error: %v", name, err)
+ } else if digest.Name() != name {
+ t.Errorf("`%v` .Name() should reproduce the original name. Wanted: %s Got: %s", digest, name, digest.Name())
+ }
+ }
+
+ for _, name := range goodStrictValidationTagDigestNames {
+ if _, err := NewDigest(name, StrictValidation); err != nil {
+ t.Errorf("`%s` should be a valid Digest name, got error: %v", name, err)
+ }
+ }
+
+ for _, name := range append(goodWeakValidationDigestNames, badDigestNames...) {
+ if repo, err := NewDigest(name, StrictValidation); err == nil {
+ t.Errorf("`%s` should be an invalid Digest name, got Digest: %#v", name, repo)
+ }
+ }
+}
+
+func TestNewDigest(t *testing.T) {
+ t.Parallel()
+
+ for _, name := range append(goodStrictValidationDigestNames, append(goodWeakValidationDigestNames, goodWeakValidationTagDigestNames...)...) {
+ if _, err := NewDigest(name, WeakValidation); err != nil {
+ t.Errorf("`%s` should be a valid Digest name, got error: %v", name, err)
+ }
+ }
+
+ for _, name := range badDigestNames {
+ if repo, err := NewDigest(name, WeakValidation); err == nil {
+ t.Errorf("`%s` should be an invalid Digest name, got Digest: %#v", name, repo)
+ }
+ }
+}
+
+func TestDigestComponents(t *testing.T) {
+ t.Parallel()
+ testRegistry := "gcr.io"
+ testRepository := "project-id/image"
+ fullRepo := path.Join(testRegistry, testRepository)
+
+ digestNameStr := testRegistry + "/" + testRepository + "@" + validDigest
+ digest, err := NewDigest(digestNameStr, StrictValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Digest name, got error: %v", digestNameStr, err)
+ }
+
+ if got := digest.String(); got != digestNameStr {
+ t.Errorf("String() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, digestNameStr, got)
+ }
+ if got := digest.Identifier(); got != validDigest {
+ t.Errorf("Identifier() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, validDigest, got)
+ }
+ actualRegistry := digest.RegistryStr()
+ if actualRegistry != testRegistry {
+ t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, testRegistry, actualRegistry)
+ }
+ actualRepository := digest.RepositoryStr()
+ if actualRepository != testRepository {
+ t.Errorf("RepositoryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, testRepository, actualRepository)
+ }
+ contextRepo := digest.Context().String()
+ if contextRepo != fullRepo {
+ t.Errorf("Context().String() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, fullRepo, contextRepo)
+ }
+ actualDigest := digest.DigestStr()
+ if actualDigest != validDigest {
+ t.Errorf("DigestStr() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, validDigest, actualDigest)
+ }
+}
+
+func TestDigestScopes(t *testing.T) {
+ t.Parallel()
+ testRegistry := "gcr.io"
+ testRepo := "project-id/image"
+ testAction := "pull"
+
+ expectedScope := strings.Join([]string{"repository", testRepo, testAction}, ":")
+
+ digestNameStr := testRegistry + "/" + testRepo + "@" + validDigest
+ digest, err := NewDigest(digestNameStr, StrictValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Digest name, got error: %v", digestNameStr, err)
+ }
+
+ actualScope := digest.Scope(testAction)
+ if actualScope != expectedScope {
+ t.Errorf("scope was incorrect for %v. Wanted: `%s` Got: `%s`", digest, expectedScope, actualScope)
+ }
+}
diff --git a/pkg/name/doc.go b/pkg/name/doc.go
new file mode 100644
index 0000000..b294794
--- /dev/null
+++ b/pkg/name/doc.go
@@ -0,0 +1,42 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package name defines structured types for representing image references.
+//
+// What's in a name? For image references, not nearly enough!
+//
+// Image references look a lot like URLs, but they differ in that they don't
+// contain the scheme (http or https), they can end with a :tag or a @digest
+// (the latter being validated), and they perform defaulting for missing
+// components.
+//
+// Since image references don't contain the scheme, we do our best to infer
+// if we use http or https from the given hostname. We allow http fallback for
+// any host that looks like localhost (localhost, 127.0.0.1, ::1), ends in
+// ".local", or is in the "private" address space per RFC 1918. For everything
+// else, we assume https only. To override this heuristic, use the Insecure
+// option.
+//
+// Image references with a digest signal to us that we should verify the content
+// of the image matches the digest. E.g. when pulling a Digest reference, we'll
+// calculate the sha256 of the manifest returned by the registry and error out
+// if it doesn't match what we asked for.
+//
+// For defaulting, we interpret "ubuntu" as
+// "index.docker.io/library/ubuntu:latest" because we add the missing repo
+// "library", the missing registry "index.docker.io", and the missing tag
+// "latest". To disable this defaulting, use the StrictValidation option. This
+// is useful e.g. to only allow image references that explicitly set a tag or
+// digest, so that you don't accidentally pull "latest".
+package name
diff --git a/pkg/name/errors.go b/pkg/name/errors.go
new file mode 100644
index 0000000..bf004ff
--- /dev/null
+++ b/pkg/name/errors.go
@@ -0,0 +1,48 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "errors"
+ "fmt"
+)
+
+// ErrBadName is an error for when a bad docker name is supplied.
+type ErrBadName struct {
+ info string
+}
+
+func (e *ErrBadName) Error() string {
+ return e.info
+}
+
+// Is reports whether target is an error of type ErrBadName
+func (e *ErrBadName) Is(target error) bool {
+ var berr *ErrBadName
+ return errors.As(target, &berr)
+}
+
+// newErrBadName returns a ErrBadName which returns the given formatted string from Error().
+func newErrBadName(fmtStr string, args ...any) *ErrBadName {
+ return &ErrBadName{fmt.Sprintf(fmtStr, args...)}
+}
+
+// IsErrBadName returns true if the given error is an ErrBadName.
+//
+// Deprecated: Use errors.Is.
+func IsErrBadName(err error) bool {
+ var berr *ErrBadName
+ return errors.As(err, &berr)
+}
diff --git a/pkg/name/errors_test.go b/pkg/name/errors_test.go
new file mode 100644
index 0000000..a9ea4da
--- /dev/null
+++ b/pkg/name/errors_test.go
@@ -0,0 +1,37 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestBadName(t *testing.T) {
+ _, err := ParseReference("@@")
+ if !IsErrBadName(err) {
+ t.Errorf("Not an ErrBadName: %v", err)
+ }
+ var berr *ErrBadName
+ if !errors.As(err, &berr) {
+ t.Errorf("Not an ErrBadName using errors.As: %v", err)
+ }
+ if err.Error() != "could not parse reference: @@" {
+ t.Errorf("Unexpected string: %v", err)
+ }
+ if !errors.Is(err, &ErrBadName{}) {
+ t.Errorf("Not an ErrBadName using errors.Is: %v", err)
+ }
+}
diff --git a/pkg/name/internal/must_test.go b/pkg/name/internal/must_test.go
new file mode 100644
index 0000000..d77d3ec
--- /dev/null
+++ b/pkg/name/internal/must_test.go
@@ -0,0 +1,27 @@
+//go:build compile
+// +build compile
+
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package internal
+
+import (
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+// This shouldn't compile.
+var _ = name.MustParseReference(strings.Join([]string{"valid", "string"}, "/"))
diff --git a/pkg/name/internal/must_test.sh b/pkg/name/internal/must_test.sh
new file mode 100755
index 0000000..91a4fd1
--- /dev/null
+++ b/pkg/name/internal/must_test.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o nounset
+set -o pipefail
+
+# Trying to compile without the build tag should work.
+go test ./pkg/name/internal
+
+# Actually trying to compile should fail.
+go test -tags=compile ./pkg/name/internal 2>&1 > /dev/null
+if [[ $? -eq 0 ]]; then
+ echo "pkg/name/internal test compiled successfully, expected failure"
+ exit 1
+fi
+echo "pkg/name/internal test successfully did not compile"
diff --git a/pkg/name/options.go b/pkg/name/options.go
new file mode 100644
index 0000000..d14fedc
--- /dev/null
+++ b/pkg/name/options.go
@@ -0,0 +1,83 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+const (
+ // DefaultRegistry is the registry name that will be used if no registry
+ // provided and the default is not overridden.
+ DefaultRegistry = "index.docker.io"
+ defaultRegistryAlias = "docker.io"
+
+ // DefaultTag is the tag name that will be used if no tag provided and the
+ // default is not overridden.
+ DefaultTag = "latest"
+)
+
+type options struct {
+ strict bool // weak by default
+ insecure bool // secure by default
+ defaultRegistry string
+ defaultTag string
+}
+
+func makeOptions(opts ...Option) options {
+ opt := options{
+ defaultRegistry: DefaultRegistry,
+ defaultTag: DefaultTag,
+ }
+ for _, o := range opts {
+ o(&opt)
+ }
+ return opt
+}
+
+// Option is a functional option for name parsing.
+type Option func(*options)
+
+// StrictValidation is an Option that requires image references to be fully
+// specified; i.e. no defaulting for registry (dockerhub), repo (library),
+// or tag (latest).
+func StrictValidation(opts *options) {
+ opts.strict = true
+}
+
+// WeakValidation is an Option that sets defaults when parsing names, see
+// StrictValidation.
+func WeakValidation(opts *options) {
+ opts.strict = false
+}
+
+// Insecure is an Option that allows image references to be fetched without TLS.
+func Insecure(opts *options) {
+ opts.insecure = true
+}
+
+// OptionFn is a function that returns an option.
+type OptionFn func() Option
+
+// WithDefaultRegistry sets the default registry that will be used if one is not
+// provided.
+func WithDefaultRegistry(r string) Option {
+ return func(opts *options) {
+ opts.defaultRegistry = r
+ }
+}
+
+// WithDefaultTag sets the default tag that will be used if one is not provided.
+func WithDefaultTag(t string) Option {
+ return func(opts *options) {
+ opts.defaultTag = t
+ }
+}
diff --git a/pkg/name/ref.go b/pkg/name/ref.go
new file mode 100644
index 0000000..912ab33
--- /dev/null
+++ b/pkg/name/ref.go
@@ -0,0 +1,75 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "fmt"
+)
+
+// Reference defines the interface that consumers use when they can
+// take either a tag or a digest.
+type Reference interface {
+ fmt.Stringer
+
+ // Context accesses the Repository context of the reference.
+ Context() Repository
+
+ // Identifier accesses the type-specific portion of the reference.
+ Identifier() string
+
+ // Name is the fully-qualified reference name.
+ Name() string
+
+ // Scope is the scope needed to access this reference.
+ Scope(string) string
+}
+
+// ParseReference parses the string as a reference, either by tag or digest.
+func ParseReference(s string, opts ...Option) (Reference, error) {
+ if t, err := NewTag(s, opts...); err == nil {
+ return t, nil
+ }
+ if d, err := NewDigest(s, opts...); err == nil {
+ return d, nil
+ }
+ return nil, newErrBadName("could not parse reference: " + s)
+}
+
+type stringConst string
+
+// MustParseReference behaves like ParseReference, but panics instead of
+// returning an error. It's intended for use in tests, or when a value is
+// expected to be valid at code authoring time.
+//
+// To discourage its use in scenarios where the value is not known at code
+// authoring time, it must be passed a string constant:
+//
+// const str = "valid/string"
+// MustParseReference(str)
+// MustParseReference("another/valid/string")
+// MustParseReference(str + "/and/more")
+//
+// These will not compile:
+//
+// var str = "valid/string"
+// MustParseReference(str)
+// MustParseReference(strings.Join([]string{"valid", "string"}, "/"))
+func MustParseReference(s stringConst, opts ...Option) Reference {
+ ref, err := ParseReference(string(s), opts...)
+ if err != nil {
+ panic(err)
+ }
+ return ref
+}
diff --git a/pkg/name/ref_test.go b/pkg/name/ref_test.go
new file mode 100644
index 0000000..c47283c
--- /dev/null
+++ b/pkg/name/ref_test.go
@@ -0,0 +1,157 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "testing"
+)
+
+var (
+ testDefaultRegistry = "registry.upbound.io"
+ testDefaultTag = "stable"
+ inputDefaultNames = []string{
+ "crossplane/provider-gcp",
+ "crossplane/provider-gcp:v0.14.0",
+ "ubuntu",
+ "gcr.io/crossplane/provider-gcp:latest",
+ }
+ outputDefaultNames = []string{
+ "registry.upbound.io/crossplane/provider-gcp:stable",
+ "registry.upbound.io/crossplane/provider-gcp:v0.14.0",
+ "registry.upbound.io/ubuntu:stable",
+ "gcr.io/crossplane/provider-gcp:latest",
+ }
+)
+
+func TestParseReferenceDefaulting(t *testing.T) {
+ for i, name := range inputDefaultNames {
+ ref, err := ParseReference(name, WithDefaultRegistry(testDefaultRegistry), WithDefaultTag(testDefaultTag))
+ if err != nil {
+ t.Errorf("ParseReference(%q); %v", name, err)
+ }
+ if ref.Name() != outputDefaultNames[i] {
+ t.Errorf("ParseReference(%q); got %v, want %v", name, ref.String(), outputDefaultNames[i])
+ }
+ }
+}
+
+func TestParseReference(t *testing.T) {
+ for _, name := range goodWeakValidationDigestNames {
+ ref, err := ParseReference(name, WeakValidation)
+ if err != nil {
+ t.Errorf("ParseReference(%q); %v", name, err)
+ }
+ dig, err := NewDigest(name, WeakValidation)
+ if err != nil {
+ t.Errorf("NewDigest(%q); %v", name, err)
+ }
+ if ref != dig {
+ t.Errorf("ParseReference(%q) != NewDigest(%q); got %v, want %v", name, name, ref, dig)
+ }
+ }
+
+ for _, name := range goodStrictValidationDigestNames {
+ ref, err := ParseReference(name, StrictValidation)
+ if err != nil {
+ t.Errorf("ParseReference(%q); %v", name, err)
+ }
+ dig, err := NewDigest(name, StrictValidation)
+ if err != nil {
+ t.Errorf("NewDigest(%q); %v", name, err)
+ }
+ if ref != dig {
+ t.Errorf("ParseReference(%q) != NewDigest(%q); got %v, want %v", name, name, ref, dig)
+ }
+ }
+
+ for _, name := range badDigestNames {
+ if _, err := ParseReference(name, WeakValidation); err == nil {
+ t.Errorf("ParseReference(%q); expected error, got none", name)
+ }
+ }
+
+ for _, name := range goodWeakValidationTagNames {
+ ref, err := ParseReference(name, WeakValidation)
+ if err != nil {
+ t.Errorf("ParseReference(%q); %v", name, err)
+ }
+ tag, err := NewTag(name, WeakValidation)
+ if err != nil {
+ t.Errorf("NewTag(%q); %v", name, err)
+ }
+ if ref != tag {
+ t.Errorf("ParseReference(%q) != NewTag(%q); got %v, want %v", name, name, ref, tag)
+ }
+ }
+
+ for _, name := range goodStrictValidationTagNames {
+ ref, err := ParseReference(name, StrictValidation)
+ if err != nil {
+ t.Errorf("ParseReference(%q); %v", name, err)
+ }
+ tag, err := NewTag(name, StrictValidation)
+ if err != nil {
+ t.Errorf("NewTag(%q); %v", name, err)
+ }
+ if ref != tag {
+ t.Errorf("ParseReference(%q) != NewTag(%q); got %v, want %v", name, name, ref, tag)
+ }
+ }
+
+ for _, name := range badTagNames {
+ if _, err := ParseReference(name, WeakValidation); err == nil {
+ t.Errorf("ParseReference(%q); expected error, got none", name)
+ }
+ }
+}
+
+func TestMustParseReference(t *testing.T) {
+ for _, name := range append(goodWeakValidationTagNames, goodWeakValidationDigestNames...) {
+ func() {
+ defer func() {
+ if err := recover(); err != nil {
+ t.Errorf("MustParseReference(%q, WeakValidation); panic: %v", name, err)
+ }
+ }()
+ MustParseReference(stringConst(name), WeakValidation)
+ }()
+ }
+
+ for _, name := range append(goodStrictValidationTagNames, goodStrictValidationDigestNames...) {
+ func() {
+ defer func() {
+ if err := recover(); err != nil {
+ t.Errorf("MustParseReference(%q, StrictValidation); panic: %v", name, err)
+ }
+ }()
+ MustParseReference(stringConst(name), StrictValidation)
+ }()
+ }
+
+ for _, name := range append(badTagNames, badDigestNames...) {
+ func() {
+ defer func() { recover() }()
+ ref := MustParseReference(stringConst(name), WeakValidation)
+ t.Errorf("MustParseReference(%q, WeakValidation) should panic, got: %#v", name, ref)
+ }()
+ }
+}
+
+// Test that MustParseReference can accept a const string or string value.
+const str = "valid/string"
+
+var _ = MustParseReference(str)
+var _ = MustParseReference("valid/string")
+var _ = MustParseReference("valid/prefix/" + str)
diff --git a/pkg/name/registry.go b/pkg/name/registry.go
new file mode 100644
index 0000000..2a26b66
--- /dev/null
+++ b/pkg/name/registry.go
@@ -0,0 +1,136 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "net"
+ "net/url"
+ "regexp"
+ "strings"
+)
+
+// Detect more complex forms of local references.
+var reLocal = regexp.MustCompile(`.*\.local(?:host)?(?::\d{1,5})?$`)
+
+// Detect the loopback IP (127.0.0.1)
+var reLoopback = regexp.MustCompile(regexp.QuoteMeta("127.0.0.1"))
+
+// Detect the loopback IPV6 (::1)
+var reipv6Loopback = regexp.MustCompile(regexp.QuoteMeta("::1"))
+
+// Registry stores a docker registry name in a structured form.
+type Registry struct {
+ insecure bool
+ registry string
+}
+
+// RegistryStr returns the registry component of the Registry.
+func (r Registry) RegistryStr() string {
+ return r.registry
+}
+
+// Name returns the name from which the Registry was derived.
+func (r Registry) Name() string {
+ return r.RegistryStr()
+}
+
+func (r Registry) String() string {
+ return r.Name()
+}
+
+// Scope returns the scope required to access the registry.
+func (r Registry) Scope(string) string {
+ // The only resource under 'registry' is 'catalog'. http://goo.gl/N9cN9Z
+ return "registry:catalog:*"
+}
+
+func (r Registry) isRFC1918() bool {
+ ipStr := strings.Split(r.Name(), ":")[0]
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ return false
+ }
+ for _, cidr := range []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"} {
+ _, block, _ := net.ParseCIDR(cidr)
+ if block.Contains(ip) {
+ return true
+ }
+ }
+ return false
+}
+
+// Scheme returns https scheme for all the endpoints except localhost or when explicitly defined.
+func (r Registry) Scheme() string {
+ if r.insecure {
+ return "http"
+ }
+ if r.isRFC1918() {
+ return "http"
+ }
+ if strings.HasPrefix(r.Name(), "localhost:") {
+ return "http"
+ }
+ if reLocal.MatchString(r.Name()) {
+ return "http"
+ }
+ if reLoopback.MatchString(r.Name()) {
+ return "http"
+ }
+ if reipv6Loopback.MatchString(r.Name()) {
+ return "http"
+ }
+ return "https"
+}
+
+func checkRegistry(name string) error {
+ // Per RFC 3986, registries (authorities) are required to be prefixed with "//"
+ // url.Host == hostname[:port] == authority
+ if url, err := url.Parse("//" + name); err != nil || url.Host != name {
+ return newErrBadName("registries must be valid RFC 3986 URI authorities: %s", name)
+ }
+ return nil
+}
+
+// NewRegistry returns a Registry based on the given name.
+// Strict validation requires explicit, valid RFC 3986 URI authorities to be given.
+func NewRegistry(name string, opts ...Option) (Registry, error) {
+ opt := makeOptions(opts...)
+ if opt.strict && len(name) == 0 {
+ return Registry{}, newErrBadName("strict validation requires the registry to be explicitly defined")
+ }
+
+ if err := checkRegistry(name); err != nil {
+ return Registry{}, err
+ }
+
+ if name == "" {
+ name = opt.defaultRegistry
+ }
+ // Rewrite "docker.io" to "index.docker.io".
+ // See: https://github.com/google/go-containerregistry/issues/68
+ if name == defaultRegistryAlias {
+ name = DefaultRegistry
+ }
+
+ return Registry{registry: name, insecure: opt.insecure}, nil
+}
+
+// NewInsecureRegistry returns an Insecure Registry based on the given name.
+//
+// Deprecated: Use the Insecure Option with NewRegistry instead.
+func NewInsecureRegistry(name string, opts ...Option) (Registry, error) {
+ opts = append(opts, Insecure)
+ return NewRegistry(name, opts...)
+}
diff --git a/pkg/name/registry_test.go b/pkg/name/registry_test.go
new file mode 100644
index 0000000..9986ee6
--- /dev/null
+++ b/pkg/name/registry_test.go
@@ -0,0 +1,252 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "testing"
+)
+
+var goodStrictValidationRegistryNames = []string{
+ "gcr.io",
+ "gcr.io:9001",
+ "index.docker.io",
+ "us.gcr.io",
+ "example.text",
+ "localhost",
+ "localhost:9090",
+}
+
+var goodWeakValidationRegistryNames = []string{
+ "",
+}
+
+var badRegistryNames = []string{
+ "white space",
+ "gcr?com",
+}
+
+func TestNewRegistryStrictValidation(t *testing.T) {
+ t.Parallel()
+
+ for _, name := range goodStrictValidationRegistryNames {
+ if registry, err := NewRegistry(name, StrictValidation); err != nil {
+ t.Errorf("`%s` should be a valid Registry name, got error: %v", name, err)
+ } else {
+ if registry.Name() != name {
+ t.Errorf("`%v` .Name() should reproduce the original name. Wanted: %s Got: %s", registry, name, registry.Name())
+ }
+ if registry.String() != name {
+ t.Errorf("`%v` .String() should reproduce the original name. Wanted: %s Got: %s", registry, name, registry.String())
+ }
+ }
+ }
+
+ for _, name := range append(goodWeakValidationRegistryNames, badRegistryNames...) {
+ if repo, err := NewRegistry(name, StrictValidation); err == nil {
+ t.Errorf("`%s` should be an invalid Registry name, got Registry: %#v", name, repo)
+ }
+ }
+}
+
+func TestNewRegistry(t *testing.T) {
+ t.Parallel()
+
+ for _, name := range append(goodStrictValidationRegistryNames, goodWeakValidationRegistryNames...) {
+ if _, err := NewRegistry(name, WeakValidation); err != nil {
+ t.Errorf("`%s` should be a valid Registry name, got error: %v", name, err)
+ }
+ }
+
+ for _, name := range badRegistryNames {
+ if repo, err := NewRegistry(name, WeakValidation); err == nil {
+ t.Errorf("`%s` should be an invalid Registry name, got Registry: %#v", name, repo)
+ }
+ }
+}
+
+func TestNewInsecureRegistry(t *testing.T) {
+ t.Parallel()
+
+ for _, name := range append(goodStrictValidationRegistryNames, goodWeakValidationRegistryNames...) {
+ if _, err := NewInsecureRegistry(name, WeakValidation); err != nil {
+ t.Errorf("`%s` should be a valid Registry name, got error: %v", name, err)
+ }
+ }
+
+ for _, name := range badRegistryNames {
+ if repo, err := NewInsecureRegistry(name, WeakValidation); err == nil {
+ t.Errorf("`%s` should be an invalid Registry name, got Registry: %#v", name, repo)
+ }
+ }
+}
+
+func TestDefaultRegistryNames(t *testing.T) {
+ testRegistries := []string{"docker.io", ""}
+
+ for _, testRegistry := range testRegistries {
+ registry, err := NewRegistry(testRegistry, WeakValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Registry name, got error: %v", testRegistry, err)
+ }
+
+ actualRegistry := registry.RegistryStr()
+ if actualRegistry != DefaultRegistry {
+ t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", registry, DefaultRegistry, actualRegistry)
+ }
+ }
+}
+
+func TestOverrideDefaultRegistryNames(t *testing.T) {
+ testRegistries := []string{"docker.io", ""}
+ expectedRegistries := []string{"index.docker.io", "gcr.io"}
+ overrideDefault := "gcr.io"
+
+ for i, testRegistry := range testRegistries {
+ registry, err := NewRegistry(testRegistry, WeakValidation, WithDefaultRegistry(overrideDefault))
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Registry name, got error: %v", testRegistry, err)
+ }
+
+ actualRegistry := registry.RegistryStr()
+ if actualRegistry != expectedRegistries[i] {
+ t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", registry, expectedRegistries[i], actualRegistry)
+ }
+ }
+}
+
+func TestRegistryComponents(t *testing.T) {
+ t.Parallel()
+ testRegistry := "gcr.io"
+
+ registry, err := NewRegistry(testRegistry, StrictValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Registry name, got error: %v", testRegistry, err)
+ }
+
+ actualRegistry := registry.RegistryStr()
+ if actualRegistry != testRegistry {
+ t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", registry, testRegistry, actualRegistry)
+ }
+}
+
+func TestRegistryScopes(t *testing.T) {
+ t.Parallel()
+ testRegistry := "gcr.io"
+ testAction := "whatever"
+
+ expectedScope := "registry:catalog:*"
+
+ registry, err := NewRegistry(testRegistry, StrictValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Registry name, got error: %v", testRegistry, err)
+ }
+
+ actualScope := registry.Scope(testAction)
+ if actualScope != expectedScope {
+ t.Errorf("scope was incorrect for %v. Wanted: `%s` Got: `%s`", registry, expectedScope, actualScope)
+ }
+}
+
+func TestIsRFC1918(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ reg string
+ result bool
+ }{{
+ reg: "index.docker.io",
+ result: false,
+ }, {
+ reg: "10.2.3.4:5000",
+ result: true,
+ }, {
+ reg: "8.8.8.8",
+ result: false,
+ }, {
+ reg: "172.16.3.4:3000",
+ result: true,
+ }, {
+ reg: "192.168.3.4",
+ result: true,
+ }, {
+ reg: "10.256.0.0:5000",
+ result: false,
+ }}
+ for _, test := range tests {
+ reg, err := NewRegistry(test.reg, WeakValidation)
+ if err != nil {
+ t.Errorf("NewRegistry(%s) = %v", test.reg, err)
+ }
+ got := reg.isRFC1918()
+ if got != test.result {
+ t.Errorf("isRFC1918(); got %v, want %v", got, test.result)
+ }
+ }
+}
+
+func TestRegistryScheme(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ domain string
+ scheme string
+ }{{
+ domain: "foo.svc.local:1234",
+ scheme: "http",
+ }, {
+ domain: "127.0.0.1:1234",
+ scheme: "http",
+ }, {
+ domain: "127.0.0.1",
+ scheme: "http",
+ }, {
+ domain: "localhost:8080",
+ scheme: "http",
+ }, {
+ domain: "gcr.io",
+ scheme: "https",
+ }, {
+ domain: "index.docker.io",
+ scheme: "https",
+ }, {
+ domain: "::1",
+ scheme: "http",
+ }, {
+ domain: "10.2.3.4:5000",
+ scheme: "http",
+ }}
+
+ for _, test := range tests {
+ reg, err := NewRegistry(test.domain, WeakValidation)
+ if err != nil {
+ t.Errorf("NewRegistry(%s) = %v", test.domain, err)
+ }
+ if got, want := reg.Scheme(), test.scheme; got != want {
+ t.Errorf("scheme(%v); got %v, want %v", reg, got, want)
+ }
+ }
+}
+
+func TestRegistryInsecureScheme(t *testing.T) {
+ t.Parallel()
+ domain := "gcr.io"
+
+ reg, err := NewInsecureRegistry(domain, WeakValidation)
+ if err != nil {
+ t.Errorf("NewRegistry(%s) = %v", domain, err)
+ }
+
+ if got := reg.Scheme(); got != "http" {
+ t.Errorf("scheme(%v); got %v, want http", reg, got)
+ }
+}
diff --git a/pkg/name/repository.go b/pkg/name/repository.go
new file mode 100644
index 0000000..9250e36
--- /dev/null
+++ b/pkg/name/repository.go
@@ -0,0 +1,121 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "fmt"
+ "strings"
+)
+
+const (
+ defaultNamespace = "library"
+ repositoryChars = "abcdefghijklmnopqrstuvwxyz0123456789_-./"
+ regRepoDelimiter = "/"
+)
+
+// Repository stores a docker repository name in a structured form.
+type Repository struct {
+ Registry
+ repository string
+}
+
+// See https://docs.docker.com/docker-hub/official_repos
+func hasImplicitNamespace(repo string, reg Registry) bool {
+ return !strings.ContainsRune(repo, '/') && reg.RegistryStr() == DefaultRegistry
+}
+
+// RepositoryStr returns the repository component of the Repository.
+func (r Repository) RepositoryStr() string {
+ if hasImplicitNamespace(r.repository, r.Registry) {
+ return fmt.Sprintf("%s/%s", defaultNamespace, r.repository)
+ }
+ return r.repository
+}
+
+// Name returns the name from which the Repository was derived.
+func (r Repository) Name() string {
+ regName := r.Registry.Name()
+ if regName != "" {
+ return regName + regRepoDelimiter + r.RepositoryStr()
+ }
+ // TODO: As far as I can tell, this is unreachable.
+ return r.RepositoryStr()
+}
+
+func (r Repository) String() string {
+ return r.Name()
+}
+
+// Scope returns the scope required to perform the given action on the registry.
+// TODO(jonjohnsonjr): consider moving scopes to a separate package.
+func (r Repository) Scope(action string) string {
+ return fmt.Sprintf("repository:%s:%s", r.RepositoryStr(), action)
+}
+
+func checkRepository(repository string) error {
+ return checkElement("repository", repository, repositoryChars, 2, 255)
+}
+
+// NewRepository returns a new Repository representing the given name, according to the given strictness.
+func NewRepository(name string, opts ...Option) (Repository, error) {
+ opt := makeOptions(opts...)
+ if len(name) == 0 {
+ return Repository{}, newErrBadName("a repository name must be specified")
+ }
+
+ var registry string
+ repo := name
+ parts := strings.SplitN(name, regRepoDelimiter, 2)
+ if len(parts) == 2 && (strings.ContainsRune(parts[0], '.') || strings.ContainsRune(parts[0], ':')) {
+ // The first part of the repository is treated as the registry domain
+ // iff it contains a '.' or ':' character, otherwise it is all repository
+ // and the domain defaults to Docker Hub.
+ registry = parts[0]
+ repo = parts[1]
+ }
+
+ if err := checkRepository(repo); err != nil {
+ return Repository{}, err
+ }
+
+ reg, err := NewRegistry(registry, opts...)
+ if err != nil {
+ return Repository{}, err
+ }
+ if hasImplicitNamespace(repo, reg) && opt.strict {
+ return Repository{}, newErrBadName("strict validation requires the full repository path (missing 'library')")
+ }
+ return Repository{reg, repo}, nil
+}
+
+// Tag returns a Tag in this Repository.
+func (r Repository) Tag(identifier string) Tag {
+ t := Tag{
+ tag: identifier,
+ Repository: r,
+ }
+ t.original = t.Name()
+ return t
+}
+
+// Digest returns a Digest in this Repository.
+func (r Repository) Digest(identifier string) Digest {
+ d := Digest{
+ digest: identifier,
+ Repository: r,
+ }
+ d.original = d.Name()
+ return d
+}
diff --git a/pkg/name/repository_test.go b/pkg/name/repository_test.go
new file mode 100644
index 0000000..790cab6
--- /dev/null
+++ b/pkg/name/repository_test.go
@@ -0,0 +1,145 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "errors"
+ "strings"
+ "testing"
+)
+
+var goodStrictValidationRepositoryNames = []string{
+ "gcr.io/g-convoy/hello-world",
+ "gcr.io/google.com/project-id/hello-world",
+ "us.gcr.io/project-id/sub-repo",
+ "example.text/foo/bar",
+ "mirror.gcr.io/ubuntu",
+ "index.docker.io/library/ubuntu",
+}
+
+var goodWeakValidationRepositoryNames = []string{
+ "namespace/pathcomponent/image",
+ "library/ubuntu",
+ "ubuntu",
+}
+
+var badRepositoryNames = []string{
+ "white space",
+ "b@char/image",
+ "",
+}
+
+func TestNewRepositoryStrictValidation(t *testing.T) {
+ t.Parallel()
+
+ for _, name := range goodStrictValidationRepositoryNames {
+ if repository, err := NewRepository(name, StrictValidation); err != nil {
+ t.Errorf("`%s` should be a valid Repository name, got error: %v", name, err)
+ } else if repository.Name() != name {
+ t.Errorf("`%v` .Name() should reproduce the original name. Wanted: %s Got: %s", repository, name, repository.Name())
+ }
+ }
+
+ for _, name := range append(goodWeakValidationRepositoryNames, badRepositoryNames...) {
+ if repo, err := NewRepository(name, StrictValidation); err == nil {
+ t.Errorf("`%s` should be an invalid repository name, got Repository: %#v", name, repo)
+ }
+ }
+}
+
+func TestNewRepository(t *testing.T) {
+ t.Parallel()
+
+ for _, name := range append(goodStrictValidationRepositoryNames, goodWeakValidationRepositoryNames...) {
+ if _, err := NewRepository(name, WeakValidation); err != nil {
+ t.Errorf("`%s` should be a valid repository name, got error: %v", name, err)
+ }
+ }
+
+ for _, name := range badRepositoryNames {
+ if repo, err := NewRepository(name, WeakValidation); err == nil {
+ t.Errorf("`%s` should be an invalid repository name, got Repository: %#v", name, repo)
+ }
+ }
+}
+
+func TestRepositoryComponents(t *testing.T) {
+ t.Parallel()
+ testRegistry := "gcr.io"
+ testRepository := "project-id/image"
+
+ repositoryNameStr := testRegistry + "/" + testRepository
+ repository, err := NewRepository(repositoryNameStr, StrictValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Repository name, got error: %v", repositoryNameStr, err)
+ }
+
+ actualRegistry := repository.RegistryStr()
+ if actualRegistry != testRegistry {
+ t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", repository, testRegistry, actualRegistry)
+ }
+ actualRepository := repository.RepositoryStr()
+ if actualRepository != testRepository {
+ t.Errorf("RepositoryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", repository, testRepository, actualRepository)
+ }
+}
+
+func TestRepositoryScopes(t *testing.T) {
+ t.Parallel()
+ testRegistry := "gcr.io"
+ testRepo := "project-id/image"
+ testAction := "pull"
+
+ expectedScope := strings.Join([]string{"repository", testRepo, testAction}, ":")
+
+ repositoryNameStr := testRegistry + "/" + testRepo
+ repository, err := NewRepository(repositoryNameStr, StrictValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Repository name, got error: %v", repositoryNameStr, err)
+ }
+
+ actualScope := repository.Scope(testAction)
+ if actualScope != expectedScope {
+ t.Errorf("scope was incorrect for %v. Wanted: `%s` Got: `%s`", repository, expectedScope, actualScope)
+ }
+}
+
+func TestRepositoryBadDefaulting(t *testing.T) {
+ var berr *ErrBadName
+ if _, err := NewRepository("index.docker.io/foo", StrictValidation); !errors.As(err, &berr) {
+ t.Errorf("Not an ErrBadName: %v", err)
+ }
+}
+
+func TestRepositoryChildren(t *testing.T) {
+ repo, err := NewRepository("example.com/repo", Insecure)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tag := repo.Tag("foo")
+ if got, want := tag.Scheme(), "http"; got != want {
+ t.Errorf("tag.Scheme(): got %s want %s", got, want)
+ }
+ if got, want := tag.String(), "example.com/repo:foo"; got != want {
+ t.Errorf("tag.String(): got %s want %s", got, want)
+ }
+ digest := repo.Digest("badf00d")
+ if got, want := digest.Scheme(), "http"; got != want {
+ t.Errorf("digest.Scheme(): got %s want %s", got, want)
+ }
+ if got, want := digest.String(), "example.com/repo@badf00d"; got != want {
+ t.Errorf("digest.String(): got %s want %s", got, want)
+ }
+}
diff --git a/pkg/name/tag.go b/pkg/name/tag.go
new file mode 100644
index 0000000..66bd1be
--- /dev/null
+++ b/pkg/name/tag.go
@@ -0,0 +1,108 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "strings"
+)
+
+const (
+ // TODO(dekkagaijin): use the docker/distribution regexes for validation.
+ tagChars = "abcdefghijklmnopqrstuvwxyz0123456789_-.ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ tagDelim = ":"
+)
+
+// Tag stores a docker tag name in a structured form.
+type Tag struct {
+ Repository
+ tag string
+ original string
+}
+
+// Ensure Tag implements Reference
+var _ Reference = (*Tag)(nil)
+
+// Context implements Reference.
+func (t Tag) Context() Repository {
+ return t.Repository
+}
+
+// Identifier implements Reference.
+func (t Tag) Identifier() string {
+ return t.TagStr()
+}
+
+// TagStr returns the tag component of the Tag.
+func (t Tag) TagStr() string {
+ return t.tag
+}
+
+// Name returns the name from which the Tag was derived.
+func (t Tag) Name() string {
+ return t.Repository.Name() + tagDelim + t.TagStr()
+}
+
+// String returns the original input string.
+func (t Tag) String() string {
+ return t.original
+}
+
+// Scope returns the scope required to perform the given action on the tag.
+func (t Tag) Scope(action string) string {
+ return t.Repository.Scope(action)
+}
+
+func checkTag(name string) error {
+ return checkElement("tag", name, tagChars, 1, 128)
+}
+
+// NewTag returns a new Tag representing the given name, according to the given strictness.
+func NewTag(name string, opts ...Option) (Tag, error) {
+ opt := makeOptions(opts...)
+ base := name
+ tag := ""
+
+ // Split on ":"
+ parts := strings.Split(name, tagDelim)
+ // Verify that we aren't confusing a tag for a hostname w/ port for the purposes of weak validation.
+ if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], regRepoDelimiter) {
+ base = strings.Join(parts[:len(parts)-1], tagDelim)
+ tag = parts[len(parts)-1]
+ }
+
+ // We don't require a tag, but if we get one check it's valid,
+ // even when not being strict.
+ // If we are being strict, we want to validate the tag regardless in case
+ // it's empty.
+ if tag != "" || opt.strict {
+ if err := checkTag(tag); err != nil {
+ return Tag{}, err
+ }
+ }
+
+ if tag == "" {
+ tag = opt.defaultTag
+ }
+
+ repo, err := NewRepository(base, opts...)
+ if err != nil {
+ return Tag{}, err
+ }
+ return Tag{
+ Repository: repo,
+ tag: tag,
+ original: name,
+ }, nil
+}
diff --git a/pkg/name/tag_test.go b/pkg/name/tag_test.go
new file mode 100644
index 0000000..e340566
--- /dev/null
+++ b/pkg/name/tag_test.go
@@ -0,0 +1,162 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package name
+
+import (
+ "path"
+ "strings"
+ "testing"
+)
+
+var goodStrictValidationTagNames = []string{
+ "gcr.io/g-convoy/hello-world:latest",
+ "gcr.io/google.com/g-convoy/hello-world:latest",
+ "gcr.io/project-id/with-nums:v2",
+ "us.gcr.io/project-id/image:with.period.in.tag",
+ "gcr.io/project-id/image:w1th-alpha_num3ric.PLUScaps",
+ "domain.with.port:9001/image:latest",
+}
+
+var goodWeakValidationTagNames = []string{
+ "namespace/pathcomponent/image",
+ "library/ubuntu",
+ "gcr.io/project-id/implicit-latest",
+ "www.example.test:12345/repo/path",
+}
+
+var badTagNames = []string{
+ "gcr.io/project-id/bad_chars:c@n'tuse",
+ "gcr.io/project-id/wrong-length:white space",
+ "gcr.io/project-id/too-many-chars:thisisthetagthatneverendsitgoesonandonmyfriendsomepeoplestartedtaggingitnotknowingwhatitwasandtheyllcontinuetaggingitforeverjustbecausethisisthetagthatneverends",
+}
+
+func TestNewTagStrictValidation(t *testing.T) {
+ t.Parallel()
+
+ for _, name := range goodStrictValidationTagNames {
+ if tag, err := NewTag(name, StrictValidation); err != nil {
+ t.Errorf("`%s` should be a valid Tag name, got error: %v", name, err)
+ } else if tag.Name() != name {
+ t.Errorf("`%v` .Name() should reproduce the original name. Wanted: %s Got: %s", tag, name, tag.Name())
+ }
+ }
+
+ for _, name := range append(goodWeakValidationTagNames, badTagNames...) {
+ if tag, err := NewTag(name, StrictValidation); err == nil {
+ t.Errorf("`%s` should be an invalid Tag name, got Tag: %#v", name, tag)
+ }
+ }
+}
+
+func TestNewTag(t *testing.T) {
+ t.Parallel()
+
+ for _, name := range append(goodStrictValidationTagNames, goodWeakValidationTagNames...) {
+ if _, err := NewTag(name, WeakValidation); err != nil {
+ t.Errorf("`%s` should be a valid Tag name, got error: %v", name, err)
+ }
+ }
+
+ for _, name := range badTagNames {
+ if tag, err := NewTag(name, WeakValidation); err == nil {
+ t.Errorf("`%s` should be an invalid Tag name, got Tag: %#v", name, tag)
+ }
+ }
+}
+
+func TestTagComponents(t *testing.T) {
+ t.Parallel()
+ testRegistry := "gcr.io"
+ testRepository := "project-id/image"
+ testTag := "latest"
+ fullRepo := path.Join(testRegistry, testRepository)
+
+ tagNameStr := testRegistry + "/" + testRepository + ":" + testTag
+ tag, err := NewTag(tagNameStr, StrictValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Tag name, got error: %v", tagNameStr, err)
+ }
+
+ actualRegistry := tag.RegistryStr()
+ if actualRegistry != testRegistry {
+ t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, testRegistry, actualRegistry)
+ }
+ actualRepository := tag.RepositoryStr()
+ if actualRepository != testRepository {
+ t.Errorf("RepositoryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, testRepository, actualRepository)
+ }
+ actualTag := tag.TagStr()
+ if actualTag != testTag {
+ t.Errorf("TagStr() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, testTag, actualTag)
+ }
+ if got, want := tag.Context().String(), fullRepo; got != want {
+ t.Errorf("Context.String() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, want, got)
+ }
+ if got, want := tag.Identifier(), testTag; got != want {
+ t.Errorf("Identifier() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, want, got)
+ }
+ if got, want := tag.String(), tagNameStr; got != want {
+ t.Errorf("String() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, want, got)
+ }
+}
+
+func TestTagScopes(t *testing.T) {
+ t.Parallel()
+ testRegistry := "gcr.io"
+ testRepo := "project-id/image"
+ testTag := "latest"
+ testAction := "pull"
+
+ expectedScope := strings.Join([]string{"repository", testRepo, testAction}, ":")
+
+ tagNameStr := testRegistry + "/" + testRepo + ":" + testTag
+ tag, err := NewTag(tagNameStr, StrictValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Tag name, got error: %v", tagNameStr, err)
+ }
+
+ actualScope := tag.Scope(testAction)
+ if actualScope != expectedScope {
+ t.Errorf("scope was incorrect for %v. Wanted: `%s` Got: `%s`", tag, expectedScope, actualScope)
+ }
+}
+
+func TestAllDefaults(t *testing.T) {
+ tagNameStr := "ubuntu"
+ tag, err := NewTag(tagNameStr, WeakValidation)
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Tag name, got error: %v", tagNameStr, err)
+ }
+
+ expectedName := "index.docker.io/library/ubuntu:latest"
+ actualName := tag.Name()
+ if actualName != expectedName {
+ t.Errorf("Name() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, expectedName, actualName)
+ }
+}
+
+func TestOverrideDefault(t *testing.T) {
+ tagNameStr := "ubuntu"
+ tag, err := NewTag(tagNameStr, WeakValidation, WithDefaultTag("other"))
+ if err != nil {
+ t.Fatalf("`%s` should be a valid Tag name, got error: %v", tagNameStr, err)
+ }
+
+ expectedName := "index.docker.io/library/ubuntu:other"
+ actualName := tag.Name()
+ if actualName != expectedName {
+ t.Errorf("Name() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, expectedName, actualName)
+ }
+}
diff --git a/pkg/registry/README.md b/pkg/registry/README.md
new file mode 100644
index 0000000..5e58bbc
--- /dev/null
+++ b/pkg/registry/README.md
@@ -0,0 +1,14 @@
+# `pkg/registry`
+
+This package implements a Docker v2 registry and the OCI distribution specification.
+
+It is designed to be used anywhere a low dependency container registry is needed, with an initial focus on tests.
+
+Its goal is to be standards compliant and its strictness will increase over time.
+
+This is currently a low flightmiles system. It's likely quite safe to use in tests; If you're using it in production, please let us know how and send us PRs for integration tests.
+
+Before sending a PR, understand that the expectation of this package is that it remain free of extraneous dependencies.
+This means that we expect `pkg/registry` to only have dependencies on Go's standard library, and other packages in `go-containerregistry`.
+
+You may be asked to change your code to reduce dependencies, and your PR might be rejected if this is deemed impossible.
diff --git a/pkg/registry/blobs.go b/pkg/registry/blobs.go
new file mode 100644
index 0000000..4bf2c65
--- /dev/null
+++ b/pkg/registry/blobs.go
@@ -0,0 +1,483 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "math/rand"
+ "net/http"
+ "path"
+ "strings"
+ "sync"
+
+ "github.com/google/go-containerregistry/internal/verify"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// Returns whether this url should be handled by the blob handler
+// This is complicated because blob is indicated by the trailing path, not the leading path.
+// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-a-layer
+// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-a-layer
+func isBlob(req *http.Request) bool {
+ elem := strings.Split(req.URL.Path, "/")
+ elem = elem[1:]
+ if elem[len(elem)-1] == "" {
+ elem = elem[:len(elem)-1]
+ }
+ if len(elem) < 3 {
+ return false
+ }
+ return elem[len(elem)-2] == "blobs" || (elem[len(elem)-3] == "blobs" &&
+ elem[len(elem)-2] == "uploads")
+}
+
+// blobHandler represents a minimal blob storage backend, capable of serving
+// blob contents.
+type blobHandler interface {
+ // Get gets the blob contents, or errNotFound if the blob wasn't found.
+ Get(ctx context.Context, repo string, h v1.Hash) (io.ReadCloser, error)
+}
+
+// blobStatHandler is an extension interface representing a blob storage
+// backend that can serve metadata about blobs.
+type blobStatHandler interface {
+ // Stat returns the size of the blob, or errNotFound if the blob wasn't
+ // found, or redirectError if the blob can be found elsewhere.
+ Stat(ctx context.Context, repo string, h v1.Hash) (int64, error)
+}
+
+// blobPutHandler is an extension interface representing a blob storage backend
+// that can write blob contents.
+type blobPutHandler interface {
+ // Put puts the blob contents.
+ //
+ // The contents will be verified against the expected size and digest
+ // as the contents are read, and an error will be returned if these
+ // don't match. Implementations should return that error, or a wrapper
+ // around that error, to return the correct error when these don't match.
+ Put(ctx context.Context, repo string, h v1.Hash, rc io.ReadCloser) error
+}
+
+// blobDeleteHandler is an extension interface representing a blob storage
+// backend that can delete blob contents.
+type blobDeleteHandler interface {
+ // Delete the blob contents.
+ Delete(ctx context.Context, repo string, h v1.Hash) error
+}
+
+// redirectError represents a signal that the blob handler doesn't have the blob
+// contents, but that those contents are at another location which registry
+// clients should redirect to.
+type redirectError struct {
+ // Location is the location to find the contents.
+ Location string
+
+ // Code is the HTTP redirect status code to return to clients.
+ Code int
+}
+
+func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) }
+
+// errNotFound represents an error locating the blob.
+var errNotFound = errors.New("not found")
+
+type memHandler struct {
+ m map[string][]byte
+ lock sync.Mutex
+}
+
+func (m *memHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ b, found := m.m[h.String()]
+ if !found {
+ return 0, errNotFound
+ }
+ return int64(len(b)), nil
+}
+func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ b, found := m.m[h.String()]
+ if !found {
+ return nil, errNotFound
+ }
+ return io.NopCloser(bytes.NewReader(b)), nil
+}
+func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadCloser) error {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ defer rc.Close()
+ all, err := io.ReadAll(rc)
+ if err != nil {
+ return err
+ }
+ m.m[h.String()] = all
+ return nil
+}
+func (m *memHandler) Delete(_ context.Context, _ string, h v1.Hash) error {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ if _, found := m.m[h.String()]; !found {
+ return errNotFound
+ }
+
+ delete(m.m, h.String())
+ return nil
+}
+
+// blobs
+type blobs struct {
+ blobHandler blobHandler
+
+ // Each upload gets a unique id that writes occur to until finalized.
+ uploads map[string][]byte
+ lock sync.Mutex
+ log *log.Logger
+}
+
+func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError {
+ elem := strings.Split(req.URL.Path, "/")
+ elem = elem[1:]
+ if elem[len(elem)-1] == "" {
+ elem = elem[:len(elem)-1]
+ }
+ // Must have a path of form /v2/{name}/blobs/{upload,sha256:}
+ if len(elem) < 4 {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "NAME_INVALID",
+ Message: "blobs must be attached to a repo",
+ }
+ }
+ target := elem[len(elem)-1]
+ service := elem[len(elem)-2]
+ digest := req.URL.Query().Get("digest")
+ contentRange := req.Header.Get("Content-Range")
+
+ repo := req.URL.Host + path.Join(elem[1:len(elem)-2]...)
+
+ switch req.Method {
+ case http.MethodHead:
+ h, err := v1.NewHash(target)
+ if err != nil {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "NAME_INVALID",
+ Message: "invalid digest",
+ }
+ }
+
+ var size int64
+ if bsh, ok := b.blobHandler.(blobStatHandler); ok {
+ size, err = bsh.Stat(req.Context(), repo, h)
+ if errors.Is(err, errNotFound) {
+ return regErrBlobUnknown
+ } else if err != nil {
+ var rerr redirectError
+ if errors.As(err, &rerr) {
+ http.Redirect(resp, req, rerr.Location, rerr.Code)
+ return nil
+ }
+ return regErrInternal(err)
+ }
+ } else {
+ rc, err := b.blobHandler.Get(req.Context(), repo, h)
+ if errors.Is(err, errNotFound) {
+ return regErrBlobUnknown
+ } else if err != nil {
+ var rerr redirectError
+ if errors.As(err, &rerr) {
+ http.Redirect(resp, req, rerr.Location, rerr.Code)
+ return nil
+ }
+ return regErrInternal(err)
+ }
+ defer rc.Close()
+ size, err = io.Copy(io.Discard, rc)
+ if err != nil {
+ return regErrInternal(err)
+ }
+ }
+
+ resp.Header().Set("Content-Length", fmt.Sprint(size))
+ resp.Header().Set("Docker-Content-Digest", h.String())
+ resp.WriteHeader(http.StatusOK)
+ return nil
+
+ case http.MethodGet:
+ h, err := v1.NewHash(target)
+ if err != nil {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "NAME_INVALID",
+ Message: "invalid digest",
+ }
+ }
+
+ var size int64
+ var r io.Reader
+ if bsh, ok := b.blobHandler.(blobStatHandler); ok {
+ size, err = bsh.Stat(req.Context(), repo, h)
+ if errors.Is(err, errNotFound) {
+ return regErrBlobUnknown
+ } else if err != nil {
+ var rerr redirectError
+ if errors.As(err, &rerr) {
+ http.Redirect(resp, req, rerr.Location, rerr.Code)
+ return nil
+ }
+ return regErrInternal(err)
+ }
+
+ rc, err := b.blobHandler.Get(req.Context(), repo, h)
+ if errors.Is(err, errNotFound) {
+ return regErrBlobUnknown
+ } else if err != nil {
+ var rerr redirectError
+ if errors.As(err, &rerr) {
+ http.Redirect(resp, req, rerr.Location, rerr.Code)
+ return nil
+ }
+
+ return regErrInternal(err)
+ }
+ defer rc.Close()
+ r = rc
+ } else {
+ tmp, err := b.blobHandler.Get(req.Context(), repo, h)
+ if errors.Is(err, errNotFound) {
+ return regErrBlobUnknown
+ } else if err != nil {
+ var rerr redirectError
+ if errors.As(err, &rerr) {
+ http.Redirect(resp, req, rerr.Location, rerr.Code)
+ return nil
+ }
+
+ return regErrInternal(err)
+ }
+ defer tmp.Close()
+ var buf bytes.Buffer
+ io.Copy(&buf, tmp)
+ size = int64(buf.Len())
+ r = &buf
+ }
+
+ resp.Header().Set("Content-Length", fmt.Sprint(size))
+ resp.Header().Set("Docker-Content-Digest", h.String())
+ resp.WriteHeader(http.StatusOK)
+ io.Copy(resp, r)
+ return nil
+
+ case http.MethodPost:
+ bph, ok := b.blobHandler.(blobPutHandler)
+ if !ok {
+ return regErrUnsupported
+ }
+
+ // It is weird that this is "target" instead of "service", but
+ // that's how the index math works out above.
+ if target != "uploads" {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "METHOD_UNKNOWN",
+ Message: fmt.Sprintf("POST to /blobs must be followed by /uploads, got %s", target),
+ }
+ }
+
+ if digest != "" {
+ h, err := v1.NewHash(digest)
+ if err != nil {
+ return regErrDigestInvalid
+ }
+
+ vrc, err := verify.ReadCloser(req.Body, req.ContentLength, h)
+ if err != nil {
+ return regErrInternal(err)
+ }
+ defer vrc.Close()
+
+ if err = bph.Put(req.Context(), repo, h, vrc); err != nil {
+ if errors.As(err, &verify.Error{}) {
+ log.Printf("Digest mismatch: %v", err)
+ return regErrDigestMismatch
+ }
+ return regErrInternal(err)
+ }
+ resp.Header().Set("Docker-Content-Digest", h.String())
+ resp.WriteHeader(http.StatusCreated)
+ return nil
+ }
+
+ id := fmt.Sprint(rand.Int63())
+ resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-2]...), "blobs/uploads", id))
+ resp.Header().Set("Range", "0-0")
+ resp.WriteHeader(http.StatusAccepted)
+ return nil
+
+ case http.MethodPatch:
+ if service != "uploads" {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "METHOD_UNKNOWN",
+ Message: fmt.Sprintf("PATCH to /blobs must be followed by /uploads, got %s", service),
+ }
+ }
+
+ if contentRange != "" {
+ start, end := 0, 0
+ if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
+ return &regError{
+ Status: http.StatusRequestedRangeNotSatisfiable,
+ Code: "BLOB_UPLOAD_UNKNOWN",
+ Message: "We don't understand your Content-Range",
+ }
+ }
+ b.lock.Lock()
+ defer b.lock.Unlock()
+ if start != len(b.uploads[target]) {
+ return &regError{
+ Status: http.StatusRequestedRangeNotSatisfiable,
+ Code: "BLOB_UPLOAD_UNKNOWN",
+ Message: "Your content range doesn't match what we have",
+ }
+ }
+ l := bytes.NewBuffer(b.uploads[target])
+ io.Copy(l, req.Body)
+ b.uploads[target] = l.Bytes()
+ resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target))
+ resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1))
+ resp.WriteHeader(http.StatusNoContent)
+ return nil
+ }
+
+ b.lock.Lock()
+ defer b.lock.Unlock()
+ if _, ok := b.uploads[target]; ok {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "BLOB_UPLOAD_INVALID",
+ Message: "Stream uploads after first write are not allowed",
+ }
+ }
+
+ l := &bytes.Buffer{}
+ io.Copy(l, req.Body)
+
+ b.uploads[target] = l.Bytes()
+ resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target))
+ resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1))
+ resp.WriteHeader(http.StatusNoContent)
+ return nil
+
+ case http.MethodPut:
+ bph, ok := b.blobHandler.(blobPutHandler)
+ if !ok {
+ return regErrUnsupported
+ }
+
+ if service != "uploads" {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "METHOD_UNKNOWN",
+ Message: fmt.Sprintf("PUT to /blobs must be followed by /uploads, got %s", service),
+ }
+ }
+
+ if digest == "" {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "DIGEST_INVALID",
+ Message: "digest not specified",
+ }
+ }
+
+ b.lock.Lock()
+ defer b.lock.Unlock()
+
+ h, err := v1.NewHash(digest)
+ if err != nil {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "NAME_INVALID",
+ Message: "invalid digest",
+ }
+ }
+
+ defer req.Body.Close()
+ in := io.NopCloser(io.MultiReader(bytes.NewBuffer(b.uploads[target]), req.Body))
+
+ size := int64(verify.SizeUnknown)
+ if req.ContentLength > 0 {
+ size = int64(len(b.uploads[target])) + req.ContentLength
+ }
+
+ vrc, err := verify.ReadCloser(in, size, h)
+ if err != nil {
+ return regErrInternal(err)
+ }
+ defer vrc.Close()
+
+ if err := bph.Put(req.Context(), repo, h, vrc); err != nil {
+ if errors.As(err, &verify.Error{}) {
+ log.Printf("Digest mismatch: %v", err)
+ return regErrDigestMismatch
+ }
+ return regErrInternal(err)
+ }
+
+ delete(b.uploads, target)
+ resp.Header().Set("Docker-Content-Digest", h.String())
+ resp.WriteHeader(http.StatusCreated)
+ return nil
+
+ case http.MethodDelete:
+ bdh, ok := b.blobHandler.(blobDeleteHandler)
+ if !ok {
+ return regErrUnsupported
+ }
+
+ h, err := v1.NewHash(target)
+ if err != nil {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "NAME_INVALID",
+ Message: "invalid digest",
+ }
+ }
+ if err := bdh.Delete(req.Context(), repo, h); err != nil {
+ return regErrInternal(err)
+ }
+ resp.WriteHeader(http.StatusAccepted)
+ return nil
+
+ default:
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "METHOD_UNKNOWN",
+ Message: "We don't understand your method + url",
+ }
+ }
+}
diff --git a/pkg/registry/compatibility_test.go b/pkg/registry/compatibility_test.go
new file mode 100644
index 0000000..eff22c2
--- /dev/null
+++ b/pkg/registry/compatibility_test.go
@@ -0,0 +1,63 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry_test
+
+import (
+ "bytes"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+func TestPushAndPullContainer(t *testing.T) {
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+
+ r := strings.TrimPrefix(s.URL, "http://") + "/foo:latest"
+ d, err := name.NewTag(r)
+ if err != nil {
+ t.Fatalf("Unable to create tag: %v", err)
+ }
+
+ i, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatalf("Unable to make random image: %v", err)
+ }
+
+ if err := remote.Write(d, i); err != nil {
+ t.Fatalf("Error writing image: %v", err)
+ }
+
+ ref, err := name.ParseReference(r)
+ if err != nil {
+ t.Fatalf("Error parsing tag: %v", err)
+ }
+
+ ri, err := remote.Image(ref)
+ if err != nil {
+ t.Fatalf("Error reading image: %v", err)
+ }
+
+ b := &bytes.Buffer{}
+ if err := tarball.Write(ref, ri, b); err != nil {
+ t.Fatalf("Error writing image to tarball: %v", err)
+ }
+}
diff --git a/pkg/registry/depcheck_test.go b/pkg/registry/depcheck_test.go
new file mode 100644
index 0000000..ca0bec5
--- /dev/null
+++ b/pkg/registry/depcheck_test.go
@@ -0,0 +1,38 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/depcheck"
+)
+
+func TestDeps(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping slow depcheck")
+ }
+ depcheck.AssertOnlyDependencies(t, map[string][]string{
+ "github.com/google/go-containerregistry/pkg/registry": append(
+ depcheck.StdlibPackages(),
+ "github.com/google/go-containerregistry/internal/httptest",
+ "github.com/google/go-containerregistry/pkg/v1",
+ "github.com/google/go-containerregistry/pkg/v1/types",
+
+ "github.com/google/go-containerregistry/internal/verify",
+ "github.com/google/go-containerregistry/internal/and",
+ ),
+ })
+}
diff --git a/pkg/registry/error.go b/pkg/registry/error.go
new file mode 100644
index 0000000..f8e126d
--- /dev/null
+++ b/pkg/registry/error.go
@@ -0,0 +1,79 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+type regError struct {
+ Status int
+ Code string
+ Message string
+}
+
+func (r *regError) Write(resp http.ResponseWriter) error {
+ resp.WriteHeader(r.Status)
+
+ type err struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ }
+ type wrap struct {
+ Errors []err `json:"errors"`
+ }
+ return json.NewEncoder(resp).Encode(wrap{
+ Errors: []err{
+ {
+ Code: r.Code,
+ Message: r.Message,
+ },
+ },
+ })
+}
+
+// regErrInternal returns an internal server error.
+func regErrInternal(err error) *regError {
+ return &regError{
+ Status: http.StatusInternalServerError,
+ Code: "INTERNAL_SERVER_ERROR",
+ Message: err.Error(),
+ }
+}
+
+var regErrBlobUnknown = &regError{
+ Status: http.StatusNotFound,
+ Code: "BLOB_UNKNOWN",
+ Message: "Unknown blob",
+}
+
+var regErrUnsupported = &regError{
+ Status: http.StatusMethodNotAllowed,
+ Code: "UNSUPPORTED",
+ Message: "Unsupported operation",
+}
+
+var regErrDigestMismatch = &regError{
+ Status: http.StatusBadRequest,
+ Code: "DIGEST_INVALID",
+ Message: "digest does not match contents",
+}
+
+var regErrDigestInvalid = &regError{
+ Status: http.StatusBadRequest,
+ Code: "NAME_INVALID",
+ Message: "invalid digest",
+}
diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go
new file mode 100644
index 0000000..cd788f7
--- /dev/null
+++ b/pkg/registry/manifest.go
@@ -0,0 +1,430 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type catalog struct {
+ Repos []string `json:"repositories"`
+}
+
+type listTags struct {
+ Name string `json:"name"`
+ Tags []string `json:"tags"`
+}
+
+type manifest struct {
+ contentType string
+ blob []byte
+}
+
+type manifests struct {
+ // maps repo -> manifest tag/digest -> manifest
+ manifests map[string]map[string]manifest
+ lock sync.Mutex
+ log *log.Logger
+}
+
+func isManifest(req *http.Request) bool {
+ elems := strings.Split(req.URL.Path, "/")
+ elems = elems[1:]
+ if len(elems) < 4 {
+ return false
+ }
+ return elems[len(elems)-2] == "manifests"
+}
+
+func isTags(req *http.Request) bool {
+ elems := strings.Split(req.URL.Path, "/")
+ elems = elems[1:]
+ if len(elems) < 4 {
+ return false
+ }
+ return elems[len(elems)-2] == "tags"
+}
+
+func isCatalog(req *http.Request) bool {
+ elems := strings.Split(req.URL.Path, "/")
+ elems = elems[1:]
+ if len(elems) < 2 {
+ return false
+ }
+
+ return elems[len(elems)-1] == "_catalog"
+}
+
+// Returns whether this url should be handled by the referrers handler
+func isReferrers(req *http.Request) bool {
+ elems := strings.Split(req.URL.Path, "/")
+ elems = elems[1:]
+ if len(elems) < 4 {
+ return false
+ }
+ return elems[len(elems)-2] == "referrers"
+}
+
+// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest
+// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image
+func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
+ elem := strings.Split(req.URL.Path, "/")
+ elem = elem[1:]
+ target := elem[len(elem)-1]
+ repo := strings.Join(elem[1:len(elem)-2], "/")
+
+ switch req.Method {
+ case http.MethodGet:
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ c, ok := m.manifests[repo]
+ if !ok {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "NAME_UNKNOWN",
+ Message: "Unknown name",
+ }
+ }
+ m, ok := c[target]
+ if !ok {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "MANIFEST_UNKNOWN",
+ Message: "Unknown manifest",
+ }
+ }
+ h, _, _ := v1.SHA256(bytes.NewReader(m.blob))
+ resp.Header().Set("Docker-Content-Digest", h.String())
+ resp.Header().Set("Content-Type", m.contentType)
+ resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
+ resp.WriteHeader(http.StatusOK)
+ io.Copy(resp, bytes.NewReader(m.blob))
+ return nil
+
+ case http.MethodHead:
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ if _, ok := m.manifests[repo]; !ok {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "NAME_UNKNOWN",
+ Message: "Unknown name",
+ }
+ }
+ m, ok := m.manifests[repo][target]
+ if !ok {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "MANIFEST_UNKNOWN",
+ Message: "Unknown manifest",
+ }
+ }
+ h, _, _ := v1.SHA256(bytes.NewReader(m.blob))
+ resp.Header().Set("Docker-Content-Digest", h.String())
+ resp.Header().Set("Content-Type", m.contentType)
+ resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
+ resp.WriteHeader(http.StatusOK)
+ return nil
+
+ case http.MethodPut:
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ if _, ok := m.manifests[repo]; !ok {
+ m.manifests[repo] = map[string]manifest{}
+ }
+ b := &bytes.Buffer{}
+ io.Copy(b, req.Body)
+ h, _, _ := v1.SHA256(bytes.NewReader(b.Bytes()))
+ digest := h.String()
+ mf := manifest{
+ blob: b.Bytes(),
+ contentType: req.Header.Get("Content-Type"),
+ }
+
+ // If the manifest is a manifest list, check that the manifest
+ // list's constituent manifests are already uploaded.
+ // This isn't strictly required by the registry API, but some
+ // registries require this.
+ if types.MediaType(mf.contentType).IsIndex() {
+ im, err := v1.ParseIndexManifest(b)
+ if err != nil {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "MANIFEST_INVALID",
+ Message: err.Error(),
+ }
+ }
+ for _, desc := range im.Manifests {
+ if !desc.MediaType.IsDistributable() {
+ continue
+ }
+ if desc.MediaType.IsIndex() || desc.MediaType.IsImage() {
+ if _, found := m.manifests[repo][desc.Digest.String()]; !found {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "MANIFEST_UNKNOWN",
+ Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest),
+ }
+ }
+ } else {
+ // TODO: Probably want to do an existence check for blobs.
+ m.log.Printf("TODO: Check blobs for %q", desc.Digest)
+ }
+ }
+ }
+
+ // Allow future references by target (tag) and immutable digest.
+ // See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier.
+ m.manifests[repo][target] = mf
+ m.manifests[repo][digest] = mf
+ resp.Header().Set("Docker-Content-Digest", digest)
+ resp.WriteHeader(http.StatusCreated)
+ return nil
+
+ case http.MethodDelete:
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ if _, ok := m.manifests[repo]; !ok {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "NAME_UNKNOWN",
+ Message: "Unknown name",
+ }
+ }
+
+ _, ok := m.manifests[repo][target]
+ if !ok {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "MANIFEST_UNKNOWN",
+ Message: "Unknown manifest",
+ }
+ }
+
+ delete(m.manifests[repo], target)
+ resp.WriteHeader(http.StatusAccepted)
+ return nil
+
+ default:
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "METHOD_UNKNOWN",
+ Message: "We don't understand your method + url",
+ }
+ }
+}
+
+func (m *manifests) handleTags(resp http.ResponseWriter, req *http.Request) *regError {
+ elem := strings.Split(req.URL.Path, "/")
+ elem = elem[1:]
+ repo := strings.Join(elem[1:len(elem)-2], "/")
+
+ if req.Method == "GET" {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ c, ok := m.manifests[repo]
+ if !ok {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "NAME_UNKNOWN",
+ Message: "Unknown name",
+ }
+ }
+
+ var tags []string
+ for tag := range c {
+ if !strings.Contains(tag, "sha256:") {
+ tags = append(tags, tag)
+ }
+ }
+ sort.Strings(tags)
+
+ // https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated
+ // Offset using last query parameter.
+ if last := req.URL.Query().Get("last"); last != "" {
+ for i, t := range tags {
+ if t > last {
+ tags = tags[i:]
+ break
+ }
+ }
+ }
+
+ // Limit using n query parameter.
+ if ns := req.URL.Query().Get("n"); ns != "" {
+ if n, err := strconv.Atoi(ns); err != nil {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "BAD_REQUEST",
+ Message: fmt.Sprintf("parsing n: %v", err),
+ }
+ } else if n < len(tags) {
+ tags = tags[:n]
+ }
+ }
+
+ tagsToList := listTags{
+ Name: repo,
+ Tags: tags,
+ }
+
+ msg, _ := json.Marshal(tagsToList)
+ resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
+ resp.WriteHeader(http.StatusOK)
+ io.Copy(resp, bytes.NewReader([]byte(msg)))
+ return nil
+ }
+
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "METHOD_UNKNOWN",
+ Message: "We don't understand your method + url",
+ }
+}
+
+func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *regError {
+ query := req.URL.Query()
+ nStr := query.Get("n")
+ n := 10000
+ if nStr != "" {
+ n, _ = strconv.Atoi(nStr)
+ }
+
+ if req.Method == "GET" {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ var repos []string
+ countRepos := 0
+ // TODO: implement pagination
+ for key := range m.manifests {
+ if countRepos >= n {
+ break
+ }
+ countRepos++
+
+ repos = append(repos, key)
+ }
+
+ repositoriesToList := catalog{
+ Repos: repos,
+ }
+
+ msg, _ := json.Marshal(repositoriesToList)
+ resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
+ resp.WriteHeader(http.StatusOK)
+ io.Copy(resp, bytes.NewReader([]byte(msg)))
+ return nil
+ }
+
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "METHOD_UNKNOWN",
+ Message: "We don't understand your method + url",
+ }
+}
+
+// TODO: implement handling of artifactType querystring
+func (m *manifests) handleReferrers(resp http.ResponseWriter, req *http.Request) *regError {
+ // Ensure this is a GET request
+ if req.Method != "GET" {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "METHOD_UNKNOWN",
+ Message: "We don't understand your method + url",
+ }
+ }
+
+ elem := strings.Split(req.URL.Path, "/")
+ elem = elem[1:]
+ target := elem[len(elem)-1]
+ repo := strings.Join(elem[1:len(elem)-2], "/")
+
+ // Validate that incoming target is a valid digest
+ if _, err := v1.NewHash(target); err != nil {
+ return &regError{
+ Status: http.StatusBadRequest,
+ Code: "UNSUPPORTED",
+ Message: "Target must be a valid digest",
+ }
+ }
+
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ digestToManifestMap, repoExists := m.manifests[repo]
+ if !repoExists {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "NAME_UNKNOWN",
+ Message: "Unknown name",
+ }
+ }
+
+ im := v1.IndexManifest{
+ SchemaVersion: 2,
+ MediaType: types.OCIImageIndex,
+ Manifests: []v1.Descriptor{},
+ }
+ for digest, manifest := range digestToManifestMap {
+ h, err := v1.NewHash(digest)
+ if err != nil {
+ continue
+ }
+ var refPointer struct {
+ Subject *v1.Descriptor `json:"subject"`
+ }
+ json.Unmarshal(manifest.blob, &refPointer)
+ if refPointer.Subject == nil {
+ continue
+ }
+ referenceDigest := refPointer.Subject.Digest
+ if referenceDigest.String() != target {
+ continue
+ }
+ // At this point, we know the current digest references the target
+ var imageAsArtifact struct {
+ Config struct {
+ MediaType string `json:"mediaType"`
+ } `json:"config"`
+ }
+ json.Unmarshal(manifest.blob, &imageAsArtifact)
+ im.Manifests = append(im.Manifests, v1.Descriptor{
+ MediaType: types.MediaType(manifest.contentType),
+ Size: int64(len(manifest.blob)),
+ Digest: h,
+ ArtifactType: imageAsArtifact.Config.MediaType,
+ })
+ }
+ msg, _ := json.Marshal(&im)
+ resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
+ resp.WriteHeader(http.StatusOK)
+ io.Copy(resp, bytes.NewReader([]byte(msg)))
+ return nil
+}
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
new file mode 100644
index 0000000..303e6e7
--- /dev/null
+++ b/pkg/registry/registry.go
@@ -0,0 +1,117 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package registry implements a docker V2 registry and the OCI distribution specification.
+//
+// It is designed to be used anywhere a low dependency container registry is needed, with an
+// initial focus on tests.
+//
+// Its goal is to be standards compliant and its strictness will increase over time.
+//
+// This is currently a low flightmiles system. It's likely quite safe to use in tests; If you're using it
+// in production, please let us know how and send us CL's for integration tests.
+package registry
+
+import (
+ "log"
+ "net/http"
+ "os"
+)
+
+type registry struct {
+ log *log.Logger
+ blobs blobs
+ manifests manifests
+ referrersEnabled bool
+}
+
+// https://docs.docker.com/registry/spec/api/#api-version-check
+// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#api-version-check
+func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError {
+ if isBlob(req) {
+ return r.blobs.handle(resp, req)
+ }
+ if isManifest(req) {
+ return r.manifests.handle(resp, req)
+ }
+ if isTags(req) {
+ return r.manifests.handleTags(resp, req)
+ }
+ if isCatalog(req) {
+ return r.manifests.handleCatalog(resp, req)
+ }
+ if r.referrersEnabled && isReferrers(req) {
+ return r.manifests.handleReferrers(resp, req)
+ }
+ resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
+ if req.URL.Path != "/v2/" && req.URL.Path != "/v2" {
+ return &regError{
+ Status: http.StatusNotFound,
+ Code: "METHOD_UNKNOWN",
+ Message: "We don't understand your method + url",
+ }
+ }
+ resp.WriteHeader(200)
+ return nil
+}
+
+func (r *registry) root(resp http.ResponseWriter, req *http.Request) {
+ if rerr := r.v2(resp, req); rerr != nil {
+ r.log.Printf("%s %s %d %s %s", req.Method, req.URL, rerr.Status, rerr.Code, rerr.Message)
+ rerr.Write(resp)
+ return
+ }
+ r.log.Printf("%s %s", req.Method, req.URL)
+}
+
+// New returns a handler which implements the docker registry protocol.
+// It should be registered at the site root.
+func New(opts ...Option) http.Handler {
+ r := &registry{
+ log: log.New(os.Stderr, "", log.LstdFlags),
+ blobs: blobs{
+ blobHandler: &memHandler{m: map[string][]byte{}},
+ uploads: map[string][]byte{},
+ log: log.New(os.Stderr, "", log.LstdFlags),
+ },
+ manifests: manifests{
+ manifests: map[string]map[string]manifest{},
+ log: log.New(os.Stderr, "", log.LstdFlags),
+ },
+ }
+ for _, o := range opts {
+ o(r)
+ }
+ return http.HandlerFunc(r.root)
+}
+
+// Option describes the available options
+// for creating the registry.
+type Option func(r *registry)
+
+// Logger overrides the logger used to record requests to the registry.
+func Logger(l *log.Logger) Option {
+ return func(r *registry) {
+ r.log = l
+ r.manifests.log = l
+ r.blobs.log = l
+ }
+}
+
+// WithReferrersSupport enables the referrers API endpoint (OCI 1.1+)
+func WithReferrersSupport(enabled bool) Option {
+ return func(r *registry) {
+ r.referrersEnabled = enabled
+ }
+}
diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go
new file mode 100644
index 0000000..0ee492e
--- /dev/null
+++ b/pkg/registry/registry_test.go
@@ -0,0 +1,609 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry_test
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+const (
+ weirdIndex = `{
+ "manifests": [
+ {
+ "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ "mediaType":"application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"
+ },{
+ "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ "mediaType":"application/xml"
+ },{
+ "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ "mediaType":"application/vnd.oci.image.manifest.v1+json"
+ }
+ ]
+}`
+)
+
+func sha256String(s string) string {
+ h, _, _ := v1.SHA256(strings.NewReader(s))
+ return h.Hex
+}
+
+func TestCalls(t *testing.T) {
+ tcs := []struct {
+ Description string
+
+ // Request / setup
+ URL string
+ Digests map[string]string
+ Manifests map[string]string
+ BlobStream map[string]string
+ RequestHeader map[string]string
+
+ // Response
+ Code int
+ Header map[string]string
+ Method string
+ Body string // request body to send
+ Want string // response body to expect
+ }{
+ {
+ Description: "/v2 returns 200",
+ Method: "GET",
+ URL: "/v2",
+ Code: http.StatusOK,
+ Header: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"},
+ },
+ {
+ Description: "/v2/ returns 200",
+ Method: "GET",
+ URL: "/v2/",
+ Code: http.StatusOK,
+ Header: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"},
+ },
+ {
+ Description: "/v2/bad returns 404",
+ Method: "GET",
+ URL: "/v2/bad",
+ Code: http.StatusNotFound,
+ Header: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"},
+ },
+ {
+ Description: "GET non existent blob",
+ Method: "GET",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "HEAD non existent blob",
+ Method: "HEAD",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "GET bad digest",
+ Method: "GET",
+ URL: "/v2/foo/blobs/sha256:asd",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "HEAD bad digest",
+ Method: "HEAD",
+ URL: "/v2/foo/blobs/sha256:asd",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "bad blob verb",
+ Method: "FOO",
+ URL: "/v2/foo/blobs/sha256:asd",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "GET containerless blob",
+ Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusOK,
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ Want: "foo",
+ },
+ {
+ Description: "GET blob",
+ Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusOK,
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ Want: "foo",
+ },
+ {
+ Description: "HEAD blob",
+ Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
+ Method: "HEAD",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusOK,
+ Header: map[string]string{
+ "Content-Length": "3",
+ "Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ },
+ },
+ {
+ Description: "DELETE blob",
+ Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
+ Method: "DELETE",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusAccepted,
+ },
+ {
+ Description: "blob url with no container",
+ Method: "GET",
+ URL: "/v2/blobs/sha256:asd",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "uploadurl",
+ Method: "POST",
+ URL: "/v2/foo/blobs/uploads",
+ Code: http.StatusAccepted,
+ Header: map[string]string{"Range": "0-0"},
+ },
+ {
+ Description: "uploadurl",
+ Method: "POST",
+ URL: "/v2/foo/blobs/uploads/",
+ Code: http.StatusAccepted,
+ Header: map[string]string{"Range": "0-0"},
+ },
+ {
+ Description: "upload put missing digest",
+ Method: "PUT",
+ URL: "/v2/foo/blobs/uploads/1",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "monolithic upload good digest",
+ Method: "POST",
+ URL: "/v2/foo/blobs/uploads?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusCreated,
+ Body: "foo",
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ },
+ {
+ Description: "monolithic upload bad digest",
+ Method: "POST",
+ URL: "/v2/foo/blobs/uploads?digest=sha256:fake",
+ Code: http.StatusBadRequest,
+ Body: "foo",
+ },
+ {
+ Description: "upload good digest",
+ Method: "PUT",
+ URL: "/v2/foo/blobs/uploads/1?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusCreated,
+ Body: "foo",
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ },
+ {
+ Description: "upload bad digest",
+ Method: "PUT",
+ URL: "/v2/foo/blobs/uploads/1?digest=sha256:baddigest",
+ Code: http.StatusBadRequest,
+ Body: "foo",
+ },
+ {
+ Description: "stream upload",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ Code: http.StatusNoContent,
+ Body: "foo",
+ Header: map[string]string{
+ "Range": "0-2",
+ "Location": "/v2/foo/blobs/uploads/1",
+ },
+ },
+ {
+ Description: "stream duplicate upload",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ Code: http.StatusBadRequest,
+ Body: "foo",
+ BlobStream: map[string]string{"1": "foo"},
+ },
+ {
+ Description: "stream finish upload",
+ Method: "PUT",
+ URL: "/v2/foo/blobs/uploads/1?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ BlobStream: map[string]string{"1": "foo"},
+ Code: http.StatusCreated,
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ },
+ {
+ Description: "get missing manifest",
+ Method: "GET",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "head missing manifest",
+ Method: "HEAD",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "get missing manifest good container",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/manifests/bar",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "head missing manifest good container",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "HEAD",
+ URL: "/v2/foo/manifests/bar",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "get manifest by tag",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusOK,
+ Want: "foo",
+ },
+ {
+ Description: "get manifest by digest",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/manifests/sha256:" + sha256String("foo"),
+ Code: http.StatusOK,
+ Want: "foo",
+ },
+ {
+ Description: "head manifest",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "HEAD",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusOK,
+ },
+ {
+ Description: "create manifest",
+ Method: "PUT",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusCreated,
+ Body: "foo",
+ },
+ {
+ Description: "create index",
+ Method: "PUT",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusCreated,
+ Body: weirdIndex,
+ RequestHeader: map[string]string{
+ "Content-Type": "application/vnd.oci.image.index.v1+json",
+ },
+ Manifests: map[string]string{"foo/manifests/image": "foo"},
+ },
+ {
+ Description: "create index missing child",
+ Method: "PUT",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusNotFound,
+ Body: weirdIndex,
+ RequestHeader: map[string]string{
+ "Content-Type": "application/vnd.oci.image.index.v1+json",
+ },
+ },
+ {
+ Description: "bad index body",
+ Method: "PUT",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusBadRequest,
+ Body: "foo",
+ RequestHeader: map[string]string{
+ "Content-Type": "application/vnd.oci.image.index.v1+json",
+ },
+ },
+ {
+ Description: "bad manifest method",
+ Method: "BAR",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "Chunk upload start",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ RequestHeader: map[string]string{"Content-Range": "0-3"},
+ Code: http.StatusNoContent,
+ Body: "foo",
+ Header: map[string]string{
+ "Range": "0-2",
+ "Location": "/v2/foo/blobs/uploads/1",
+ },
+ },
+ {
+ Description: "Chunk upload bad content range",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ RequestHeader: map[string]string{"Content-Range": "0-bar"},
+ Code: http.StatusRequestedRangeNotSatisfiable,
+ Body: "foo",
+ },
+ {
+ Description: "Chunk upload overlaps previous data",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ BlobStream: map[string]string{"1": "foo"},
+ RequestHeader: map[string]string{"Content-Range": "2-5"},
+ Code: http.StatusRequestedRangeNotSatisfiable,
+ Body: "bar",
+ },
+ {
+ Description: "Chunk upload after previous data",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ BlobStream: map[string]string{"1": "foo"},
+ RequestHeader: map[string]string{"Content-Range": "3-6"},
+ Code: http.StatusNoContent,
+ Body: "bar",
+ Header: map[string]string{
+ "Range": "0-5",
+ "Location": "/v2/foo/blobs/uploads/1",
+ },
+ },
+ {
+ Description: "DELETE Unknown name",
+ Method: "DELETE",
+ URL: "/v2/test/honk/manifests/latest",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "DELETE Unknown manifest",
+ Manifests: map[string]string{"honk/manifests/latest": "honk"},
+ Method: "DELETE",
+ URL: "/v2/honk/manifests/tag-honk",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "DELETE existing manifest",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "DELETE",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusAccepted,
+ },
+ {
+ Description: "DELETE existing manifest by digest",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "DELETE",
+ URL: "/v2/foo/manifests/sha256:" + sha256String("foo"),
+ Code: http.StatusAccepted,
+ },
+ {
+ Description: "list tags",
+ Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/tags/list?n=1000",
+ Code: http.StatusOK,
+ Want: `{"name":"foo","tags":["latest","tag1"]}`,
+ },
+ {
+ Description: "limit tags",
+ Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/tags/list?n=1",
+ Code: http.StatusOK,
+ Want: `{"name":"foo","tags":["latest"]}`,
+ },
+ {
+ Description: "offset tags",
+ Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/tags/list?last=latest",
+ Code: http.StatusOK,
+ Want: `{"name":"foo","tags":["tag1"]}`,
+ },
+ {
+ Description: "list non existing tags",
+ Method: "GET",
+ URL: "/v2/foo/tags/list?n=1000",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "list repos",
+ Manifests: map[string]string{"foo/manifests/latest": "foo", "bar/manifests/latest": "bar"},
+ Method: "GET",
+ URL: "/v2/_catalog?n=1000",
+ Code: http.StatusOK,
+ },
+ {
+ Description: "fetch references",
+ Method: "GET",
+ URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusOK,
+ Manifests: map[string]string{
+ "foo/manifests/image": "foo",
+ "foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("foo") + "\"}}",
+ },
+ },
+ {
+ Description: "fetch references, subject pointing elsewhere",
+ Method: "GET",
+ URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusOK,
+ Manifests: map[string]string{
+ "foo/manifests/image": "foo",
+ "foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("nonexistant") + "\"}}",
+ },
+ },
+ {
+ Description: "fetch references, no results",
+ Method: "GET",
+ URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusOK,
+ Manifests: map[string]string{
+ "foo/manifests/image": "foo",
+ },
+ },
+ {
+ Description: "fetch references, missing repo",
+ Method: "GET",
+ URL: "/v2/does-not-exist/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "fetch references, bad target (tag vs. digest)",
+ Method: "GET",
+ URL: "/v2/foo/referrers/latest",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "fetch references, bad method",
+ Method: "POST",
+ URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusBadRequest,
+ },
+ }
+
+ for _, tc := range tcs {
+
+ var logger *log.Logger
+ testf := func(t *testing.T) {
+
+ opts := []registry.Option{registry.WithReferrersSupport(true)}
+ if logger != nil {
+ opts = append(opts, registry.Logger(logger))
+ }
+ r := registry.New(opts...)
+ s := httptest.NewServer(r)
+ defer s.Close()
+
+ for manifest, contents := range tc.Manifests {
+ u, err := url.Parse(s.URL + "/v2/" + manifest)
+ if err != nil {
+ t.Fatalf("Error parsing %q: %v", s.URL+"/v2", err)
+ }
+ req := &http.Request{
+ Method: "PUT",
+ URL: u,
+ Body: io.NopCloser(strings.NewReader(contents)),
+ }
+ t.Log(req.Method, req.URL)
+ resp, err := s.Client().Do(req)
+ if err != nil {
+ t.Fatalf("Error uploading manifest: %v", err)
+ }
+ if resp.StatusCode != http.StatusCreated {
+ body, _ := io.ReadAll(resp.Body)
+ t.Fatalf("Error uploading manifest got status: %d %s", resp.StatusCode, body)
+ }
+ t.Logf("created manifest with digest %v", resp.Header.Get("Docker-Content-Digest"))
+ }
+
+ for digest, contents := range tc.Digests {
+ u, err := url.Parse(fmt.Sprintf("%s/v2/foo/blobs/uploads/1?digest=%s", s.URL, digest))
+ if err != nil {
+ t.Fatalf("Error parsing %q: %v", s.URL+tc.URL, err)
+ }
+ req := &http.Request{
+ Method: "PUT",
+ URL: u,
+ Body: io.NopCloser(strings.NewReader(contents)),
+ }
+ t.Log(req.Method, req.URL)
+ resp, err := s.Client().Do(req)
+ if err != nil {
+ t.Fatalf("Error uploading digest: %v", err)
+ }
+ if resp.StatusCode != http.StatusCreated {
+ body, _ := io.ReadAll(resp.Body)
+ t.Fatalf("Error uploading digest got status: %d %s", resp.StatusCode, body)
+ }
+ }
+
+ for upload, contents := range tc.BlobStream {
+ u, err := url.Parse(fmt.Sprintf("%s/v2/foo/blobs/uploads/%s", s.URL, upload))
+ if err != nil {
+ t.Fatalf("Error parsing %q: %v", s.URL+tc.URL, err)
+ }
+ req := &http.Request{
+ Method: "PATCH",
+ URL: u,
+ Body: io.NopCloser(strings.NewReader(contents)),
+ }
+ t.Log(req.Method, req.URL)
+ resp, err := s.Client().Do(req)
+ if err != nil {
+ t.Fatalf("Error streaming blob: %v", err)
+ }
+ if resp.StatusCode != http.StatusNoContent {
+ body, _ := io.ReadAll(resp.Body)
+ t.Fatalf("Error streaming blob: %d %s", resp.StatusCode, body)
+ }
+
+ }
+
+ u, err := url.Parse(s.URL + tc.URL)
+ if err != nil {
+ t.Fatalf("Error parsing %q: %v", s.URL+tc.URL, err)
+ }
+ req := &http.Request{
+ Method: tc.Method,
+ URL: u,
+ Body: io.NopCloser(strings.NewReader(tc.Body)),
+ Header: map[string][]string{},
+ }
+ for k, v := range tc.RequestHeader {
+ req.Header.Set(k, v)
+ }
+ t.Log(req.Method, req.URL)
+ resp, err := s.Client().Do(req)
+ if err != nil {
+ t.Fatalf("Error getting %q: %v", tc.URL, err)
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Errorf("Reading response body: %v", err)
+ }
+ if resp.StatusCode != tc.Code {
+ t.Errorf("Incorrect status code, got %d, want %d; body: %s", resp.StatusCode, tc.Code, body)
+ }
+
+ for k, v := range tc.Header {
+ r := resp.Header.Get(k)
+ if r != v {
+ t.Errorf("Incorrect header %q received, got %q, want %q", k, r, v)
+ }
+ }
+
+ if tc.Want != "" && string(body) != tc.Want {
+ t.Errorf("Incorrect response body, got %q, want %q", body, tc.Want)
+ }
+ }
+ t.Run(tc.Description, testf)
+ logger = log.New(io.Discard, "", log.Ldate)
+ t.Run(tc.Description+" - custom log", testf)
+ }
+}
diff --git a/pkg/registry/tls.go b/pkg/registry/tls.go
new file mode 100644
index 0000000..cb2644e
--- /dev/null
+++ b/pkg/registry/tls.go
@@ -0,0 +1,29 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry
+
+import (
+ "net/http/httptest"
+
+ ggcrtest "github.com/google/go-containerregistry/internal/httptest"
+)
+
+// TLS returns an httptest server, with an http client that has been configured to
+// send all requests to the returned server. The TLS certs are generated for the given domain
+// which should correspond to the domain the image is stored in.
+// If you need a transport, Client().Transport is correctly configured.
+func TLS(domain string) (*httptest.Server, error) {
+ return ggcrtest.NewTLSServer(domain, New())
+}
diff --git a/pkg/registry/tls_test.go b/pkg/registry/tls_test.go
new file mode 100644
index 0000000..0f65b70
--- /dev/null
+++ b/pkg/registry/tls_test.go
@@ -0,0 +1,49 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry_test
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func TestTLS(t *testing.T) {
+ s, err := registry.TLS("registry.example.com")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+
+ i, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatalf("Unable to make image: %v", err)
+ }
+ rd, err := i.Digest()
+ if err != nil {
+ t.Fatalf("Unable to get image digest: %v", err)
+ }
+
+ d, err := name.NewDigest("registry.example.com/foo@" + rd.String())
+ if err != nil {
+ t.Fatalf("Unable to parse digest: %v", err)
+ }
+ if err := remote.Write(d, i, remote.WithTransport(s.Client().Transport)); err != nil {
+ t.Fatalf("Unable to write image to remote: %s", err)
+ }
+}
diff --git a/pkg/v1/cache/cache.go b/pkg/v1/cache/cache.go
new file mode 100644
index 0000000..31d9c93
--- /dev/null
+++ b/pkg/v1/cache/cache.go
@@ -0,0 +1,194 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package cache provides methods to cache layers.
+package cache
+
+import (
+ "errors"
+ "io"
+
+ "github.com/google/go-containerregistry/pkg/logs"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Cache encapsulates methods to interact with cached layers.
+type Cache interface {
+ // Put writes the Layer to the Cache.
+ //
+ // The returned Layer should be used for future operations, since lazy
+ // cachers might only populate the cache when the layer is actually
+ // consumed.
+ //
+ // The returned layer can be consumed, and the cache entry populated,
+ // by calling either Compressed or Uncompressed and consuming the
+ // returned io.ReadCloser.
+ Put(v1.Layer) (v1.Layer, error)
+
+ // Get returns the Layer cached by the given Hash, or ErrNotFound if no
+ // such layer was found.
+ Get(v1.Hash) (v1.Layer, error)
+
+ // Delete removes the Layer with the given Hash from the Cache.
+ Delete(v1.Hash) error
+}
+
+// ErrNotFound is returned by Get when no layer with the given Hash is found.
+var ErrNotFound = errors.New("layer was not found")
+
+// Image returns a new Image which wraps the given Image, whose layers will be
+// pulled from the Cache if they are found, and written to the Cache as they
+// are read from the underlying Image.
+func Image(i v1.Image, c Cache) v1.Image {
+ return &image{
+ Image: i,
+ c: c,
+ }
+}
+
+type image struct {
+ v1.Image
+ c Cache
+}
+
+func (i *image) Layers() ([]v1.Layer, error) {
+ ls, err := i.Image.Layers()
+ if err != nil {
+ return nil, err
+ }
+
+ out := make([]v1.Layer, len(ls))
+ for idx, l := range ls {
+ out[idx] = &lazyLayer{inner: l, c: i.c}
+ }
+ return out, nil
+}
+
+type lazyLayer struct {
+ inner v1.Layer
+ c Cache
+}
+
+func (l *lazyLayer) Compressed() (io.ReadCloser, error) {
+ digest, err := l.inner.Digest()
+ if err != nil {
+ return nil, err
+ }
+
+ if cl, err := l.c.Get(digest); err == nil {
+ // Layer found in the cache.
+ logs.Progress.Printf("Layer %s found (compressed) in cache", digest)
+ return cl.Compressed()
+ } else if !errors.Is(err, ErrNotFound) {
+ return nil, err
+ }
+
+ // Not cached, pull and return the real layer.
+ logs.Progress.Printf("Layer %s not found (compressed) in cache, getting", digest)
+ rl, err := l.c.Put(l.inner)
+ if err != nil {
+ return nil, err
+ }
+ return rl.Compressed()
+}
+
+func (l *lazyLayer) Uncompressed() (io.ReadCloser, error) {
+ diffID, err := l.inner.DiffID()
+ if err != nil {
+ return nil, err
+ }
+ if cl, err := l.c.Get(diffID); err == nil {
+ // Layer found in the cache.
+ logs.Progress.Printf("Layer %s found (uncompressed) in cache", diffID)
+ return cl.Uncompressed()
+ } else if !errors.Is(err, ErrNotFound) {
+ return nil, err
+ }
+
+ // Not cached, pull and return the real layer.
+ logs.Progress.Printf("Layer %s not found (uncompressed) in cache, getting", diffID)
+ rl, err := l.c.Put(l.inner)
+ if err != nil {
+ return nil, err
+ }
+ return rl.Uncompressed()
+}
+
+func (l *lazyLayer) Size() (int64, error) { return l.inner.Size() }
+func (l *lazyLayer) DiffID() (v1.Hash, error) { return l.inner.DiffID() }
+func (l *lazyLayer) Digest() (v1.Hash, error) { return l.inner.Digest() }
+func (l *lazyLayer) MediaType() (types.MediaType, error) { return l.inner.MediaType() }
+
+func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) {
+ l, err := i.c.Get(h)
+ if errors.Is(err, ErrNotFound) {
+ // Not cached, get it and write it.
+ l, err := i.Image.LayerByDigest(h)
+ if err != nil {
+ return nil, err
+ }
+ return i.c.Put(l)
+ }
+ return l, err
+}
+
+func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
+ l, err := i.c.Get(h)
+ if errors.Is(err, ErrNotFound) {
+ // Not cached, get it and write it.
+ l, err := i.Image.LayerByDiffID(h)
+ if err != nil {
+ return nil, err
+ }
+ return i.c.Put(l)
+ }
+ return l, err
+}
+
+// ImageIndex returns a new ImageIndex which wraps the given ImageIndex's
+// children with either Image(child, c) or ImageIndex(child, c) depending on type.
+func ImageIndex(ii v1.ImageIndex, c Cache) v1.ImageIndex {
+ return &imageIndex{
+ inner: ii,
+ c: c,
+ }
+}
+
+type imageIndex struct {
+ inner v1.ImageIndex
+ c Cache
+}
+
+func (ii *imageIndex) MediaType() (types.MediaType, error) { return ii.inner.MediaType() }
+func (ii *imageIndex) Digest() (v1.Hash, error) { return ii.inner.Digest() }
+func (ii *imageIndex) Size() (int64, error) { return ii.inner.Size() }
+func (ii *imageIndex) IndexManifest() (*v1.IndexManifest, error) { return ii.inner.IndexManifest() }
+func (ii *imageIndex) RawManifest() ([]byte, error) { return ii.inner.RawManifest() }
+
+func (ii *imageIndex) Image(h v1.Hash) (v1.Image, error) {
+ i, err := ii.inner.Image(h)
+ if err != nil {
+ return nil, err
+ }
+ return Image(i, ii.c), nil
+}
+
+func (ii *imageIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
+ idx, err := ii.inner.ImageIndex(h)
+ if err != nil {
+ return nil, err
+ }
+ return ImageIndex(idx, ii.c), nil
+}
diff --git a/pkg/v1/cache/cache_test.go b/pkg/v1/cache/cache_test.go
new file mode 100644
index 0000000..ee5091b
--- /dev/null
+++ b/pkg/v1/cache/cache_test.go
@@ -0,0 +1,154 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+ "errors"
+ "io"
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestImage(t *testing.T) {
+ img, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatalf("random.Image: %v", err)
+ }
+ m := &memcache{map[v1.Hash]v1.Layer{}}
+ img = Image(img, m)
+
+ // Validate twice to hit the cache.
+ if err := validate.Image(img); err != nil {
+ t.Errorf("Validate: %v", err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Errorf("Validate: %v", err)
+ }
+}
+
+func TestImageIndex(t *testing.T) {
+ // ImageIndex with child Image and ImageIndex manifests.
+ ii, err := random.Index(1024, 5, 2)
+ if err != nil {
+ t.Fatalf("random.Index: %v", err)
+ }
+ iiChild, err := random.Index(1024, 5, 2)
+ if err != nil {
+ t.Fatalf("random.Index: %v", err)
+ }
+ ii = mutate.AppendManifests(ii, mutate.IndexAddendum{Add: iiChild})
+
+ m := &memcache{map[v1.Hash]v1.Layer{}}
+ ii = ImageIndex(ii, m)
+
+ // Validate twice to hit the cache.
+ if err := validate.Index(ii); err != nil {
+ t.Errorf("Validate: %v", err)
+ }
+ if err := validate.Index(ii); err != nil {
+ t.Errorf("Validate: %v", err)
+ }
+}
+
+func TestLayersLazy(t *testing.T) {
+ img, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatalf("random.Image: %v", err)
+ }
+ m := &memcache{map[v1.Hash]v1.Layer{}}
+ img = Image(img, m)
+
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatalf("img.Layers: %v", err)
+ }
+
+ // After calling Layers, nothing is cached.
+ if got, want := len(m.m), 0; got != want {
+ t.Errorf("Cache has %d entries, want %d", got, want)
+ }
+
+ rc, err := layers[0].Uncompressed()
+ if err != nil {
+ t.Fatalf("layer.Uncompressed: %v", err)
+ }
+ io.Copy(io.Discard, rc)
+
+ if got, expected := len(m.m), 1; got != expected {
+ t.Errorf("expected %v layers in cache after reading, got %v", expected, got)
+ }
+}
+
+// TestCacheShortCircuit tests that if a layer is found in the cache,
+// LayerByDigest is not called in the underlying Image implementation.
+func TestCacheShortCircuit(t *testing.T) {
+ l := &fakeLayer{}
+ m := &memcache{map[v1.Hash]v1.Layer{
+ fakeHash: l,
+ }}
+ img := Image(&fakeImage{}, m)
+
+ for i := 0; i < 10; i++ {
+ if _, err := img.LayerByDigest(fakeHash); err != nil {
+ t.Errorf("LayerByDigest[%d]: %v", i, err)
+ }
+ }
+}
+
+var fakeHash = v1.Hash{Algorithm: "fake", Hex: "data"}
+
+type fakeLayer struct{ v1.Layer }
+type fakeImage struct{ v1.Image }
+
+func (f *fakeImage) LayerByDigest(v1.Hash) (v1.Layer, error) {
+ return nil, errors.New("LayerByDigest was called")
+}
+
+// memcache is an in-memory Cache implementation.
+//
+// It doesn't intend to actually write layer data, it just keeps a reference
+// to the original Layer.
+//
+// It only assumes/considers compressed layers, and so only writes layers by
+// digest.
+type memcache struct {
+ m map[v1.Hash]v1.Layer
+}
+
+func (m *memcache) Put(l v1.Layer) (v1.Layer, error) {
+ digest, err := l.Digest()
+ if err != nil {
+ return nil, err
+ }
+ m.m[digest] = l
+ return l, nil
+}
+
+func (m *memcache) Get(h v1.Hash) (v1.Layer, error) {
+ l, found := m.m[h]
+ if !found {
+ return nil, ErrNotFound
+ }
+ return l, nil
+}
+
+func (m *memcache) Delete(h v1.Hash) error {
+ delete(m.m, h)
+ return nil
+}
diff --git a/pkg/v1/cache/example_test.go b/pkg/v1/cache/example_test.go
new file mode 100644
index 0000000..7f20474
--- /dev/null
+++ b/pkg/v1/cache/example_test.go
@@ -0,0 +1,46 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache_test
+
+import (
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/google/go-containerregistry/pkg/v1/cache"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+)
+
+func ExampleImage() {
+ img, err := random.Image(1024*1024, 3)
+ if err != nil {
+ log.Fatal(err)
+ }
+ dir, err := os.MkdirTemp("", "")
+ if err != nil {
+ log.Fatal(err)
+ }
+ fs := cache.NewFilesystemCache(dir)
+
+ // cached will cache layers from img using the fs cache
+ cached := cache.Image(img, fs)
+
+ // Use cached as you would use img.
+ digest, err := cached.Digest()
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Println(digest)
+}
diff --git a/pkg/v1/cache/fs.go b/pkg/v1/cache/fs.go
new file mode 100644
index 0000000..75b826e
--- /dev/null
+++ b/pkg/v1/cache/fs.go
@@ -0,0 +1,151 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+type fscache struct {
+ path string
+}
+
+// NewFilesystemCache returns a Cache implementation backed by files.
+func NewFilesystemCache(path string) Cache {
+ return &fscache{path}
+}
+
+func (fs *fscache) Put(l v1.Layer) (v1.Layer, error) {
+ digest, err := l.Digest()
+ if err != nil {
+ return nil, err
+ }
+ diffID, err := l.DiffID()
+ if err != nil {
+ return nil, err
+ }
+ return &layer{
+ Layer: l,
+ path: fs.path,
+ digest: digest,
+ diffID: diffID,
+ }, nil
+}
+
+type layer struct {
+ v1.Layer
+ path string
+ digest, diffID v1.Hash
+}
+
+func (l *layer) create(h v1.Hash) (io.WriteCloser, error) {
+ if err := os.MkdirAll(l.path, 0700); err != nil {
+ return nil, err
+ }
+ return os.Create(cachepath(l.path, h))
+}
+
+func (l *layer) Compressed() (io.ReadCloser, error) {
+ f, err := l.create(l.digest)
+ if err != nil {
+ return nil, err
+ }
+ rc, err := l.Layer.Compressed()
+ if err != nil {
+ return nil, err
+ }
+ return &readcloser{
+ t: io.TeeReader(rc, f),
+ closes: []func() error{rc.Close, f.Close},
+ }, nil
+}
+
+func (l *layer) Uncompressed() (io.ReadCloser, error) {
+ f, err := l.create(l.diffID)
+ if err != nil {
+ return nil, err
+ }
+ rc, err := l.Layer.Uncompressed()
+ if err != nil {
+ return nil, err
+ }
+ return &readcloser{
+ t: io.TeeReader(rc, f),
+ closes: []func() error{rc.Close, f.Close},
+ }, nil
+}
+
+type readcloser struct {
+ t io.Reader
+ closes []func() error
+}
+
+func (rc *readcloser) Read(b []byte) (int, error) {
+ return rc.t.Read(b)
+}
+
+func (rc *readcloser) Close() error {
+ // Call all Close methods, even if any returned an error. Return the
+ // first returned error.
+ var err error
+ for _, c := range rc.closes {
+ lastErr := c()
+ if err == nil {
+ err = lastErr
+ }
+ }
+ return err
+}
+
+func (fs *fscache) Get(h v1.Hash) (v1.Layer, error) {
+ l, err := tarball.LayerFromFile(cachepath(fs.path, h))
+ if os.IsNotExist(err) {
+ return nil, ErrNotFound
+ }
+ if errors.Is(err, io.ErrUnexpectedEOF) {
+ // Delete and return ErrNotFound because the layer was incomplete.
+ if err := fs.Delete(h); err != nil {
+ return nil, err
+ }
+ return nil, ErrNotFound
+ }
+ return l, err
+}
+
+func (fs *fscache) Delete(h v1.Hash) error {
+ err := os.Remove(cachepath(fs.path, h))
+ if os.IsNotExist(err) {
+ return ErrNotFound
+ }
+ return err
+}
+
+func cachepath(path string, h v1.Hash) string {
+ var file string
+ if runtime.GOOS == "windows" {
+ file = fmt.Sprintf("%s-%s", h.Algorithm, h.Hex)
+ } else {
+ file = h.String()
+ }
+ return filepath.Join(path, file)
+}
diff --git a/pkg/v1/cache/fs_test.go b/pkg/v1/cache/fs_test.go
new file mode 100644
index 0000000..2e05d29
--- /dev/null
+++ b/pkg/v1/cache/fs_test.go
@@ -0,0 +1,213 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+ "errors"
+ "io"
+ "os"
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestFilesystemCache(t *testing.T) {
+ dir := t.TempDir()
+
+ numLayers := 5
+ img, err := random.Image(10, int64(numLayers))
+ if err != nil {
+ t.Fatalf("random.Image: %v", err)
+ }
+ c := NewFilesystemCache(dir)
+ img = Image(img, c)
+
+ // Read all the (compressed) layers to populate the cache.
+ ls, err := img.Layers()
+ if err != nil {
+ t.Fatalf("Layers: %v", err)
+ }
+ for i, l := range ls {
+ rc, err := l.Compressed()
+ if err != nil {
+ t.Fatalf("layer[%d].Compressed: %v", i, err)
+ }
+ if _, err := io.Copy(io.Discard, rc); err != nil {
+ t.Fatalf("Error reading contents: %v", err)
+ }
+ rc.Close()
+ }
+
+ // Check that layers exist in the fs cache.
+ dirEntries, err := os.ReadDir(dir)
+ if err != nil {
+ t.Fatalf("ReadDir: %v", err)
+ }
+ if got, want := len(dirEntries), numLayers; got != want {
+ t.Errorf("Got %d cached files, want %d", got, want)
+ }
+ for _, de := range dirEntries {
+ fi, err := de.Info()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fi.Size() == 0 {
+ t.Errorf("Cached file %q is empty", fi.Name())
+ }
+ }
+
+ // Read all (uncompressed) layers, those populate the cache too.
+ for i, l := range ls {
+ rc, err := l.Uncompressed()
+ if err != nil {
+ t.Fatalf("layer[%d].Compressed: %v", i, err)
+ }
+ if _, err := io.Copy(io.Discard, rc); err != nil {
+ t.Fatalf("Error reading contents: %v", err)
+ }
+ rc.Close()
+ }
+
+ // Check that double the layers are present now, both compressed and
+ // uncompressed.
+ dirEntries, err = os.ReadDir(dir)
+ if err != nil {
+ t.Fatalf("ReadDir: %v", err)
+ }
+ if got, want := len(dirEntries), numLayers*2; got != want {
+ t.Errorf("Got %d cached files, want %d", got, want)
+ }
+ for _, de := range dirEntries {
+ fi, err := de.Info()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fi.Size() == 0 {
+ t.Errorf("Cached file %q is empty", fi.Name())
+ }
+ }
+
+ // Delete a cached layer, see it disappear.
+ l := ls[0]
+ h, err := l.Digest()
+ if err != nil {
+ t.Fatalf("layer.Digest: %v", err)
+ }
+ if err := c.Delete(h); err != nil {
+ t.Errorf("cache.Delete: %v", err)
+ }
+ dirEntries, err = os.ReadDir(dir)
+ if err != nil {
+ t.Fatalf("ReadDir: %v", err)
+ }
+ if got, want := len(dirEntries), numLayers*2-1; got != want {
+ t.Errorf("Got %d cached files, want %d", got, want)
+ }
+
+ // Read the image again, see the layer reappear.
+ for i, l := range ls {
+ rc, err := l.Compressed()
+ if err != nil {
+ t.Fatalf("layer[%d].Compressed: %v", i, err)
+ }
+ if _, err := io.Copy(io.Discard, rc); err != nil {
+ t.Fatalf("Error reading contents: %v", err)
+ }
+ rc.Close()
+ }
+
+ // Check that layers exist in the fs cache.
+ dirEntries, err = os.ReadDir(dir)
+ if err != nil {
+ t.Fatalf("ReadDir: %v", err)
+ }
+ if got, want := len(dirEntries), numLayers*2; got != want {
+ t.Errorf("Got %d cached files, want %d", got, want)
+ }
+ for _, de := range dirEntries {
+ fi, err := de.Info()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fi.Size() == 0 {
+ t.Errorf("Cached file %q is empty", fi.Name())
+ }
+ }
+}
+
+func TestErrNotFound(t *testing.T) {
+ dir := t.TempDir()
+
+ c := NewFilesystemCache(dir)
+ h := v1.Hash{Algorithm: "fake", Hex: "not-found"}
+ if _, err := c.Get(h); !errors.Is(err, ErrNotFound) {
+ t.Errorf("Get(%q): %v", h, err)
+ }
+ if err := c.Delete(h); !errors.Is(err, ErrNotFound) {
+ t.Errorf("Delete(%q): %v", h, err)
+ }
+}
+
+func TestErrUnexpectedEOF(t *testing.T) {
+ dir := t.TempDir()
+
+ // create a random layer
+ l, err := random.Layer(10, types.DockerLayer)
+ if err != nil {
+ t.Fatalf("random.Layer: %v", err)
+ }
+ rc, err := l.Compressed()
+ if err != nil {
+ t.Fatalf("layer.Compressed(): %v", err)
+ }
+
+ h, err := l.Digest()
+ if err != nil {
+ t.Fatalf("layer.Digest(): %v", err)
+ }
+ p := cachepath(dir, h)
+
+ // Write only the first segment of the compressed layer to produce an
+ // UnexpectedEOF error when reading it
+ buf := make([]byte, 10)
+ n, err := rc.Read(buf)
+ if err != nil {
+ t.Fatalf("Read(buf): %v", err)
+ }
+ if err := os.WriteFile(p, buf[:n], 0644); err != nil {
+ t.Fatalf("os.WriteFile(%s, buf[:%d]): %v", p, n, err)
+ }
+
+ c := NewFilesystemCache(dir)
+
+ // make sure LayerFromFile returns UnexpectedEOF
+ if _, err := tarball.LayerFromFile(p); !errors.Is(err, io.ErrUnexpectedEOF) {
+ t.Fatalf("tarball.LayerFromFile(%s): expected %v, got %v", p, io.ErrUnexpectedEOF, err)
+ }
+
+ // Try to Get the layer
+ if _, err := c.Get(h); !errors.Is(err, ErrNotFound) {
+ t.Errorf("Get(%q): %v", h, err)
+ }
+
+ // If we had an UnexpectedEOF and the cache deleted the broken layer no file
+ // should exist
+ if _, err := os.Stat(p); !os.IsNotExist(err) {
+ t.Errorf("os.Stat(%q): %v", p, err)
+ }
+}
diff --git a/pkg/v1/cache/ro.go b/pkg/v1/cache/ro.go
new file mode 100644
index 0000000..028a612
--- /dev/null
+++ b/pkg/v1/cache/ro.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import v1 "github.com/google/go-containerregistry/pkg/v1"
+
+// ReadOnly returns a read-only implementation of the given Cache.
+//
+// Put and Delete operations are a no-op.
+func ReadOnly(c Cache) Cache { return &ro{Cache: c} }
+
+type ro struct{ Cache }
+
+func (ro) Put(l v1.Layer) (v1.Layer, error) { return l, nil }
+func (ro) Delete(v1.Hash) error { return nil }
diff --git a/pkg/v1/cache/ro_test.go b/pkg/v1/cache/ro_test.go
new file mode 100644
index 0000000..4ec6b1c
--- /dev/null
+++ b/pkg/v1/cache/ro_test.go
@@ -0,0 +1,79 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+)
+
+func TestReadOnly(t *testing.T) {
+ m := &memcache{map[v1.Hash]v1.Layer{}}
+ ro := ReadOnly(m)
+
+ // Populate the cache.
+ img, err := random.Image(10, 1)
+ if err != nil {
+ t.Fatalf("random.Image: %v", err)
+ }
+ img = Image(img, m)
+ ls, err := img.Layers()
+ if err != nil {
+ t.Fatalf("Layers: %v", err)
+ }
+ if got, want := len(ls), 1; got != want {
+ t.Fatalf("Layers returned %d layers, want %d", got, want)
+ }
+ h, err := ls[0].Digest()
+ if err != nil {
+ t.Fatalf("layer.Digest: %v", err)
+ }
+ m.m[h] = ls[0]
+
+ // Layer can be read from original cache and RO cache.
+ if _, err := m.Get(h); err != nil {
+ t.Fatalf("m.Get: %v", err)
+ }
+ if _, err := ro.Get(h); err != nil {
+ t.Fatalf("ro.Get: %v", err)
+ }
+ ln := len(m.m)
+
+ // RO Put is a no-op.
+ if _, err := ro.Put(ls[0]); err != nil {
+ t.Fatalf("ro.Put: %v", err)
+ }
+ if got, want := len(m.m), ln; got != want {
+ t.Errorf("After Put, got %v entries, want %v", got, want)
+ }
+
+ // RO Delete is a no-op.
+ if err := ro.Delete(h); err != nil {
+ t.Fatalf("ro.Delete: %v", err)
+ }
+ if got, want := len(m.m), ln; got != want {
+ t.Errorf("After Delete, got %v entries, want %v", got, want)
+ }
+
+ // Deleting from the underlying RW cache updates RO view.
+ if err := m.Delete(h); err != nil {
+ t.Fatalf("m.Delete: %v", err)
+ }
+ if got, want := len(m.m), 0; got != want {
+ t.Errorf("After RW Delete, got %v entries, want %v", got, want)
+ }
+}
diff --git a/pkg/v1/config.go b/pkg/v1/config.go
new file mode 100644
index 0000000..960c93b
--- /dev/null
+++ b/pkg/v1/config.go
@@ -0,0 +1,151 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "encoding/json"
+ "io"
+ "time"
+)
+
+// ConfigFile is the configuration file that holds the metadata describing
+// how to launch a container. See:
+// https://github.com/opencontainers/image-spec/blob/master/config.md
+//
+// docker_version and os.version are not part of the spec but included
+// for backwards compatibility.
+type ConfigFile struct {
+ Architecture string `json:"architecture"`
+ Author string `json:"author,omitempty"`
+ Container string `json:"container,omitempty"`
+ Created Time `json:"created,omitempty"`
+ DockerVersion string `json:"docker_version,omitempty"`
+ History []History `json:"history,omitempty"`
+ OS string `json:"os"`
+ RootFS RootFS `json:"rootfs"`
+ Config Config `json:"config"`
+ OSVersion string `json:"os.version,omitempty"`
+ Variant string `json:"variant,omitempty"`
+ OSFeatures []string `json:"os.features,omitempty"`
+}
+
+// Platform attempts to generates a Platform from the ConfigFile fields.
+func (cf *ConfigFile) Platform() *Platform {
+ if cf.OS == "" && cf.Architecture == "" && cf.OSVersion == "" && cf.Variant == "" && len(cf.OSFeatures) == 0 {
+ return nil
+ }
+ return &Platform{
+ OS: cf.OS,
+ Architecture: cf.Architecture,
+ OSVersion: cf.OSVersion,
+ Variant: cf.Variant,
+ OSFeatures: cf.OSFeatures,
+ }
+}
+
+// History is one entry of a list recording how this container image was built.
+type History struct {
+ Author string `json:"author,omitempty"`
+ Created Time `json:"created,omitempty"`
+ CreatedBy string `json:"created_by,omitempty"`
+ Comment string `json:"comment,omitempty"`
+ EmptyLayer bool `json:"empty_layer,omitempty"`
+}
+
+// Time is a wrapper around time.Time to help with deep copying
+type Time struct {
+ time.Time
+}
+
+// DeepCopyInto creates a deep-copy of the Time value. The underlying time.Time
+// type is effectively immutable in the time API, so it is safe to
+// copy-by-assign, despite the presence of (unexported) Pointer fields.
+func (t *Time) DeepCopyInto(out *Time) {
+ *out = *t
+}
+
+// RootFS holds the ordered list of file system deltas that comprise the
+// container image's root filesystem.
+type RootFS struct {
+ Type string `json:"type"`
+ DiffIDs []Hash `json:"diff_ids"`
+}
+
+// HealthConfig holds configuration settings for the HEALTHCHECK feature.
+type HealthConfig struct {
+ // Test is the test to perform to check that the container is healthy.
+ // An empty slice means to inherit the default.
+ // The options are:
+ // {} : inherit healthcheck
+ // {"NONE"} : disable healthcheck
+ // {"CMD", args...} : exec arguments directly
+ // {"CMD-SHELL", command} : run command with system's default shell
+ Test []string `json:",omitempty"`
+
+ // Zero means to inherit. Durations are expressed as integer nanoseconds.
+ Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks.
+ Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung.
+ StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down.
+
+ // Retries is the number of consecutive failures needed to consider a container as unhealthy.
+ // Zero means inherit.
+ Retries int `json:",omitempty"`
+}
+
+// Config is a submessage of the config file described as:
+//
+// The execution parameters which SHOULD be used as a base when running
+// a container using the image.
+//
+// The names of the fields in this message are chosen to reflect the JSON
+// payload of the Config as defined here:
+// https://git.io/vrAET
+// and
+// https://github.com/opencontainers/image-spec/blob/master/config.md
+type Config struct {
+ AttachStderr bool `json:"AttachStderr,omitempty"`
+ AttachStdin bool `json:"AttachStdin,omitempty"`
+ AttachStdout bool `json:"AttachStdout,omitempty"`
+ Cmd []string `json:"Cmd,omitempty"`
+ Healthcheck *HealthConfig `json:"Healthcheck,omitempty"`
+ Domainname string `json:"Domainname,omitempty"`
+ Entrypoint []string `json:"Entrypoint,omitempty"`
+ Env []string `json:"Env,omitempty"`
+ Hostname string `json:"Hostname,omitempty"`
+ Image string `json:"Image,omitempty"`
+ Labels map[string]string `json:"Labels,omitempty"`
+ OnBuild []string `json:"OnBuild,omitempty"`
+ OpenStdin bool `json:"OpenStdin,omitempty"`
+ StdinOnce bool `json:"StdinOnce,omitempty"`
+ Tty bool `json:"Tty,omitempty"`
+ User string `json:"User,omitempty"`
+ Volumes map[string]struct{} `json:"Volumes,omitempty"`
+ WorkingDir string `json:"WorkingDir,omitempty"`
+ ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"`
+ ArgsEscaped bool `json:"ArgsEscaped,omitempty"`
+ NetworkDisabled bool `json:"NetworkDisabled,omitempty"`
+ MacAddress string `json:"MacAddress,omitempty"`
+ StopSignal string `json:"StopSignal,omitempty"`
+ Shell []string `json:"Shell,omitempty"`
+}
+
+// ParseConfigFile parses the io.Reader's contents into a ConfigFile.
+func ParseConfigFile(r io.Reader) (*ConfigFile, error) {
+ cf := ConfigFile{}
+ if err := json.NewDecoder(r).Decode(&cf); err != nil {
+ return nil, err
+ }
+ return &cf, nil
+}
diff --git a/pkg/v1/config_test.go b/pkg/v1/config_test.go
new file mode 100644
index 0000000..6e190bf
--- /dev/null
+++ b/pkg/v1/config_test.go
@@ -0,0 +1,38 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestParseConfig(t *testing.T) {
+ got, err := ParseConfigFile(strings.NewReader("{}"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := ConfigFile{}
+
+ if diff := cmp.Diff(want, *got); diff != "" {
+ t.Errorf("ParseConfigFile({}); (-want +got) %s", diff)
+ }
+
+ if got, err := ParseConfigFile(strings.NewReader("{")); err == nil {
+ t.Errorf("expected error, got: %v", got)
+ }
+}
diff --git a/pkg/v1/daemon/README.md b/pkg/v1/daemon/README.md
new file mode 100644
index 0000000..74fc3a8
--- /dev/null
+++ b/pkg/v1/daemon/README.md
@@ -0,0 +1,11 @@
+# `daemon`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon)
+
+The `daemon` package enables reading/writing images from/to the docker daemon.
+
+It is not fully fleshed out, but is useful for interoperability, see various issues:
+
+* https://github.com/google/go-containerregistry/issues/205
+* https://github.com/google/go-containerregistry/issues/552
+* https://github.com/google/go-containerregistry/issues/627
diff --git a/pkg/v1/daemon/doc.go b/pkg/v1/daemon/doc.go
new file mode 100644
index 0000000..ac05d96
--- /dev/null
+++ b/pkg/v1/daemon/doc.go
@@ -0,0 +1,17 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package daemon provides facilities for reading/writing v1.Image from/to
+// a running daemon.
+package daemon
diff --git a/pkg/v1/daemon/image.go b/pkg/v1/daemon/image.go
new file mode 100644
index 0000000..55ba833
--- /dev/null
+++ b/pkg/v1/daemon/image.go
@@ -0,0 +1,203 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package daemon
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "sync"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type image struct {
+ ref name.Reference
+ opener *imageOpener
+ tarballImage v1.Image
+ id *v1.Hash
+
+ once sync.Once
+ err error
+}
+
+type imageOpener struct {
+ ref name.Reference
+ ctx context.Context
+
+ buffered bool
+ client Client
+
+ once sync.Once
+ bytes []byte
+ err error
+}
+
+func (i *imageOpener) saveImage() (io.ReadCloser, error) {
+ return i.client.ImageSave(i.ctx, []string{i.ref.Name()})
+}
+
+func (i *imageOpener) bufferedOpener() (io.ReadCloser, error) {
+ // Store the tarball in memory and return a new reader into the bytes each time we need to access something.
+ i.once.Do(func() {
+ i.bytes, i.err = func() ([]byte, error) {
+ rc, err := i.saveImage()
+ if err != nil {
+ return nil, err
+ }
+ defer rc.Close()
+
+ return io.ReadAll(rc)
+ }()
+ })
+
+ // Wrap the bytes in a ReadCloser so it looks like an opened file.
+ return io.NopCloser(bytes.NewReader(i.bytes)), i.err
+}
+
+func (i *imageOpener) opener() tarball.Opener {
+ if i.buffered {
+ return i.bufferedOpener
+ }
+
+ // To avoid storing the tarball in memory, do a save every time we need to access something.
+ return i.saveImage
+}
+
+// Image provides access to an image reference from the Docker daemon,
+// applying functional options to the underlying imageOpener before
+// resolving the reference into a v1.Image.
+func Image(ref name.Reference, options ...Option) (v1.Image, error) {
+ o, err := makeOptions(options...)
+ if err != nil {
+ return nil, err
+ }
+
+ i := &imageOpener{
+ ref: ref,
+ buffered: o.buffered,
+ client: o.client,
+ ctx: o.ctx,
+ }
+
+ img := &image{
+ ref: ref,
+ opener: i,
+ }
+
+ // Eagerly fetch Image ID to ensure it actually exists.
+ // https://github.com/google/go-containerregistry/issues/1186
+ id, err := img.ConfigName()
+ if err != nil {
+ return nil, err
+ }
+ img.id = &id
+
+ return img, nil
+}
+
+func (i *image) initialize() error {
+ // Don't re-initialize tarball if already initialized.
+ if i.tarballImage == nil {
+ i.once.Do(func() {
+ i.tarballImage, i.err = tarball.Image(i.opener.opener(), nil)
+ })
+ }
+ return i.err
+}
+
+func (i *image) Layers() ([]v1.Layer, error) {
+ if err := i.initialize(); err != nil {
+ return nil, err
+ }
+ return i.tarballImage.Layers()
+}
+
+func (i *image) MediaType() (types.MediaType, error) {
+ if err := i.initialize(); err != nil {
+ return "", err
+ }
+ return i.tarballImage.MediaType()
+}
+
+func (i *image) Size() (int64, error) {
+ if err := i.initialize(); err != nil {
+ return 0, err
+ }
+ return i.tarballImage.Size()
+}
+
+func (i *image) ConfigName() (v1.Hash, error) {
+ if i.id != nil {
+ return *i.id, nil
+ }
+ res, _, err := i.opener.client.ImageInspectWithRaw(i.opener.ctx, i.ref.String())
+ if err != nil {
+ return v1.Hash{}, err
+ }
+ return v1.NewHash(res.ID)
+}
+
+func (i *image) ConfigFile() (*v1.ConfigFile, error) {
+ if err := i.initialize(); err != nil {
+ return nil, err
+ }
+ return i.tarballImage.ConfigFile()
+}
+
+func (i *image) RawConfigFile() ([]byte, error) {
+ if err := i.initialize(); err != nil {
+ return nil, err
+ }
+ return i.tarballImage.RawConfigFile()
+}
+
+func (i *image) Digest() (v1.Hash, error) {
+ if err := i.initialize(); err != nil {
+ return v1.Hash{}, err
+ }
+ return i.tarballImage.Digest()
+}
+
+func (i *image) Manifest() (*v1.Manifest, error) {
+ if err := i.initialize(); err != nil {
+ return nil, err
+ }
+ return i.tarballImage.Manifest()
+}
+
+func (i *image) RawManifest() ([]byte, error) {
+ if err := i.initialize(); err != nil {
+ return nil, err
+ }
+ return i.tarballImage.RawManifest()
+}
+
+func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) {
+ if err := i.initialize(); err != nil {
+ return nil, err
+ }
+ return i.tarballImage.LayerByDigest(h)
+}
+
+func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
+ if err := i.initialize(); err != nil {
+ return nil, err
+ }
+ return i.tarballImage.LayerByDiffID(h)
+}
diff --git a/pkg/v1/daemon/image_test.go b/pkg/v1/daemon/image_test.go
new file mode 100644
index 0000000..6456832
--- /dev/null
+++ b/pkg/v1/daemon/image_test.go
@@ -0,0 +1,159 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package daemon
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/docker/docker/api/types"
+ "github.com/google/go-containerregistry/internal/compare"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+var imagePath = "../tarball/testdata/test_image_1.tar"
+
+type MockClient struct {
+ Client
+ path string
+ negotiated bool
+
+ wantCtx context.Context
+
+ loadErr error
+ loadBody io.ReadCloser
+
+ saveErr error
+ saveBody io.ReadCloser
+}
+
+func (m *MockClient) NegotiateAPIVersion(ctx context.Context) {
+ m.negotiated = true
+}
+
+func (m *MockClient) ImageSave(_ context.Context, _ []string) (io.ReadCloser, error) {
+ if !m.negotiated {
+ return nil, errors.New("you forgot to call NegotiateAPIVersion before calling ImageSave")
+ }
+
+ if m.path != "" {
+ return os.Open(m.path)
+ }
+
+ return m.saveBody, m.saveErr
+}
+
+func (m *MockClient) ImageInspectWithRaw(context.Context, string) (types.ImageInspect, []byte, error) {
+ return types.ImageInspect{
+ ID: "sha256:6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e",
+ }, nil, nil
+}
+
+func TestImage(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ buffered bool
+ client *MockClient
+ wantResponse string
+ wantErr string
+ }{{
+ name: "success",
+ client: &MockClient{
+ path: imagePath,
+ },
+ }, {
+ name: "save err",
+ client: &MockClient{
+ saveBody: io.NopCloser(strings.NewReader("Loaded")),
+ saveErr: fmt.Errorf("locked and loaded"),
+ },
+ wantErr: "locked and loaded",
+ }, {
+ name: "read err",
+ client: &MockClient{
+ saveBody: io.NopCloser(&errReader{fmt.Errorf("goodbye, world")}),
+ },
+ wantErr: "goodbye, world",
+ }} {
+ run := func(t *testing.T) {
+ opts := []Option{WithClient(tc.client)}
+ if tc.buffered {
+ opts = append(opts, WithBufferedOpener())
+ } else {
+ opts = append(opts, WithUnbufferedOpener())
+ }
+ img, err := tarball.ImageFromPath(imagePath, nil)
+ if err != nil {
+ t.Fatalf("error loading test image: %s", err)
+ }
+
+ tag, err := name.NewTag("unused", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("error creating test name: %s", err)
+ }
+
+ dmn, err := Image(tag, opts...)
+ if err != nil {
+ if tc.wantErr == "" {
+ t.Errorf("Error loading daemon image: %s", err)
+ } else if !strings.Contains(err.Error(), tc.wantErr) {
+ t.Errorf("wanted %s to contain %s", err.Error(), tc.wantErr)
+ }
+ return
+ }
+ err = compare.Images(img, dmn)
+ if err != nil {
+ if tc.wantErr == "" {
+ t.Errorf("compare.Images: %v", err)
+ } else if !strings.Contains(err.Error(), tc.wantErr) {
+ t.Errorf("wanted %s to contain %s", err.Error(), tc.wantErr)
+ }
+ }
+
+ err = validate.Image(dmn)
+ if err != nil {
+ if tc.wantErr == "" {
+ t.Errorf("validate.Image: %v", err)
+ } else if !strings.Contains(err.Error(), tc.wantErr) {
+ t.Errorf("wanted %s to contain %s", err.Error(), tc.wantErr)
+ }
+ }
+ }
+
+ tc.buffered = true
+ t.Run(tc.name+" buffered", run)
+
+ tc.buffered = false
+ t.Run(tc.name+" unbuffered", run)
+ }
+}
+
+func TestImageDefaultClient(t *testing.T) {
+ wantErr := fmt.Errorf("bad client")
+ defaultClient = func() (Client, error) {
+ return nil, wantErr
+ }
+
+ if _, err := Image(name.MustParseReference("unused")); !errors.Is(err, wantErr) {
+ t.Errorf("Image(): want %v; got %v", wantErr, err)
+ }
+}
diff --git a/pkg/v1/daemon/options.go b/pkg/v1/daemon/options.go
new file mode 100644
index 0000000..e8a5a1e
--- /dev/null
+++ b/pkg/v1/daemon/options.go
@@ -0,0 +1,103 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package daemon
+
+import (
+ "context"
+ "io"
+
+ "github.com/docker/docker/api/types"
+ "github.com/docker/docker/client"
+)
+
+// ImageOption is an alias for Option.
+// Deprecated: Use Option instead.
+type ImageOption Option
+
+// Option is a functional option for daemon operations.
+type Option func(*options)
+
+type options struct {
+ ctx context.Context
+ client Client
+ buffered bool
+}
+
+var defaultClient = func() (Client, error) {
+ return client.NewClientWithOpts(client.FromEnv)
+}
+
+func makeOptions(opts ...Option) (*options, error) {
+ o := &options{
+ buffered: true,
+ ctx: context.Background(),
+ }
+ for _, opt := range opts {
+ opt(o)
+ }
+
+ if o.client == nil {
+ client, err := defaultClient()
+ if err != nil {
+ return nil, err
+ }
+ o.client = client
+ }
+ o.client.NegotiateAPIVersion(o.ctx)
+
+ return o, nil
+}
+
+// WithBufferedOpener buffers the image.
+func WithBufferedOpener() Option {
+ return func(o *options) {
+ o.buffered = true
+ }
+}
+
+// WithUnbufferedOpener streams the image to avoid buffering.
+func WithUnbufferedOpener() Option {
+ return func(o *options) {
+ o.buffered = false
+ }
+}
+
+// WithClient is a functional option to allow injecting a docker client.
+//
+// By default, github.com/docker/docker/client.FromEnv is used.
+func WithClient(client Client) Option {
+ return func(o *options) {
+ o.client = client
+ }
+}
+
+// WithContext is a functional option to pass through a context.Context.
+//
+// By default, context.Background() is used.
+func WithContext(ctx context.Context) Option {
+ return func(o *options) {
+ o.ctx = ctx
+ }
+}
+
+// Client represents the subset of a docker client that the daemon
+// package uses.
+type Client interface {
+ NegotiateAPIVersion(ctx context.Context)
+ ImageSave(context.Context, []string) (io.ReadCloser, error)
+ ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error)
+ ImageTag(context.Context, string, string) error
+ ImageInspectWithRaw(context.Context, string) (types.ImageInspect, []byte, error)
+}
diff --git a/pkg/v1/daemon/write.go b/pkg/v1/daemon/write.go
new file mode 100644
index 0000000..48186f6
--- /dev/null
+++ b/pkg/v1/daemon/write.go
@@ -0,0 +1,60 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package daemon
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+// Tag adds a tag to an already existent image.
+func Tag(src, dest name.Tag, options ...Option) error {
+ o, err := makeOptions(options...)
+ if err != nil {
+ return err
+ }
+
+ return o.client.ImageTag(o.ctx, src.String(), dest.String())
+}
+
+// Write saves the image into the daemon as the given tag.
+func Write(tag name.Tag, img v1.Image, options ...Option) (string, error) {
+ o, err := makeOptions(options...)
+ if err != nil {
+ return "", err
+ }
+
+ pr, pw := io.Pipe()
+ go func() {
+ pw.CloseWithError(tarball.Write(tag, img, pw))
+ }()
+
+ // write the image in docker save format first, then load it
+ resp, err := o.client.ImageLoad(o.ctx, pr, false)
+ if err != nil {
+ return "", fmt.Errorf("error loading image: %w", err)
+ }
+ defer resp.Body.Close()
+ b, err := io.ReadAll(resp.Body)
+ response := string(b)
+ if err != nil {
+ return response, fmt.Errorf("error reading load response body: %w", err)
+ }
+ return response, nil
+}
diff --git a/pkg/v1/daemon/write_test.go b/pkg/v1/daemon/write_test.go
new file mode 100644
index 0000000..0e5495c
--- /dev/null
+++ b/pkg/v1/daemon/write_test.go
@@ -0,0 +1,159 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package daemon
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/docker/docker/api/types"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+type errReader struct {
+ err error
+}
+
+func (r *errReader) Read(p []byte) (int, error) {
+ return 0, r.err
+}
+
+func (m *MockClient) ImageLoad(ctx context.Context, r io.Reader, _ bool) (types.ImageLoadResponse, error) {
+ if !m.negotiated {
+ return types.ImageLoadResponse{}, errors.New("you forgot to call NegotiateAPIVersion before calling ImageLoad")
+ }
+ if m.wantCtx != nil && m.wantCtx != ctx {
+ return types.ImageLoadResponse{}, fmt.Errorf("ImageLoad: wrong context")
+ }
+
+ _, _ = io.Copy(io.Discard, r)
+ return types.ImageLoadResponse{
+ Body: m.loadBody,
+ }, m.loadErr
+}
+
+func (m *MockClient) ImageTag(ctx context.Context, source, target string) error {
+ if !m.negotiated {
+ return errors.New("you forgot to call NegotiateAPIVersion before calling ImageTag")
+ }
+ if m.wantCtx != nil && m.wantCtx != ctx {
+ return fmt.Errorf("ImageTag: wrong context")
+ }
+ return nil
+}
+
+func TestWriteImage(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ client *MockClient
+ wantResponse string
+ wantErr string
+ }{{
+ name: "success",
+ client: &MockClient{
+ loadBody: io.NopCloser(strings.NewReader("Loaded")),
+ },
+ wantResponse: "Loaded",
+ }, {
+ name: "load err",
+ client: &MockClient{
+ loadBody: io.NopCloser(strings.NewReader("Loaded")),
+ loadErr: fmt.Errorf("locked and loaded"),
+ },
+ wantErr: "locked and loaded",
+ }, {
+ name: "read err",
+ client: &MockClient{
+ loadBody: io.NopCloser(&errReader{fmt.Errorf("goodbye, world")}),
+ },
+ wantErr: "goodbye, world",
+ }} {
+ t.Run(tc.name, func(t *testing.T) {
+ image, err := tarball.ImageFromPath("../tarball/testdata/test_image_1.tar", nil)
+ if err != nil {
+ t.Errorf("Error loading image: %v", err.Error())
+ }
+ tag, err := name.NewTag("test_image_2:latest")
+ if err != nil {
+ t.Fatal(err)
+ }
+ response, err := Write(tag, image, WithClient(tc.client))
+ if tc.wantErr == "" {
+ if err != nil {
+ t.Errorf("Error writing image tar: %s", err.Error())
+ }
+ } else {
+ if err == nil {
+ t.Errorf("expected err")
+ } else if !strings.Contains(err.Error(), tc.wantErr) {
+ t.Errorf("Error writing image tar: wanted %s to contain %s", err.Error(), tc.wantErr)
+ }
+ }
+ if !strings.Contains(response, tc.wantResponse) {
+ t.Errorf("Error loading image. Response: %s", response)
+ }
+
+ dst, err := name.NewTag("hello:world")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := Tag(tag, dst, WithClient(tc.client)); err != nil {
+ t.Errorf("Error tagging image: %v", err)
+ }
+ })
+ }
+}
+
+func TestWriteDefaultClient(t *testing.T) {
+ wantErr := fmt.Errorf("bad client")
+ defaultClient = func() (Client, error) {
+ return nil, wantErr
+ }
+
+ tag, err := name.NewTag("test_image_2:latest")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := Write(tag, empty.Image); !errors.Is(err, wantErr) {
+ t.Errorf("Write(): want %v; got %v", wantErr, err)
+ }
+
+ if err := Tag(tag, tag); !errors.Is(err, wantErr) {
+ t.Errorf("Tag(): want %v; got %v", wantErr, err)
+ }
+
+ // Cover default client init and ctx use as well.
+ ctx := context.TODO()
+ defaultClient = func() (Client, error) {
+ return &MockClient{
+ loadBody: io.NopCloser(strings.NewReader("Loaded")),
+ wantCtx: ctx,
+ }, nil
+ }
+ if err := Tag(tag, tag, WithContext(ctx)); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := Write(tag, empty.Image, WithContext(ctx)); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/pkg/v1/doc.go b/pkg/v1/doc.go
new file mode 100644
index 0000000..7a84736
--- /dev/null
+++ b/pkg/v1/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +k8s:deepcopy-gen=package
+
+// Package v1 defines structured types for OCI v1 images
+package v1
diff --git a/pkg/v1/empty/README.md b/pkg/v1/empty/README.md
new file mode 100644
index 0000000..8663a83
--- /dev/null
+++ b/pkg/v1/empty/README.md
@@ -0,0 +1,8 @@
+# `empty`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/empty?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/empty)
+
+The empty packages provides an empty base for constructing a `v1.Image` or `v1.ImageIndex`.
+This is especially useful when paired with the [`mutate`](/pkg/v1/mutate) package,
+see [`mutate.Append`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate#Append)
+and [`mutate.AppendManifests`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate#AppendManifests).
diff --git a/pkg/v1/empty/doc.go b/pkg/v1/empty/doc.go
new file mode 100644
index 0000000..1a521e9
--- /dev/null
+++ b/pkg/v1/empty/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package empty provides an implementation of v1.Image equivalent to "FROM scratch".
+package empty
diff --git a/pkg/v1/empty/image.go b/pkg/v1/empty/image.go
new file mode 100644
index 0000000..c58a06c
--- /dev/null
+++ b/pkg/v1/empty/image.go
@@ -0,0 +1,52 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package empty
+
+import (
+ "fmt"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Image is a singleton empty image, think: FROM scratch.
+var Image, _ = partial.UncompressedToImage(emptyImage{})
+
+type emptyImage struct{}
+
+// MediaType implements partial.UncompressedImageCore.
+func (i emptyImage) MediaType() (types.MediaType, error) {
+ return types.DockerManifestSchema2, nil
+}
+
+// RawConfigFile implements partial.UncompressedImageCore.
+func (i emptyImage) RawConfigFile() ([]byte, error) {
+ return partial.RawConfigFile(i)
+}
+
+// ConfigFile implements v1.Image.
+func (i emptyImage) ConfigFile() (*v1.ConfigFile, error) {
+ return &v1.ConfigFile{
+ RootFS: v1.RootFS{
+ // Some clients check this.
+ Type: "layers",
+ },
+ }, nil
+}
+
+func (i emptyImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) {
+ return nil, fmt.Errorf("LayerByDiffID(%s): empty image", h)
+}
diff --git a/pkg/v1/empty/image_test.go b/pkg/v1/empty/image_test.go
new file mode 100644
index 0000000..c9204d9
--- /dev/null
+++ b/pkg/v1/empty/image_test.go
@@ -0,0 +1,48 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package empty
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestImage(t *testing.T) {
+ if err := validate.Image(Image); err != nil {
+ t.Fatalf("validate.Image(empty.Image) = %v", err)
+ }
+}
+
+func TestManifestAndConfig(t *testing.T) {
+ manifest, err := Image.Manifest()
+ if err != nil {
+ t.Fatalf("Error loading manifest: %v", err)
+ }
+ if got, want := len(manifest.Layers), 0; got != want {
+ t.Fatalf("num layers; got %v, want %v", got, want)
+ }
+
+ config, err := Image.ConfigFile()
+ if err != nil {
+ t.Fatalf("Error loading config file: %v", err)
+ }
+ if got, want := len(config.RootFS.DiffIDs), 0; got != want {
+ t.Fatalf("num diff ids; got %v, want %v", got, want)
+ }
+ if got, want := config.RootFS.Type, "layers"; got != want {
+ t.Fatalf("rootfs type; got %v, want %v", got, want)
+ }
+}
diff --git a/pkg/v1/empty/index.go b/pkg/v1/empty/index.go
new file mode 100644
index 0000000..1066535
--- /dev/null
+++ b/pkg/v1/empty/index.go
@@ -0,0 +1,64 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package empty
+
+import (
+ "encoding/json"
+ "errors"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Index is a singleton empty index, think: FROM scratch.
+var Index = emptyIndex{}
+
+type emptyIndex struct{}
+
+func (i emptyIndex) MediaType() (types.MediaType, error) {
+ return types.OCIImageIndex, nil
+}
+
+func (i emptyIndex) Digest() (v1.Hash, error) {
+ return partial.Digest(i)
+}
+
+func (i emptyIndex) Size() (int64, error) {
+ return partial.Size(i)
+}
+
+func (i emptyIndex) IndexManifest() (*v1.IndexManifest, error) {
+ return base(), nil
+}
+
+func (i emptyIndex) RawManifest() ([]byte, error) {
+ return json.Marshal(base())
+}
+
+func (i emptyIndex) Image(v1.Hash) (v1.Image, error) {
+ return nil, errors.New("empty index")
+}
+
+func (i emptyIndex) ImageIndex(v1.Hash) (v1.ImageIndex, error) {
+ return nil, errors.New("empty index")
+}
+
+func base() *v1.IndexManifest {
+ return &v1.IndexManifest{
+ SchemaVersion: 2,
+ MediaType: types.OCIImageIndex,
+ }
+}
diff --git a/pkg/v1/empty/index_test.go b/pkg/v1/empty/index_test.go
new file mode 100644
index 0000000..dd99e10
--- /dev/null
+++ b/pkg/v1/empty/index_test.go
@@ -0,0 +1,40 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package empty
+
+import (
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestIndex(t *testing.T) {
+ if err := validate.Index(Index); err != nil {
+ t.Fatalf("validate.Index(empty.Index) = %v", err)
+ }
+
+ if mt, err := Index.MediaType(); err != nil || mt != types.OCIImageIndex {
+ t.Errorf("empty.Index.MediaType() = %v, %v", mt, err)
+ }
+
+ if _, err := Index.Image(v1.Hash{}); err == nil {
+ t.Errorf("empty.Index.Image() should always fail")
+ }
+ if _, err := Index.ImageIndex(v1.Hash{}); err == nil {
+ t.Errorf("empty.Index.ImageIndex() should always fail")
+ }
+}
diff --git a/pkg/v1/fake/image.go b/pkg/v1/fake/image.go
new file mode 100644
index 0000000..f95ac61
--- /dev/null
+++ b/pkg/v1/fake/image.go
@@ -0,0 +1,826 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package fake
+
+import (
+ "sync"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type FakeImage struct {
+ ConfigFileStub func() (*v1.ConfigFile, error)
+ configFileMutex sync.RWMutex
+ configFileArgsForCall []struct {
+ }
+ configFileReturns struct {
+ result1 *v1.ConfigFile
+ result2 error
+ }
+ configFileReturnsOnCall map[int]struct {
+ result1 *v1.ConfigFile
+ result2 error
+ }
+ ConfigNameStub func() (v1.Hash, error)
+ configNameMutex sync.RWMutex
+ configNameArgsForCall []struct {
+ }
+ configNameReturns struct {
+ result1 v1.Hash
+ result2 error
+ }
+ configNameReturnsOnCall map[int]struct {
+ result1 v1.Hash
+ result2 error
+ }
+ DigestStub func() (v1.Hash, error)
+ digestMutex sync.RWMutex
+ digestArgsForCall []struct {
+ }
+ digestReturns struct {
+ result1 v1.Hash
+ result2 error
+ }
+ digestReturnsOnCall map[int]struct {
+ result1 v1.Hash
+ result2 error
+ }
+ LayerByDiffIDStub func(v1.Hash) (v1.Layer, error)
+ layerByDiffIDMutex sync.RWMutex
+ layerByDiffIDArgsForCall []struct {
+ arg1 v1.Hash
+ }
+ layerByDiffIDReturns struct {
+ result1 v1.Layer
+ result2 error
+ }
+ layerByDiffIDReturnsOnCall map[int]struct {
+ result1 v1.Layer
+ result2 error
+ }
+ LayerByDigestStub func(v1.Hash) (v1.Layer, error)
+ layerByDigestMutex sync.RWMutex
+ layerByDigestArgsForCall []struct {
+ arg1 v1.Hash
+ }
+ layerByDigestReturns struct {
+ result1 v1.Layer
+ result2 error
+ }
+ layerByDigestReturnsOnCall map[int]struct {
+ result1 v1.Layer
+ result2 error
+ }
+ LayersStub func() ([]v1.Layer, error)
+ layersMutex sync.RWMutex
+ layersArgsForCall []struct {
+ }
+ layersReturns struct {
+ result1 []v1.Layer
+ result2 error
+ }
+ layersReturnsOnCall map[int]struct {
+ result1 []v1.Layer
+ result2 error
+ }
+ ManifestStub func() (*v1.Manifest, error)
+ manifestMutex sync.RWMutex
+ manifestArgsForCall []struct {
+ }
+ manifestReturns struct {
+ result1 *v1.Manifest
+ result2 error
+ }
+ manifestReturnsOnCall map[int]struct {
+ result1 *v1.Manifest
+ result2 error
+ }
+ MediaTypeStub func() (types.MediaType, error)
+ mediaTypeMutex sync.RWMutex
+ mediaTypeArgsForCall []struct {
+ }
+ mediaTypeReturns struct {
+ result1 types.MediaType
+ result2 error
+ }
+ mediaTypeReturnsOnCall map[int]struct {
+ result1 types.MediaType
+ result2 error
+ }
+ RawConfigFileStub func() ([]byte, error)
+ rawConfigFileMutex sync.RWMutex
+ rawConfigFileArgsForCall []struct {
+ }
+ rawConfigFileReturns struct {
+ result1 []byte
+ result2 error
+ }
+ rawConfigFileReturnsOnCall map[int]struct {
+ result1 []byte
+ result2 error
+ }
+ RawManifestStub func() ([]byte, error)
+ rawManifestMutex sync.RWMutex
+ rawManifestArgsForCall []struct {
+ }
+ rawManifestReturns struct {
+ result1 []byte
+ result2 error
+ }
+ rawManifestReturnsOnCall map[int]struct {
+ result1 []byte
+ result2 error
+ }
+ SizeStub func() (int64, error)
+ sizeMutex sync.RWMutex
+ sizeArgsForCall []struct {
+ }
+ sizeReturns struct {
+ result1 int64
+ result2 error
+ }
+ sizeReturnsOnCall map[int]struct {
+ result1 int64
+ result2 error
+ }
+ invocations map[string][][]interface{}
+ invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeImage) ConfigFile() (*v1.ConfigFile, error) {
+ fake.configFileMutex.Lock()
+ ret, specificReturn := fake.configFileReturnsOnCall[len(fake.configFileArgsForCall)]
+ fake.configFileArgsForCall = append(fake.configFileArgsForCall, struct {
+ }{})
+ stub := fake.ConfigFileStub
+ fakeReturns := fake.configFileReturns
+ fake.recordInvocation("ConfigFile", []interface{}{})
+ fake.configFileMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) ConfigFileCallCount() int {
+ fake.configFileMutex.RLock()
+ defer fake.configFileMutex.RUnlock()
+ return len(fake.configFileArgsForCall)
+}
+
+func (fake *FakeImage) ConfigFileCalls(stub func() (*v1.ConfigFile, error)) {
+ fake.configFileMutex.Lock()
+ defer fake.configFileMutex.Unlock()
+ fake.ConfigFileStub = stub
+}
+
+func (fake *FakeImage) ConfigFileReturns(result1 *v1.ConfigFile, result2 error) {
+ fake.configFileMutex.Lock()
+ defer fake.configFileMutex.Unlock()
+ fake.ConfigFileStub = nil
+ fake.configFileReturns = struct {
+ result1 *v1.ConfigFile
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) ConfigFileReturnsOnCall(i int, result1 *v1.ConfigFile, result2 error) {
+ fake.configFileMutex.Lock()
+ defer fake.configFileMutex.Unlock()
+ fake.ConfigFileStub = nil
+ if fake.configFileReturnsOnCall == nil {
+ fake.configFileReturnsOnCall = make(map[int]struct {
+ result1 *v1.ConfigFile
+ result2 error
+ })
+ }
+ fake.configFileReturnsOnCall[i] = struct {
+ result1 *v1.ConfigFile
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) ConfigName() (v1.Hash, error) {
+ fake.configNameMutex.Lock()
+ ret, specificReturn := fake.configNameReturnsOnCall[len(fake.configNameArgsForCall)]
+ fake.configNameArgsForCall = append(fake.configNameArgsForCall, struct {
+ }{})
+ stub := fake.ConfigNameStub
+ fakeReturns := fake.configNameReturns
+ fake.recordInvocation("ConfigName", []interface{}{})
+ fake.configNameMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) ConfigNameCallCount() int {
+ fake.configNameMutex.RLock()
+ defer fake.configNameMutex.RUnlock()
+ return len(fake.configNameArgsForCall)
+}
+
+func (fake *FakeImage) ConfigNameCalls(stub func() (v1.Hash, error)) {
+ fake.configNameMutex.Lock()
+ defer fake.configNameMutex.Unlock()
+ fake.ConfigNameStub = stub
+}
+
+func (fake *FakeImage) ConfigNameReturns(result1 v1.Hash, result2 error) {
+ fake.configNameMutex.Lock()
+ defer fake.configNameMutex.Unlock()
+ fake.ConfigNameStub = nil
+ fake.configNameReturns = struct {
+ result1 v1.Hash
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) ConfigNameReturnsOnCall(i int, result1 v1.Hash, result2 error) {
+ fake.configNameMutex.Lock()
+ defer fake.configNameMutex.Unlock()
+ fake.ConfigNameStub = nil
+ if fake.configNameReturnsOnCall == nil {
+ fake.configNameReturnsOnCall = make(map[int]struct {
+ result1 v1.Hash
+ result2 error
+ })
+ }
+ fake.configNameReturnsOnCall[i] = struct {
+ result1 v1.Hash
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) Digest() (v1.Hash, error) {
+ fake.digestMutex.Lock()
+ ret, specificReturn := fake.digestReturnsOnCall[len(fake.digestArgsForCall)]
+ fake.digestArgsForCall = append(fake.digestArgsForCall, struct {
+ }{})
+ stub := fake.DigestStub
+ fakeReturns := fake.digestReturns
+ fake.recordInvocation("Digest", []interface{}{})
+ fake.digestMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) DigestCallCount() int {
+ fake.digestMutex.RLock()
+ defer fake.digestMutex.RUnlock()
+ return len(fake.digestArgsForCall)
+}
+
+func (fake *FakeImage) DigestCalls(stub func() (v1.Hash, error)) {
+ fake.digestMutex.Lock()
+ defer fake.digestMutex.Unlock()
+ fake.DigestStub = stub
+}
+
+func (fake *FakeImage) DigestReturns(result1 v1.Hash, result2 error) {
+ fake.digestMutex.Lock()
+ defer fake.digestMutex.Unlock()
+ fake.DigestStub = nil
+ fake.digestReturns = struct {
+ result1 v1.Hash
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) DigestReturnsOnCall(i int, result1 v1.Hash, result2 error) {
+ fake.digestMutex.Lock()
+ defer fake.digestMutex.Unlock()
+ fake.DigestStub = nil
+ if fake.digestReturnsOnCall == nil {
+ fake.digestReturnsOnCall = make(map[int]struct {
+ result1 v1.Hash
+ result2 error
+ })
+ }
+ fake.digestReturnsOnCall[i] = struct {
+ result1 v1.Hash
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) LayerByDiffID(arg1 v1.Hash) (v1.Layer, error) {
+ fake.layerByDiffIDMutex.Lock()
+ ret, specificReturn := fake.layerByDiffIDReturnsOnCall[len(fake.layerByDiffIDArgsForCall)]
+ fake.layerByDiffIDArgsForCall = append(fake.layerByDiffIDArgsForCall, struct {
+ arg1 v1.Hash
+ }{arg1})
+ stub := fake.LayerByDiffIDStub
+ fakeReturns := fake.layerByDiffIDReturns
+ fake.recordInvocation("LayerByDiffID", []interface{}{arg1})
+ fake.layerByDiffIDMutex.Unlock()
+ if stub != nil {
+ return stub(arg1)
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) LayerByDiffIDCallCount() int {
+ fake.layerByDiffIDMutex.RLock()
+ defer fake.layerByDiffIDMutex.RUnlock()
+ return len(fake.layerByDiffIDArgsForCall)
+}
+
+func (fake *FakeImage) LayerByDiffIDCalls(stub func(v1.Hash) (v1.Layer, error)) {
+ fake.layerByDiffIDMutex.Lock()
+ defer fake.layerByDiffIDMutex.Unlock()
+ fake.LayerByDiffIDStub = stub
+}
+
+func (fake *FakeImage) LayerByDiffIDArgsForCall(i int) v1.Hash {
+ fake.layerByDiffIDMutex.RLock()
+ defer fake.layerByDiffIDMutex.RUnlock()
+ argsForCall := fake.layerByDiffIDArgsForCall[i]
+ return argsForCall.arg1
+}
+
+func (fake *FakeImage) LayerByDiffIDReturns(result1 v1.Layer, result2 error) {
+ fake.layerByDiffIDMutex.Lock()
+ defer fake.layerByDiffIDMutex.Unlock()
+ fake.LayerByDiffIDStub = nil
+ fake.layerByDiffIDReturns = struct {
+ result1 v1.Layer
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) LayerByDiffIDReturnsOnCall(i int, result1 v1.Layer, result2 error) {
+ fake.layerByDiffIDMutex.Lock()
+ defer fake.layerByDiffIDMutex.Unlock()
+ fake.LayerByDiffIDStub = nil
+ if fake.layerByDiffIDReturnsOnCall == nil {
+ fake.layerByDiffIDReturnsOnCall = make(map[int]struct {
+ result1 v1.Layer
+ result2 error
+ })
+ }
+ fake.layerByDiffIDReturnsOnCall[i] = struct {
+ result1 v1.Layer
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) LayerByDigest(arg1 v1.Hash) (v1.Layer, error) {
+ fake.layerByDigestMutex.Lock()
+ ret, specificReturn := fake.layerByDigestReturnsOnCall[len(fake.layerByDigestArgsForCall)]
+ fake.layerByDigestArgsForCall = append(fake.layerByDigestArgsForCall, struct {
+ arg1 v1.Hash
+ }{arg1})
+ stub := fake.LayerByDigestStub
+ fakeReturns := fake.layerByDigestReturns
+ fake.recordInvocation("LayerByDigest", []interface{}{arg1})
+ fake.layerByDigestMutex.Unlock()
+ if stub != nil {
+ return stub(arg1)
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) LayerByDigestCallCount() int {
+ fake.layerByDigestMutex.RLock()
+ defer fake.layerByDigestMutex.RUnlock()
+ return len(fake.layerByDigestArgsForCall)
+}
+
+func (fake *FakeImage) LayerByDigestCalls(stub func(v1.Hash) (v1.Layer, error)) {
+ fake.layerByDigestMutex.Lock()
+ defer fake.layerByDigestMutex.Unlock()
+ fake.LayerByDigestStub = stub
+}
+
+func (fake *FakeImage) LayerByDigestArgsForCall(i int) v1.Hash {
+ fake.layerByDigestMutex.RLock()
+ defer fake.layerByDigestMutex.RUnlock()
+ argsForCall := fake.layerByDigestArgsForCall[i]
+ return argsForCall.arg1
+}
+
+func (fake *FakeImage) LayerByDigestReturns(result1 v1.Layer, result2 error) {
+ fake.layerByDigestMutex.Lock()
+ defer fake.layerByDigestMutex.Unlock()
+ fake.LayerByDigestStub = nil
+ fake.layerByDigestReturns = struct {
+ result1 v1.Layer
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) LayerByDigestReturnsOnCall(i int, result1 v1.Layer, result2 error) {
+ fake.layerByDigestMutex.Lock()
+ defer fake.layerByDigestMutex.Unlock()
+ fake.LayerByDigestStub = nil
+ if fake.layerByDigestReturnsOnCall == nil {
+ fake.layerByDigestReturnsOnCall = make(map[int]struct {
+ result1 v1.Layer
+ result2 error
+ })
+ }
+ fake.layerByDigestReturnsOnCall[i] = struct {
+ result1 v1.Layer
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) Layers() ([]v1.Layer, error) {
+ fake.layersMutex.Lock()
+ ret, specificReturn := fake.layersReturnsOnCall[len(fake.layersArgsForCall)]
+ fake.layersArgsForCall = append(fake.layersArgsForCall, struct {
+ }{})
+ stub := fake.LayersStub
+ fakeReturns := fake.layersReturns
+ fake.recordInvocation("Layers", []interface{}{})
+ fake.layersMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) LayersCallCount() int {
+ fake.layersMutex.RLock()
+ defer fake.layersMutex.RUnlock()
+ return len(fake.layersArgsForCall)
+}
+
+func (fake *FakeImage) LayersCalls(stub func() ([]v1.Layer, error)) {
+ fake.layersMutex.Lock()
+ defer fake.layersMutex.Unlock()
+ fake.LayersStub = stub
+}
+
+func (fake *FakeImage) LayersReturns(result1 []v1.Layer, result2 error) {
+ fake.layersMutex.Lock()
+ defer fake.layersMutex.Unlock()
+ fake.LayersStub = nil
+ fake.layersReturns = struct {
+ result1 []v1.Layer
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) LayersReturnsOnCall(i int, result1 []v1.Layer, result2 error) {
+ fake.layersMutex.Lock()
+ defer fake.layersMutex.Unlock()
+ fake.LayersStub = nil
+ if fake.layersReturnsOnCall == nil {
+ fake.layersReturnsOnCall = make(map[int]struct {
+ result1 []v1.Layer
+ result2 error
+ })
+ }
+ fake.layersReturnsOnCall[i] = struct {
+ result1 []v1.Layer
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) Manifest() (*v1.Manifest, error) {
+ fake.manifestMutex.Lock()
+ ret, specificReturn := fake.manifestReturnsOnCall[len(fake.manifestArgsForCall)]
+ fake.manifestArgsForCall = append(fake.manifestArgsForCall, struct {
+ }{})
+ stub := fake.ManifestStub
+ fakeReturns := fake.manifestReturns
+ fake.recordInvocation("Manifest", []interface{}{})
+ fake.manifestMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) ManifestCallCount() int {
+ fake.manifestMutex.RLock()
+ defer fake.manifestMutex.RUnlock()
+ return len(fake.manifestArgsForCall)
+}
+
+func (fake *FakeImage) ManifestCalls(stub func() (*v1.Manifest, error)) {
+ fake.manifestMutex.Lock()
+ defer fake.manifestMutex.Unlock()
+ fake.ManifestStub = stub
+}
+
+func (fake *FakeImage) ManifestReturns(result1 *v1.Manifest, result2 error) {
+ fake.manifestMutex.Lock()
+ defer fake.manifestMutex.Unlock()
+ fake.ManifestStub = nil
+ fake.manifestReturns = struct {
+ result1 *v1.Manifest
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) ManifestReturnsOnCall(i int, result1 *v1.Manifest, result2 error) {
+ fake.manifestMutex.Lock()
+ defer fake.manifestMutex.Unlock()
+ fake.ManifestStub = nil
+ if fake.manifestReturnsOnCall == nil {
+ fake.manifestReturnsOnCall = make(map[int]struct {
+ result1 *v1.Manifest
+ result2 error
+ })
+ }
+ fake.manifestReturnsOnCall[i] = struct {
+ result1 *v1.Manifest
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) MediaType() (types.MediaType, error) {
+ fake.mediaTypeMutex.Lock()
+ ret, specificReturn := fake.mediaTypeReturnsOnCall[len(fake.mediaTypeArgsForCall)]
+ fake.mediaTypeArgsForCall = append(fake.mediaTypeArgsForCall, struct {
+ }{})
+ stub := fake.MediaTypeStub
+ fakeReturns := fake.mediaTypeReturns
+ fake.recordInvocation("MediaType", []interface{}{})
+ fake.mediaTypeMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) MediaTypeCallCount() int {
+ fake.mediaTypeMutex.RLock()
+ defer fake.mediaTypeMutex.RUnlock()
+ return len(fake.mediaTypeArgsForCall)
+}
+
+func (fake *FakeImage) MediaTypeCalls(stub func() (types.MediaType, error)) {
+ fake.mediaTypeMutex.Lock()
+ defer fake.mediaTypeMutex.Unlock()
+ fake.MediaTypeStub = stub
+}
+
+func (fake *FakeImage) MediaTypeReturns(result1 types.MediaType, result2 error) {
+ fake.mediaTypeMutex.Lock()
+ defer fake.mediaTypeMutex.Unlock()
+ fake.MediaTypeStub = nil
+ fake.mediaTypeReturns = struct {
+ result1 types.MediaType
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) MediaTypeReturnsOnCall(i int, result1 types.MediaType, result2 error) {
+ fake.mediaTypeMutex.Lock()
+ defer fake.mediaTypeMutex.Unlock()
+ fake.MediaTypeStub = nil
+ if fake.mediaTypeReturnsOnCall == nil {
+ fake.mediaTypeReturnsOnCall = make(map[int]struct {
+ result1 types.MediaType
+ result2 error
+ })
+ }
+ fake.mediaTypeReturnsOnCall[i] = struct {
+ result1 types.MediaType
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) RawConfigFile() ([]byte, error) {
+ fake.rawConfigFileMutex.Lock()
+ ret, specificReturn := fake.rawConfigFileReturnsOnCall[len(fake.rawConfigFileArgsForCall)]
+ fake.rawConfigFileArgsForCall = append(fake.rawConfigFileArgsForCall, struct {
+ }{})
+ stub := fake.RawConfigFileStub
+ fakeReturns := fake.rawConfigFileReturns
+ fake.recordInvocation("RawConfigFile", []interface{}{})
+ fake.rawConfigFileMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) RawConfigFileCallCount() int {
+ fake.rawConfigFileMutex.RLock()
+ defer fake.rawConfigFileMutex.RUnlock()
+ return len(fake.rawConfigFileArgsForCall)
+}
+
+func (fake *FakeImage) RawConfigFileCalls(stub func() ([]byte, error)) {
+ fake.rawConfigFileMutex.Lock()
+ defer fake.rawConfigFileMutex.Unlock()
+ fake.RawConfigFileStub = stub
+}
+
+func (fake *FakeImage) RawConfigFileReturns(result1 []byte, result2 error) {
+ fake.rawConfigFileMutex.Lock()
+ defer fake.rawConfigFileMutex.Unlock()
+ fake.RawConfigFileStub = nil
+ fake.rawConfigFileReturns = struct {
+ result1 []byte
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) RawConfigFileReturnsOnCall(i int, result1 []byte, result2 error) {
+ fake.rawConfigFileMutex.Lock()
+ defer fake.rawConfigFileMutex.Unlock()
+ fake.RawConfigFileStub = nil
+ if fake.rawConfigFileReturnsOnCall == nil {
+ fake.rawConfigFileReturnsOnCall = make(map[int]struct {
+ result1 []byte
+ result2 error
+ })
+ }
+ fake.rawConfigFileReturnsOnCall[i] = struct {
+ result1 []byte
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) RawManifest() ([]byte, error) {
+ fake.rawManifestMutex.Lock()
+ ret, specificReturn := fake.rawManifestReturnsOnCall[len(fake.rawManifestArgsForCall)]
+ fake.rawManifestArgsForCall = append(fake.rawManifestArgsForCall, struct {
+ }{})
+ stub := fake.RawManifestStub
+ fakeReturns := fake.rawManifestReturns
+ fake.recordInvocation("RawManifest", []interface{}{})
+ fake.rawManifestMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) RawManifestCallCount() int {
+ fake.rawManifestMutex.RLock()
+ defer fake.rawManifestMutex.RUnlock()
+ return len(fake.rawManifestArgsForCall)
+}
+
+func (fake *FakeImage) RawManifestCalls(stub func() ([]byte, error)) {
+ fake.rawManifestMutex.Lock()
+ defer fake.rawManifestMutex.Unlock()
+ fake.RawManifestStub = stub
+}
+
+func (fake *FakeImage) RawManifestReturns(result1 []byte, result2 error) {
+ fake.rawManifestMutex.Lock()
+ defer fake.rawManifestMutex.Unlock()
+ fake.RawManifestStub = nil
+ fake.rawManifestReturns = struct {
+ result1 []byte
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) RawManifestReturnsOnCall(i int, result1 []byte, result2 error) {
+ fake.rawManifestMutex.Lock()
+ defer fake.rawManifestMutex.Unlock()
+ fake.RawManifestStub = nil
+ if fake.rawManifestReturnsOnCall == nil {
+ fake.rawManifestReturnsOnCall = make(map[int]struct {
+ result1 []byte
+ result2 error
+ })
+ }
+ fake.rawManifestReturnsOnCall[i] = struct {
+ result1 []byte
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) Size() (int64, error) {
+ fake.sizeMutex.Lock()
+ ret, specificReturn := fake.sizeReturnsOnCall[len(fake.sizeArgsForCall)]
+ fake.sizeArgsForCall = append(fake.sizeArgsForCall, struct {
+ }{})
+ stub := fake.SizeStub
+ fakeReturns := fake.sizeReturns
+ fake.recordInvocation("Size", []interface{}{})
+ fake.sizeMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImage) SizeCallCount() int {
+ fake.sizeMutex.RLock()
+ defer fake.sizeMutex.RUnlock()
+ return len(fake.sizeArgsForCall)
+}
+
+func (fake *FakeImage) SizeCalls(stub func() (int64, error)) {
+ fake.sizeMutex.Lock()
+ defer fake.sizeMutex.Unlock()
+ fake.SizeStub = stub
+}
+
+func (fake *FakeImage) SizeReturns(result1 int64, result2 error) {
+ fake.sizeMutex.Lock()
+ defer fake.sizeMutex.Unlock()
+ fake.SizeStub = nil
+ fake.sizeReturns = struct {
+ result1 int64
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) SizeReturnsOnCall(i int, result1 int64, result2 error) {
+ fake.sizeMutex.Lock()
+ defer fake.sizeMutex.Unlock()
+ fake.SizeStub = nil
+ if fake.sizeReturnsOnCall == nil {
+ fake.sizeReturnsOnCall = make(map[int]struct {
+ result1 int64
+ result2 error
+ })
+ }
+ fake.sizeReturnsOnCall[i] = struct {
+ result1 int64
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImage) Invocations() map[string][][]interface{} {
+ fake.invocationsMutex.RLock()
+ defer fake.invocationsMutex.RUnlock()
+ fake.configFileMutex.RLock()
+ defer fake.configFileMutex.RUnlock()
+ fake.configNameMutex.RLock()
+ defer fake.configNameMutex.RUnlock()
+ fake.digestMutex.RLock()
+ defer fake.digestMutex.RUnlock()
+ fake.layerByDiffIDMutex.RLock()
+ defer fake.layerByDiffIDMutex.RUnlock()
+ fake.layerByDigestMutex.RLock()
+ defer fake.layerByDigestMutex.RUnlock()
+ fake.layersMutex.RLock()
+ defer fake.layersMutex.RUnlock()
+ fake.manifestMutex.RLock()
+ defer fake.manifestMutex.RUnlock()
+ fake.mediaTypeMutex.RLock()
+ defer fake.mediaTypeMutex.RUnlock()
+ fake.rawConfigFileMutex.RLock()
+ defer fake.rawConfigFileMutex.RUnlock()
+ fake.rawManifestMutex.RLock()
+ defer fake.rawManifestMutex.RUnlock()
+ fake.sizeMutex.RLock()
+ defer fake.sizeMutex.RUnlock()
+ copiedInvocations := map[string][][]interface{}{}
+ for key, value := range fake.invocations {
+ copiedInvocations[key] = value
+ }
+ return copiedInvocations
+}
+
+func (fake *FakeImage) recordInvocation(key string, args []interface{}) {
+ fake.invocationsMutex.Lock()
+ defer fake.invocationsMutex.Unlock()
+ if fake.invocations == nil {
+ fake.invocations = map[string][][]interface{}{}
+ }
+ if fake.invocations[key] == nil {
+ fake.invocations[key] = [][]interface{}{}
+ }
+ fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ v1.Image = new(FakeImage)
diff --git a/pkg/v1/fake/index.go b/pkg/v1/fake/index.go
new file mode 100644
index 0000000..8c66a98
--- /dev/null
+++ b/pkg/v1/fake/index.go
@@ -0,0 +1,546 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package fake
+
+import (
+ "sync"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type FakeImageIndex struct {
+ DigestStub func() (v1.Hash, error)
+ digestMutex sync.RWMutex
+ digestArgsForCall []struct {
+ }
+ digestReturns struct {
+ result1 v1.Hash
+ result2 error
+ }
+ digestReturnsOnCall map[int]struct {
+ result1 v1.Hash
+ result2 error
+ }
+ ImageStub func(v1.Hash) (v1.Image, error)
+ imageMutex sync.RWMutex
+ imageArgsForCall []struct {
+ arg1 v1.Hash
+ }
+ imageReturns struct {
+ result1 v1.Image
+ result2 error
+ }
+ imageReturnsOnCall map[int]struct {
+ result1 v1.Image
+ result2 error
+ }
+ ImageIndexStub func(v1.Hash) (v1.ImageIndex, error)
+ imageIndexMutex sync.RWMutex
+ imageIndexArgsForCall []struct {
+ arg1 v1.Hash
+ }
+ imageIndexReturns struct {
+ result1 v1.ImageIndex
+ result2 error
+ }
+ imageIndexReturnsOnCall map[int]struct {
+ result1 v1.ImageIndex
+ result2 error
+ }
+ IndexManifestStub func() (*v1.IndexManifest, error)
+ indexManifestMutex sync.RWMutex
+ indexManifestArgsForCall []struct {
+ }
+ indexManifestReturns struct {
+ result1 *v1.IndexManifest
+ result2 error
+ }
+ indexManifestReturnsOnCall map[int]struct {
+ result1 *v1.IndexManifest
+ result2 error
+ }
+ MediaTypeStub func() (types.MediaType, error)
+ mediaTypeMutex sync.RWMutex
+ mediaTypeArgsForCall []struct {
+ }
+ mediaTypeReturns struct {
+ result1 types.MediaType
+ result2 error
+ }
+ mediaTypeReturnsOnCall map[int]struct {
+ result1 types.MediaType
+ result2 error
+ }
+ RawManifestStub func() ([]byte, error)
+ rawManifestMutex sync.RWMutex
+ rawManifestArgsForCall []struct {
+ }
+ rawManifestReturns struct {
+ result1 []byte
+ result2 error
+ }
+ rawManifestReturnsOnCall map[int]struct {
+ result1 []byte
+ result2 error
+ }
+ SizeStub func() (int64, error)
+ sizeMutex sync.RWMutex
+ sizeArgsForCall []struct {
+ }
+ sizeReturns struct {
+ result1 int64
+ result2 error
+ }
+ sizeReturnsOnCall map[int]struct {
+ result1 int64
+ result2 error
+ }
+ invocations map[string][][]interface{}
+ invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeImageIndex) Digest() (v1.Hash, error) {
+ fake.digestMutex.Lock()
+ ret, specificReturn := fake.digestReturnsOnCall[len(fake.digestArgsForCall)]
+ fake.digestArgsForCall = append(fake.digestArgsForCall, struct {
+ }{})
+ stub := fake.DigestStub
+ fakeReturns := fake.digestReturns
+ fake.recordInvocation("Digest", []interface{}{})
+ fake.digestMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImageIndex) DigestCallCount() int {
+ fake.digestMutex.RLock()
+ defer fake.digestMutex.RUnlock()
+ return len(fake.digestArgsForCall)
+}
+
+func (fake *FakeImageIndex) DigestCalls(stub func() (v1.Hash, error)) {
+ fake.digestMutex.Lock()
+ defer fake.digestMutex.Unlock()
+ fake.DigestStub = stub
+}
+
+func (fake *FakeImageIndex) DigestReturns(result1 v1.Hash, result2 error) {
+ fake.digestMutex.Lock()
+ defer fake.digestMutex.Unlock()
+ fake.DigestStub = nil
+ fake.digestReturns = struct {
+ result1 v1.Hash
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) DigestReturnsOnCall(i int, result1 v1.Hash, result2 error) {
+ fake.digestMutex.Lock()
+ defer fake.digestMutex.Unlock()
+ fake.DigestStub = nil
+ if fake.digestReturnsOnCall == nil {
+ fake.digestReturnsOnCall = make(map[int]struct {
+ result1 v1.Hash
+ result2 error
+ })
+ }
+ fake.digestReturnsOnCall[i] = struct {
+ result1 v1.Hash
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) Image(arg1 v1.Hash) (v1.Image, error) {
+ fake.imageMutex.Lock()
+ ret, specificReturn := fake.imageReturnsOnCall[len(fake.imageArgsForCall)]
+ fake.imageArgsForCall = append(fake.imageArgsForCall, struct {
+ arg1 v1.Hash
+ }{arg1})
+ stub := fake.ImageStub
+ fakeReturns := fake.imageReturns
+ fake.recordInvocation("Image", []interface{}{arg1})
+ fake.imageMutex.Unlock()
+ if stub != nil {
+ return stub(arg1)
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImageIndex) ImageCallCount() int {
+ fake.imageMutex.RLock()
+ defer fake.imageMutex.RUnlock()
+ return len(fake.imageArgsForCall)
+}
+
+func (fake *FakeImageIndex) ImageCalls(stub func(v1.Hash) (v1.Image, error)) {
+ fake.imageMutex.Lock()
+ defer fake.imageMutex.Unlock()
+ fake.ImageStub = stub
+}
+
+func (fake *FakeImageIndex) ImageArgsForCall(i int) v1.Hash {
+ fake.imageMutex.RLock()
+ defer fake.imageMutex.RUnlock()
+ argsForCall := fake.imageArgsForCall[i]
+ return argsForCall.arg1
+}
+
+func (fake *FakeImageIndex) ImageReturns(result1 v1.Image, result2 error) {
+ fake.imageMutex.Lock()
+ defer fake.imageMutex.Unlock()
+ fake.ImageStub = nil
+ fake.imageReturns = struct {
+ result1 v1.Image
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) ImageReturnsOnCall(i int, result1 v1.Image, result2 error) {
+ fake.imageMutex.Lock()
+ defer fake.imageMutex.Unlock()
+ fake.ImageStub = nil
+ if fake.imageReturnsOnCall == nil {
+ fake.imageReturnsOnCall = make(map[int]struct {
+ result1 v1.Image
+ result2 error
+ })
+ }
+ fake.imageReturnsOnCall[i] = struct {
+ result1 v1.Image
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) ImageIndex(arg1 v1.Hash) (v1.ImageIndex, error) {
+ fake.imageIndexMutex.Lock()
+ ret, specificReturn := fake.imageIndexReturnsOnCall[len(fake.imageIndexArgsForCall)]
+ fake.imageIndexArgsForCall = append(fake.imageIndexArgsForCall, struct {
+ arg1 v1.Hash
+ }{arg1})
+ stub := fake.ImageIndexStub
+ fakeReturns := fake.imageIndexReturns
+ fake.recordInvocation("ImageIndex", []interface{}{arg1})
+ fake.imageIndexMutex.Unlock()
+ if stub != nil {
+ return stub(arg1)
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImageIndex) ImageIndexCallCount() int {
+ fake.imageIndexMutex.RLock()
+ defer fake.imageIndexMutex.RUnlock()
+ return len(fake.imageIndexArgsForCall)
+}
+
+func (fake *FakeImageIndex) ImageIndexCalls(stub func(v1.Hash) (v1.ImageIndex, error)) {
+ fake.imageIndexMutex.Lock()
+ defer fake.imageIndexMutex.Unlock()
+ fake.ImageIndexStub = stub
+}
+
+func (fake *FakeImageIndex) ImageIndexArgsForCall(i int) v1.Hash {
+ fake.imageIndexMutex.RLock()
+ defer fake.imageIndexMutex.RUnlock()
+ argsForCall := fake.imageIndexArgsForCall[i]
+ return argsForCall.arg1
+}
+
+func (fake *FakeImageIndex) ImageIndexReturns(result1 v1.ImageIndex, result2 error) {
+ fake.imageIndexMutex.Lock()
+ defer fake.imageIndexMutex.Unlock()
+ fake.ImageIndexStub = nil
+ fake.imageIndexReturns = struct {
+ result1 v1.ImageIndex
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) ImageIndexReturnsOnCall(i int, result1 v1.ImageIndex, result2 error) {
+ fake.imageIndexMutex.Lock()
+ defer fake.imageIndexMutex.Unlock()
+ fake.ImageIndexStub = nil
+ if fake.imageIndexReturnsOnCall == nil {
+ fake.imageIndexReturnsOnCall = make(map[int]struct {
+ result1 v1.ImageIndex
+ result2 error
+ })
+ }
+ fake.imageIndexReturnsOnCall[i] = struct {
+ result1 v1.ImageIndex
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) IndexManifest() (*v1.IndexManifest, error) {
+ fake.indexManifestMutex.Lock()
+ ret, specificReturn := fake.indexManifestReturnsOnCall[len(fake.indexManifestArgsForCall)]
+ fake.indexManifestArgsForCall = append(fake.indexManifestArgsForCall, struct {
+ }{})
+ stub := fake.IndexManifestStub
+ fakeReturns := fake.indexManifestReturns
+ fake.recordInvocation("IndexManifest", []interface{}{})
+ fake.indexManifestMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImageIndex) IndexManifestCallCount() int {
+ fake.indexManifestMutex.RLock()
+ defer fake.indexManifestMutex.RUnlock()
+ return len(fake.indexManifestArgsForCall)
+}
+
+func (fake *FakeImageIndex) IndexManifestCalls(stub func() (*v1.IndexManifest, error)) {
+ fake.indexManifestMutex.Lock()
+ defer fake.indexManifestMutex.Unlock()
+ fake.IndexManifestStub = stub
+}
+
+func (fake *FakeImageIndex) IndexManifestReturns(result1 *v1.IndexManifest, result2 error) {
+ fake.indexManifestMutex.Lock()
+ defer fake.indexManifestMutex.Unlock()
+ fake.IndexManifestStub = nil
+ fake.indexManifestReturns = struct {
+ result1 *v1.IndexManifest
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) IndexManifestReturnsOnCall(i int, result1 *v1.IndexManifest, result2 error) {
+ fake.indexManifestMutex.Lock()
+ defer fake.indexManifestMutex.Unlock()
+ fake.IndexManifestStub = nil
+ if fake.indexManifestReturnsOnCall == nil {
+ fake.indexManifestReturnsOnCall = make(map[int]struct {
+ result1 *v1.IndexManifest
+ result2 error
+ })
+ }
+ fake.indexManifestReturnsOnCall[i] = struct {
+ result1 *v1.IndexManifest
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) MediaType() (types.MediaType, error) {
+ fake.mediaTypeMutex.Lock()
+ ret, specificReturn := fake.mediaTypeReturnsOnCall[len(fake.mediaTypeArgsForCall)]
+ fake.mediaTypeArgsForCall = append(fake.mediaTypeArgsForCall, struct {
+ }{})
+ stub := fake.MediaTypeStub
+ fakeReturns := fake.mediaTypeReturns
+ fake.recordInvocation("MediaType", []interface{}{})
+ fake.mediaTypeMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImageIndex) MediaTypeCallCount() int {
+ fake.mediaTypeMutex.RLock()
+ defer fake.mediaTypeMutex.RUnlock()
+ return len(fake.mediaTypeArgsForCall)
+}
+
+func (fake *FakeImageIndex) MediaTypeCalls(stub func() (types.MediaType, error)) {
+ fake.mediaTypeMutex.Lock()
+ defer fake.mediaTypeMutex.Unlock()
+ fake.MediaTypeStub = stub
+}
+
+func (fake *FakeImageIndex) MediaTypeReturns(result1 types.MediaType, result2 error) {
+ fake.mediaTypeMutex.Lock()
+ defer fake.mediaTypeMutex.Unlock()
+ fake.MediaTypeStub = nil
+ fake.mediaTypeReturns = struct {
+ result1 types.MediaType
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) MediaTypeReturnsOnCall(i int, result1 types.MediaType, result2 error) {
+ fake.mediaTypeMutex.Lock()
+ defer fake.mediaTypeMutex.Unlock()
+ fake.MediaTypeStub = nil
+ if fake.mediaTypeReturnsOnCall == nil {
+ fake.mediaTypeReturnsOnCall = make(map[int]struct {
+ result1 types.MediaType
+ result2 error
+ })
+ }
+ fake.mediaTypeReturnsOnCall[i] = struct {
+ result1 types.MediaType
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) RawManifest() ([]byte, error) {
+ fake.rawManifestMutex.Lock()
+ ret, specificReturn := fake.rawManifestReturnsOnCall[len(fake.rawManifestArgsForCall)]
+ fake.rawManifestArgsForCall = append(fake.rawManifestArgsForCall, struct {
+ }{})
+ stub := fake.RawManifestStub
+ fakeReturns := fake.rawManifestReturns
+ fake.recordInvocation("RawManifest", []interface{}{})
+ fake.rawManifestMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImageIndex) RawManifestCallCount() int {
+ fake.rawManifestMutex.RLock()
+ defer fake.rawManifestMutex.RUnlock()
+ return len(fake.rawManifestArgsForCall)
+}
+
+func (fake *FakeImageIndex) RawManifestCalls(stub func() ([]byte, error)) {
+ fake.rawManifestMutex.Lock()
+ defer fake.rawManifestMutex.Unlock()
+ fake.RawManifestStub = stub
+}
+
+func (fake *FakeImageIndex) RawManifestReturns(result1 []byte, result2 error) {
+ fake.rawManifestMutex.Lock()
+ defer fake.rawManifestMutex.Unlock()
+ fake.RawManifestStub = nil
+ fake.rawManifestReturns = struct {
+ result1 []byte
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) RawManifestReturnsOnCall(i int, result1 []byte, result2 error) {
+ fake.rawManifestMutex.Lock()
+ defer fake.rawManifestMutex.Unlock()
+ fake.RawManifestStub = nil
+ if fake.rawManifestReturnsOnCall == nil {
+ fake.rawManifestReturnsOnCall = make(map[int]struct {
+ result1 []byte
+ result2 error
+ })
+ }
+ fake.rawManifestReturnsOnCall[i] = struct {
+ result1 []byte
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) Size() (int64, error) {
+ fake.sizeMutex.Lock()
+ ret, specificReturn := fake.sizeReturnsOnCall[len(fake.sizeArgsForCall)]
+ fake.sizeArgsForCall = append(fake.sizeArgsForCall, struct {
+ }{})
+ stub := fake.SizeStub
+ fakeReturns := fake.sizeReturns
+ fake.recordInvocation("Size", []interface{}{})
+ fake.sizeMutex.Unlock()
+ if stub != nil {
+ return stub()
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImageIndex) SizeCallCount() int {
+ fake.sizeMutex.RLock()
+ defer fake.sizeMutex.RUnlock()
+ return len(fake.sizeArgsForCall)
+}
+
+func (fake *FakeImageIndex) SizeCalls(stub func() (int64, error)) {
+ fake.sizeMutex.Lock()
+ defer fake.sizeMutex.Unlock()
+ fake.SizeStub = stub
+}
+
+func (fake *FakeImageIndex) SizeReturns(result1 int64, result2 error) {
+ fake.sizeMutex.Lock()
+ defer fake.sizeMutex.Unlock()
+ fake.SizeStub = nil
+ fake.sizeReturns = struct {
+ result1 int64
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) SizeReturnsOnCall(i int, result1 int64, result2 error) {
+ fake.sizeMutex.Lock()
+ defer fake.sizeMutex.Unlock()
+ fake.SizeStub = nil
+ if fake.sizeReturnsOnCall == nil {
+ fake.sizeReturnsOnCall = make(map[int]struct {
+ result1 int64
+ result2 error
+ })
+ }
+ fake.sizeReturnsOnCall[i] = struct {
+ result1 int64
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeImageIndex) Invocations() map[string][][]interface{} {
+ fake.invocationsMutex.RLock()
+ defer fake.invocationsMutex.RUnlock()
+ fake.digestMutex.RLock()
+ defer fake.digestMutex.RUnlock()
+ fake.imageMutex.RLock()
+ defer fake.imageMutex.RUnlock()
+ fake.imageIndexMutex.RLock()
+ defer fake.imageIndexMutex.RUnlock()
+ fake.indexManifestMutex.RLock()
+ defer fake.indexManifestMutex.RUnlock()
+ fake.mediaTypeMutex.RLock()
+ defer fake.mediaTypeMutex.RUnlock()
+ fake.rawManifestMutex.RLock()
+ defer fake.rawManifestMutex.RUnlock()
+ fake.sizeMutex.RLock()
+ defer fake.sizeMutex.RUnlock()
+ copiedInvocations := map[string][][]interface{}{}
+ for key, value := range fake.invocations {
+ copiedInvocations[key] = value
+ }
+ return copiedInvocations
+}
+
+func (fake *FakeImageIndex) recordInvocation(key string, args []interface{}) {
+ fake.invocationsMutex.Lock()
+ defer fake.invocationsMutex.Unlock()
+ if fake.invocations == nil {
+ fake.invocations = map[string][][]interface{}{}
+ }
+ if fake.invocations[key] == nil {
+ fake.invocations[key] = [][]interface{}{}
+ }
+ fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ v1.ImageIndex = new(FakeImageIndex)
diff --git a/pkg/v1/google/README.md b/pkg/v1/google/README.md
new file mode 100644
index 0000000..7cd8971
--- /dev/null
+++ b/pkg/v1/google/README.md
@@ -0,0 +1,7 @@
+# `google`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/google?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/google)
+
+The `google` package provides:
+* Some google-specific authentication methods.
+* Some [GCR](gcr.io)-specific listing methods.
diff --git a/pkg/v1/google/auth.go b/pkg/v1/google/auth.go
new file mode 100644
index 0000000..11ae397
--- /dev/null
+++ b/pkg/v1/google/auth.go
@@ -0,0 +1,179 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "time"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "golang.org/x/oauth2"
+ googauth "golang.org/x/oauth2/google"
+)
+
+const cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
+
+// GetGcloudCmd is exposed so we can test this.
+var GetGcloudCmd = func() *exec.Cmd {
+ // This is odd, but basically what docker-credential-gcr does.
+ //
+ // config-helper is undocumented, but it's purportedly the only supported way
+ // of accessing tokens (`gcloud auth print-access-token` is discouraged).
+ //
+ // --force-auth-refresh means we are getting a token that is valid for about
+ // an hour (we reuse it until it's expired).
+ return exec.Command("gcloud", "config", "config-helper", "--force-auth-refresh", "--format=json(credential)")
+}
+
+// NewEnvAuthenticator returns an authn.Authenticator that generates access
+// tokens from the environment we're running in.
+//
+// See: https://godoc.org/golang.org/x/oauth2/google#FindDefaultCredentials
+func NewEnvAuthenticator() (authn.Authenticator, error) {
+ ts, err := googauth.DefaultTokenSource(context.Background(), cloudPlatformScope)
+ if err != nil {
+ return nil, err
+ }
+
+ token, err := ts.Token()
+ if err != nil {
+ return nil, err
+ }
+
+ return &tokenSourceAuth{oauth2.ReuseTokenSource(token, ts)}, nil
+}
+
+// NewGcloudAuthenticator returns an oauth2.TokenSource that generates access
+// tokens by shelling out to the gcloud sdk.
+func NewGcloudAuthenticator() (authn.Authenticator, error) {
+ if _, err := exec.LookPath("gcloud"); err != nil {
+ // gcloud is not available, fall back to anonymous
+ logs.Warn.Println("gcloud binary not found")
+ return authn.Anonymous, nil
+ }
+
+ ts := gcloudSource{GetGcloudCmd}
+
+ // Attempt to fetch a token to ensure gcloud is installed and we can run it.
+ token, err := ts.Token()
+ if err != nil {
+ return nil, err
+ }
+
+ return &tokenSourceAuth{oauth2.ReuseTokenSource(token, ts)}, nil
+}
+
+// NewJSONKeyAuthenticator returns a Basic authenticator which uses Service Account
+// as a way of authenticating with Google Container Registry.
+// More information: https://cloud.google.com/container-registry/docs/advanced-authentication#json_key_file
+func NewJSONKeyAuthenticator(serviceAccountJSON string) authn.Authenticator {
+ return &authn.Basic{
+ Username: "_json_key",
+ Password: serviceAccountJSON,
+ }
+}
+
+// NewTokenAuthenticator returns an oauth2.TokenSource that generates access
+// tokens by using the Google SDK to produce JWT tokens from a Service Account.
+// More information: https://godoc.org/golang.org/x/oauth2/google#JWTAccessTokenSourceFromJSON
+func NewTokenAuthenticator(serviceAccountJSON string, scope string) (authn.Authenticator, error) {
+ ts, err := googauth.JWTAccessTokenSourceFromJSON([]byte(serviceAccountJSON), scope)
+ if err != nil {
+ return nil, err
+ }
+
+ return &tokenSourceAuth{oauth2.ReuseTokenSource(nil, ts)}, nil
+}
+
+// NewTokenSourceAuthenticator converts an oauth2.TokenSource into an authn.Authenticator.
+func NewTokenSourceAuthenticator(ts oauth2.TokenSource) authn.Authenticator {
+ return &tokenSourceAuth{ts}
+}
+
+// tokenSourceAuth turns an oauth2.TokenSource into an authn.Authenticator.
+type tokenSourceAuth struct {
+ oauth2.TokenSource
+}
+
+// Authorization implements authn.Authenticator.
+func (tsa *tokenSourceAuth) Authorization() (*authn.AuthConfig, error) {
+ token, err := tsa.Token()
+ if err != nil {
+ return nil, err
+ }
+
+ return &authn.AuthConfig{
+ Username: "_token",
+ Password: token.AccessToken,
+ }, nil
+}
+
+// gcloudOutput represents the output of the gcloud command we invoke.
+//
+// `gcloud config config-helper --format=json(credential)` looks something like:
+//
+// {
+// "credential": {
+// "access_token": "supersecretaccesstoken",
+// "token_expiry": "2018-12-02T04:08:13Z"
+// }
+// }
+type gcloudOutput struct {
+ Credential struct {
+ AccessToken string `json:"access_token"`
+ TokenExpiry string `json:"token_expiry"`
+ } `json:"credential"`
+}
+
+type gcloudSource struct {
+ // This is passed in so that we mock out gcloud and test Token.
+ exec func() *exec.Cmd
+}
+
+// Token implements oauath2.TokenSource.
+func (gs gcloudSource) Token() (*oauth2.Token, error) {
+ cmd := gs.exec()
+ var out bytes.Buffer
+ cmd.Stdout = &out
+
+ // Don't attempt to interpret stderr, just pass it through.
+ cmd.Stderr = logs.Warn.Writer()
+
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("error executing `gcloud config config-helper`: %w", err)
+ }
+
+ creds := gcloudOutput{}
+ if err := json.Unmarshal(out.Bytes(), &creds); err != nil {
+ return nil, fmt.Errorf("failed to parse `gcloud config config-helper` output: %w", err)
+ }
+
+ expiry, err := time.Parse(time.RFC3339, creds.Credential.TokenExpiry)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse gcloud token expiry: %w", err)
+ }
+
+ token := oauth2.Token{
+ AccessToken: creds.Credential.AccessToken,
+ Expiry: expiry,
+ }
+
+ return &token, nil
+}
diff --git a/pkg/v1/google/auth_test.go b/pkg/v1/google/auth_test.go
new file mode 100644
index 0000000..d2974ff
--- /dev/null
+++ b/pkg/v1/google/auth_test.go
@@ -0,0 +1,270 @@
+//go:build !arm64
+// +build !arm64
+
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ "golang.org/x/oauth2"
+)
+
+const (
+ // Fails to parse as JSON at all.
+ badoutput = ""
+
+ // Fails to parse token_expiry format.
+ badexpiry = `
+{
+ "credential": {
+ "access_token": "mytoken",
+ "token_expiry": "most-definitely-not-a-date"
+ }
+}`
+
+ // Expires in 6,000 years. Hopefully nobody is using software then.
+ success = `
+{
+ "credential": {
+ "access_token": "mytoken",
+ "token_expiry": "8018-12-02T04:08:13Z"
+ }
+}`
+)
+
+// We'll invoke ourselves with a special environment variable in order to mock
+// out the gcloud dependency of gcloudSource. The exec package does this, too.
+//
+// See: https://www.joeshaw.org/testing-with-os-exec-and-testmain/
+//
+// TODO(#908): This doesn't work on arm64 or darwin for some reason.
+func TestMain(m *testing.M) {
+ switch os.Getenv("GO_TEST_MODE") {
+ case "":
+ // Normal test mode
+ os.Exit(m.Run())
+
+ case "error":
+ // Makes cmd.Run() return an error.
+ os.Exit(2)
+
+ case "badoutput":
+ // Makes the gcloudOutput Unmarshaler fail.
+ fmt.Println(badoutput)
+
+ case "badexpiry":
+ // Makes the token_expiry time parser fail.
+ fmt.Println(badexpiry)
+
+ case "success":
+ // Returns a seemingly valid token.
+ fmt.Println(success)
+ }
+}
+
+func newGcloudCmdMock(env string) func() *exec.Cmd {
+ return func() *exec.Cmd {
+ cmd := exec.Command(os.Args[0])
+ cmd.Env = []string{fmt.Sprintf("GO_TEST_MODE=%s", env)}
+ return cmd
+ }
+}
+
+func TestGcloudErrors(t *testing.T) {
+ cases := []struct {
+ env string
+
+ // Just look for the prefix because we can't control other packages' errors.
+ wantPrefix string
+ }{{
+ env: "error",
+ wantPrefix: "error executing `gcloud config config-helper`:",
+ }, {
+ env: "badoutput",
+ wantPrefix: "failed to parse `gcloud config config-helper` output:",
+ }, {
+ env: "badexpiry",
+ wantPrefix: "failed to parse gcloud token expiry:",
+ }}
+
+ for _, tc := range cases {
+ t.Run(tc.env, func(t *testing.T) {
+ GetGcloudCmd = newGcloudCmdMock(tc.env)
+
+ if _, err := NewGcloudAuthenticator(); err == nil {
+ t.Errorf("wanted error, got nil")
+ } else if got := err.Error(); !strings.HasPrefix(got, tc.wantPrefix) {
+ t.Errorf("wanted error prefix %q, got %q", tc.wantPrefix, got)
+ }
+ })
+ }
+}
+
+func TestGcloudSuccess(t *testing.T) {
+ // Stupid coverage to make sure it doesn't panic.
+ var b bytes.Buffer
+ logs.Debug.SetOutput(&b)
+
+ GetGcloudCmd = newGcloudCmdMock("success")
+
+ auth, err := NewGcloudAuthenticator()
+ if err != nil {
+ t.Fatalf("NewGcloudAuthenticator got error %v", err)
+ }
+
+ token, err := auth.Authorization()
+ if err != nil {
+ t.Fatalf("Authorization got error %v", err)
+ }
+
+ if got, want := token.Password, "mytoken"; got != want {
+ t.Errorf("wanted token %q, got %q", want, got)
+ }
+}
+
+//
+// Keychain tests are in here so we can reuse the fake gcloud stuff.
+//
+
+func mustRegistry(r string) name.Registry {
+ reg, err := name.NewRegistry(r, name.StrictValidation)
+ if err != nil {
+ panic(err)
+ }
+ return reg
+}
+
+func TestKeychainDockerHub(t *testing.T) {
+ if auth, err := Keychain.Resolve(mustRegistry("index.docker.io")); err != nil {
+ t.Errorf("expected success, got: %v", err)
+ } else if auth != authn.Anonymous {
+ t.Errorf("expected anonymous, got: %v", auth)
+ }
+}
+
+func TestKeychainGCRandAR(t *testing.T) {
+ cases := []struct {
+ host string
+ expectAuth bool
+ }{
+ // GCR hosts
+ {"gcr.io", true},
+ {"us.gcr.io", true},
+ {"eu.gcr.io", true},
+ {"asia.gcr.io", true},
+ {"staging-k8s.gcr.io", true},
+ {"global.gcr.io", true},
+ {"notgcr.io", false},
+ {"fake-gcr.io", false},
+ {"alsonot.gcr.iot", false},
+ // AR hosts
+ {"us-docker.pkg.dev", true},
+ {"asia-docker.pkg.dev", true},
+ {"europe-docker.pkg.dev", true},
+ {"us-central1-docker.pkg.dev", true},
+ {"us-docker-pkg.dev", false},
+ {"someotherpkg.dev", false},
+ {"looks-like-pkg.dev", false},
+ {"closeto.pkg.devops", false},
+ }
+
+ // Env should fail.
+ if err := os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/dev/null"); err != nil {
+ t.Fatalf("unexpected err os.Setenv: %v", err)
+ }
+
+ for i, tc := range cases {
+ t.Run(fmt.Sprintf("cases[%d]", i), func(t *testing.T) {
+ // Reset the keychain to ensure we don't cache earlier results.
+ Keychain = &googleKeychain{}
+
+ // Gcloud should succeed.
+ GetGcloudCmd = newGcloudCmdMock("success")
+
+ if auth, err := Keychain.Resolve(mustRegistry(tc.host)); err != nil {
+ t.Errorf("expected success for %v, got: %v", tc.host, err)
+ } else if tc.expectAuth && auth == authn.Anonymous {
+ t.Errorf("expected not anonymous auth for %v, got: %v", tc, auth)
+ } else if !tc.expectAuth && auth != authn.Anonymous {
+ t.Errorf("expected anonymous auth for %v, got: %v", tc, auth)
+ }
+
+ // Make gcloud fail to test that caching works.
+ GetGcloudCmd = newGcloudCmdMock("badoutput")
+
+ if auth, err := Keychain.Resolve(mustRegistry(tc.host)); err != nil {
+ t.Errorf("expected success for %v, got: %v", tc.host, err)
+ } else if tc.expectAuth && auth == authn.Anonymous {
+ t.Errorf("expected not anonymous auth for %v, got: %v", tc, auth)
+ } else if !tc.expectAuth && auth != authn.Anonymous {
+ t.Errorf("expected anonymous auth for %v, got: %v", tc, auth)
+ }
+ })
+ }
+}
+
+func TestKeychainError(t *testing.T) {
+ if err := os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/dev/null"); err != nil {
+ t.Fatalf("unexpected err os.Setenv: %v", err)
+ }
+
+ GetGcloudCmd = newGcloudCmdMock("badoutput")
+
+ // Reset the keychain to ensure we don't cache earlier results.
+ Keychain = &googleKeychain{}
+ if auth, err := Keychain.Resolve(mustRegistry("gcr.io")); err != nil {
+ t.Fatalf("got error: %v", err)
+ } else if auth != authn.Anonymous {
+ t.Fatalf("wanted Anonymous, got %v", auth)
+ }
+}
+
+type badSource struct{}
+
+func (bs badSource) Token() (*oauth2.Token, error) {
+ return nil, fmt.Errorf("oops")
+}
+
+// This test is silly, but coverage.
+func TestTokenSourceAuthError(t *testing.T) {
+ auth := tokenSourceAuth{badSource{}}
+
+ _, err := auth.Authorization()
+ if err == nil {
+ t.Errorf("expected err, got nil")
+ }
+}
+
+func TestNewEnvAuthenticatorFailure(t *testing.T) {
+ if err := os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/dev/null"); err != nil {
+ t.Fatalf("unexpected err os.Setenv: %v", err)
+ }
+
+ // Expect error.
+ _, err := NewEnvAuthenticator()
+ if err == nil {
+ t.Errorf("expected err, got nil")
+ }
+}
diff --git a/pkg/v1/google/doc.go b/pkg/v1/google/doc.go
new file mode 100644
index 0000000..b6a67df
--- /dev/null
+++ b/pkg/v1/google/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package google provides facilities for listing images in gcr.io.
+package google
diff --git a/pkg/v1/google/keychain.go b/pkg/v1/google/keychain.go
new file mode 100644
index 0000000..6dc7a50
--- /dev/null
+++ b/pkg/v1/google/keychain.go
@@ -0,0 +1,92 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google
+
+import (
+ "strings"
+ "sync"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/logs"
+)
+
+// Keychain exports an instance of the google Keychain.
+var Keychain authn.Keychain = &googleKeychain{}
+
+type googleKeychain struct {
+ once sync.Once
+ auth authn.Authenticator
+}
+
+// Resolve implements authn.Keychain a la docker-credential-gcr.
+//
+// This behaves similarly to the GCR credential helper, but reuses tokens until
+// they expire.
+//
+// We can't easily add this behavior to our credential helper implementation
+// of authn.Authenticator because the credential helper protocol doesn't include
+// expiration information, see here:
+// https://godoc.org/github.com/docker/docker-credential-helpers/credentials#Credentials
+//
+// In addition to being a performance optimization, the reuse of these access
+// tokens works around a bug in gcloud. It appears that attempting to invoke
+// `gcloud config config-helper` multiple times too quickly will fail:
+// https://github.com/GoogleCloudPlatform/docker-credential-gcr/issues/54
+//
+// We could upstream this behavior into docker-credential-gcr by parsing
+// gcloud's output and persisting its tokens across invocations, but then
+// we have to deal with invalidating caches across multiple runs (no fun).
+//
+// In general, we don't worry about that here because we expect to use the same
+// gcloud configuration in the scope of this one process.
+func (gk *googleKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
+ // Only authenticate GCR and AR so it works with authn.NewMultiKeychain to fallback.
+ host := target.RegistryStr()
+ if host != "gcr.io" &&
+ !strings.HasSuffix(host, ".gcr.io") &&
+ !strings.HasSuffix(host, ".pkg.dev") &&
+ !strings.HasSuffix(host, ".google.com") {
+ return authn.Anonymous, nil
+ }
+
+ gk.once.Do(func() {
+ gk.auth = resolve()
+ })
+
+ return gk.auth, nil
+}
+
+func resolve() authn.Authenticator {
+ auth, envErr := NewEnvAuthenticator()
+ if envErr == nil && auth != authn.Anonymous {
+ logs.Debug.Println("google.Keychain: using Application Default Credentials")
+ return auth
+ }
+
+ auth, gErr := NewGcloudAuthenticator()
+ if gErr == nil && auth != authn.Anonymous {
+ logs.Debug.Println("google.Keychain: using gcloud fallback")
+ return auth
+ }
+
+ logs.Debug.Println("Failed to get any Google credentials, falling back to Anonymous")
+ if envErr != nil {
+ logs.Debug.Printf("Google env error: %v", envErr)
+ }
+ if gErr != nil {
+ logs.Debug.Printf("gcloud error: %v", gErr)
+ }
+ return authn.Anonymous
+}
diff --git a/pkg/v1/google/list.go b/pkg/v1/google/list.go
new file mode 100644
index 0000000..a70bb27
--- /dev/null
+++ b/pkg/v1/google/list.go
@@ -0,0 +1,331 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+)
+
+// Option is a functional option for List and Walk.
+// TODO: Can we somehow reuse the remote options here?
+type Option func(*lister) error
+
+type lister struct {
+ auth authn.Authenticator
+ transport http.RoundTripper
+ repo name.Repository
+ client *http.Client
+ ctx context.Context
+ userAgent string
+}
+
+func newLister(repo name.Repository, options ...Option) (*lister, error) {
+ l := &lister{
+ auth: authn.Anonymous,
+ transport: http.DefaultTransport,
+ repo: repo,
+ ctx: context.Background(),
+ }
+
+ for _, option := range options {
+ if err := option(l); err != nil {
+ return nil, err
+ }
+ }
+
+ // transport.Wrapper is a signal that consumers are opt-ing into providing their own transport without any additional wrapping.
+ // This is to allow consumers full control over the transports logic, such as providing retry logic.
+ if _, ok := l.transport.(*transport.Wrapper); !ok {
+ // Wrap the transport in something that logs requests and responses.
+ // It's expensive to generate the dumps, so skip it if we're writing
+ // to nothing.
+ if logs.Enabled(logs.Debug) {
+ l.transport = transport.NewLogger(l.transport)
+ }
+
+ // Wrap the transport in something that can retry network flakes.
+ l.transport = transport.NewRetry(l.transport)
+
+ // Wrap this last to prevent transport.New from double-wrapping.
+ if l.userAgent != "" {
+ l.transport = transport.NewUserAgent(l.transport, l.userAgent)
+ }
+ }
+
+ scopes := []string{repo.Scope(transport.PullScope)}
+ tr, err := transport.NewWithContext(l.ctx, repo.Registry, l.auth, l.transport, scopes)
+ if err != nil {
+ return nil, err
+ }
+
+ l.client = &http.Client{Transport: tr}
+
+ return l, nil
+}
+
+func (l *lister) list(repo name.Repository) (*Tags, error) {
+ uri := &url.URL{
+ Scheme: repo.Registry.Scheme(),
+ Host: repo.Registry.RegistryStr(),
+ Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()),
+ // ECR returns an error if n > 1000:
+ // https://github.com/google/go-containerregistry/issues/681
+ RawQuery: "n=1000",
+ }
+
+ tags := Tags{}
+
+ // get responses until there is no next page
+ for {
+ select {
+ case <-l.ctx.Done():
+ return nil, l.ctx.Err()
+ default:
+ }
+
+ req, err := http.NewRequest("GET", uri.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(l.ctx)
+
+ resp, err := l.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ return nil, err
+ }
+
+ parsed := Tags{}
+ if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
+ return nil, err
+ }
+
+ if err := resp.Body.Close(); err != nil {
+ return nil, err
+ }
+
+ if len(parsed.Manifests) != 0 || len(parsed.Children) != 0 {
+ // We're dealing with GCR, just return directly.
+ return &parsed, nil
+ }
+
+ // This isn't GCR, just append the tags and keep paginating.
+ tags.Tags = append(tags.Tags, parsed.Tags...)
+
+ uri, err = getNextPageURL(resp)
+ if err != nil {
+ return nil, err
+ }
+ // no next page
+ if uri == nil {
+ break
+ }
+ logs.Warn.Printf("saw non-google tag listing response, falling back to pagination")
+ }
+
+ return &tags, nil
+}
+
+// getNextPageURL checks if there is a Link header in a http.Response which
+// contains a link to the next page. If yes it returns the url.URL of the next
+// page otherwise it returns nil.
+func getNextPageURL(resp *http.Response) (*url.URL, error) {
+ link := resp.Header.Get("Link")
+ if link == "" {
+ return nil, nil
+ }
+
+ if link[0] != '<' {
+ return nil, fmt.Errorf("failed to parse link header: missing '<' in: %s", link)
+ }
+
+ end := strings.Index(link, ">")
+ if end == -1 {
+ return nil, fmt.Errorf("failed to parse link header: missing '>' in: %s", link)
+ }
+ link = link[1:end]
+
+ linkURL, err := url.Parse(link)
+ if err != nil {
+ return nil, err
+ }
+ if resp.Request == nil || resp.Request.URL == nil {
+ return nil, nil
+ }
+ linkURL = resp.Request.URL.ResolveReference(linkURL)
+ return linkURL, nil
+}
+
+type rawManifestInfo struct {
+ Size string `json:"imageSizeBytes"`
+ MediaType string `json:"mediaType"`
+ Created string `json:"timeCreatedMs"`
+ Uploaded string `json:"timeUploadedMs"`
+ Tags []string `json:"tag"`
+}
+
+// ManifestInfo is a Manifests entry is the output of List and Walk.
+type ManifestInfo struct {
+ Size uint64 `json:"imageSizeBytes"`
+ MediaType string `json:"mediaType"`
+ Created time.Time `json:"timeCreatedMs"`
+ Uploaded time.Time `json:"timeUploadedMs"`
+ Tags []string `json:"tag"`
+}
+
+func fromUnixMs(ms int64) time.Time {
+ sec := ms / 1000
+ ns := (ms % 1000) * 1000000
+ return time.Unix(sec, ns)
+}
+
+func toUnixMs(t time.Time) string {
+ return strconv.FormatInt(t.UnixNano()/1000000, 10)
+}
+
+// MarshalJSON implements json.Marshaler
+func (m ManifestInfo) MarshalJSON() ([]byte, error) {
+ return json.Marshal(rawManifestInfo{
+ Size: strconv.FormatUint(m.Size, 10),
+ MediaType: m.MediaType,
+ Created: toUnixMs(m.Created),
+ Uploaded: toUnixMs(m.Uploaded),
+ Tags: m.Tags,
+ })
+}
+
+// UnmarshalJSON implements json.Unmarshaler
+func (m *ManifestInfo) UnmarshalJSON(data []byte) error {
+ raw := rawManifestInfo{}
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ if raw.Size != "" {
+ size, err := strconv.ParseUint(raw.Size, 10, 64)
+ if err != nil {
+ return err
+ }
+ m.Size = size
+ }
+
+ if raw.Created != "" {
+ created, err := strconv.ParseInt(raw.Created, 10, 64)
+ if err != nil {
+ return err
+ }
+ m.Created = fromUnixMs(created)
+ }
+
+ if raw.Uploaded != "" {
+ uploaded, err := strconv.ParseInt(raw.Uploaded, 10, 64)
+ if err != nil {
+ return err
+ }
+ m.Uploaded = fromUnixMs(uploaded)
+ }
+
+ m.MediaType = raw.MediaType
+ m.Tags = raw.Tags
+
+ return nil
+}
+
+// Tags is the result of List and Walk.
+type Tags struct {
+ Children []string `json:"child"`
+ Manifests map[string]ManifestInfo `json:"manifest"`
+ Name string `json:"name"`
+ Tags []string `json:"tags"`
+}
+
+// List calls /tags/list for the given repository.
+func List(repo name.Repository, options ...Option) (*Tags, error) {
+ l, err := newLister(repo, options...)
+ if err != nil {
+ return nil, err
+ }
+
+ return l.list(repo)
+}
+
+// WalkFunc is the type of the function called for each repository visited by
+// Walk. This implements a similar API to filepath.Walk.
+//
+// The repo argument contains the argument to Walk as a prefix; that is, if Walk
+// is called with "gcr.io/foo", which is a repository containing the repository
+// "bar", the walk function will be called with argument "gcr.io/foo/bar".
+// The tags and error arguments are the result of calling List on repo.
+//
+// TODO: Do we want a SkipDir error, as in filepath.WalkFunc?
+type WalkFunc func(repo name.Repository, tags *Tags, err error) error
+
+func walk(repo name.Repository, tags *Tags, walkFn WalkFunc, options ...Option) error {
+ if tags == nil {
+ // This shouldn't happen.
+ return fmt.Errorf("tags nil for %q", repo)
+ }
+
+ if err := walkFn(repo, tags, nil); err != nil {
+ return err
+ }
+
+ for _, path := range tags.Children {
+ child, err := name.NewRepository(fmt.Sprintf("%s/%s", repo, path), name.StrictValidation)
+ if err != nil {
+ // We don't expect this ever, so don't pass it through to walkFn.
+ return fmt.Errorf("unexpected path failure: %w", err)
+ }
+
+ childTags, err := List(child, options...)
+ if err != nil {
+ if err := walkFn(child, nil, err); err != nil {
+ return err
+ }
+ } else {
+ if err := walk(child, childTags, walkFn, options...); err != nil {
+ return err
+ }
+ }
+ }
+
+ // We made it!
+ return nil
+}
+
+// Walk recursively descends repositories, calling walkFn.
+func Walk(root name.Repository, walkFn WalkFunc, options ...Option) error {
+ tags, err := List(root, options...)
+ if err != nil {
+ return walkFn(root, nil, err)
+ }
+
+ return walk(root, tags, walkFn, options...)
+}
diff --git a/pkg/v1/google/list_test.go b/pkg/v1/google/list_test.go
new file mode 100644
index 0000000..5718526
--- /dev/null
+++ b/pkg/v1/google/list_test.go
@@ -0,0 +1,339 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+func mustParseDuration(t *testing.T, d string) time.Duration {
+ dur, err := time.ParseDuration(d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return dur
+}
+
+func TestRoundtrip(t *testing.T) {
+ raw := rawManifestInfo{
+ Size: "100",
+ MediaType: "hi",
+ Created: "12345678",
+ Uploaded: "23456789",
+ Tags: []string{"latest"},
+ }
+
+ og, err := json.Marshal(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ parsed := ManifestInfo{}
+ if err := json.Unmarshal(og, &parsed); err != nil {
+ t.Fatal(err)
+ }
+
+ roundtripped, err := json.Marshal(parsed)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := cmp.Diff(og, roundtripped); diff != "" {
+ t.Errorf("ManifestInfo can't roundtrip: (-want +got) = %s", diff)
+ }
+}
+
+func TestList(t *testing.T) {
+ cases := []struct {
+ name string
+ responseBody []byte
+ wantErr bool
+ wantTags *Tags
+ }{{
+ name: "success",
+ responseBody: []byte(`{"tags":["foo","bar"]}`),
+ wantErr: false,
+ wantTags: &Tags{Tags: []string{"foo", "bar"}},
+ }, {
+ name: "gcr success",
+ responseBody: []byte(`{"child":["hello", "world"],"manifest":{"digest1":{"imageSizeBytes":"1","mediaType":"mainstream","timeCreatedms":"1","timeUploadedMs":"2","tag":["foo"]},"digest2":{"imageSizeBytes":"2","mediaType":"indie","timeCreatedMs":"3","timeUploadedMs":"4","tag":["bar","baz"]}},"tags":["foo","bar","baz"]}`),
+ wantErr: false,
+ wantTags: &Tags{
+ Children: []string{"hello", "world"},
+ Manifests: map[string]ManifestInfo{
+ "digest1": {
+ Size: 1,
+ MediaType: "mainstream",
+ Created: time.Unix(0, 0).Add(mustParseDuration(t, "1ms")),
+ Uploaded: time.Unix(0, 0).Add(mustParseDuration(t, "2ms")),
+ Tags: []string{"foo"},
+ },
+ "digest2": {
+ Size: 2,
+ MediaType: "indie",
+ Created: time.Unix(0, 0).Add(mustParseDuration(t, "3ms")),
+ Uploaded: time.Unix(0, 0).Add(mustParseDuration(t, "4ms")),
+ Tags: []string{"bar", "baz"},
+ },
+ },
+ Tags: []string{"foo", "bar", "baz"},
+ },
+ }, {
+ name: "just children",
+ responseBody: []byte(`{"child":["hello", "world"]}`),
+ wantErr: false,
+ wantTags: &Tags{
+ Children: []string{"hello", "world"},
+ },
+ }, {
+ name: "not json",
+ responseBody: []byte("notjson"),
+ wantErr: true,
+ }}
+
+ repoName := "ubuntu"
+ // To test WithUserAgent
+ uaSentinel := "this-is-the-user-agent"
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ tagsPath := fmt.Sprintf("/v2/%s/tags/list", repoName)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if got, want := r.Header.Get("User-Agent"), uaSentinel; !strings.Contains(got, want) {
+ t.Errorf("request did not container useragent, got %q want Contains(%q)", got, want)
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case tagsPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+
+ w.Write(tc.responseBody)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewRepository(%v) = %v", repoName, err)
+ }
+
+ tags, err := List(repo, WithAuthFromKeychain(authn.DefaultKeychain), WithTransport(http.DefaultTransport), WithUserAgent(uaSentinel), WithContext(context.Background()))
+ if (err != nil) != tc.wantErr {
+ t.Errorf("List() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err)
+ }
+
+ if diff := cmp.Diff(tc.wantTags, tags); diff != "" {
+ t.Errorf("List() wrong tags (-want +got) = %s", diff)
+ }
+ })
+ }
+}
+
+type recorder struct {
+ Tags []*Tags
+ Errs []error
+}
+
+func (r *recorder) walk(repo name.Repository, tags *Tags, err error) error {
+ r.Tags = append(r.Tags, tags)
+ r.Errs = append(r.Errs, err)
+
+ return nil
+}
+
+func TestWalk(t *testing.T) {
+ // Stupid coverage to make sure it doesn't panic.
+ var b bytes.Buffer
+ logs.Debug.SetOutput(&b)
+
+ cases := []struct {
+ name string
+ responseBody []byte
+ wantResult recorder
+ }{{
+ name: "gcr success",
+ responseBody: []byte(`{"child":["hello", "world"],"manifest":{"digest1":{"imageSizeBytes":"1","mediaType":"mainstream","timeCreatedms":"1","timeUploadedMs":"2","tag":["foo"]},"digest2":{"imageSizeBytes":"2","mediaType":"indie","timeCreatedMs":"3","timeUploadedMs":"4","tag":["bar","baz"]}},"tags":["foo","bar","baz"]}`),
+ wantResult: recorder{
+ Tags: []*Tags{{
+ Children: []string{"hello", "world"},
+ Manifests: map[string]ManifestInfo{
+ "digest1": {
+ Size: 1,
+ MediaType: "mainstream",
+ Created: time.Unix(0, 0).Add(mustParseDuration(t, "1ms")),
+ Uploaded: time.Unix(0, 0).Add(mustParseDuration(t, "2ms")),
+ Tags: []string{"foo"},
+ },
+ "digest2": {
+ Size: 2,
+ MediaType: "indie",
+ Created: time.Unix(0, 0).Add(mustParseDuration(t, "3ms")),
+ Uploaded: time.Unix(0, 0).Add(mustParseDuration(t, "4ms")),
+ Tags: []string{"bar", "baz"},
+ },
+ },
+ Tags: []string{"foo", "bar", "baz"},
+ }, {
+ Tags: []string{"hello"},
+ }, {
+ Tags: []string{"world"},
+ }},
+ Errs: []error{nil, nil, nil},
+ },
+ }}
+
+ repoName := "ubuntu"
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ rootPath := fmt.Sprintf("/v2/%s/tags/list", repoName)
+ helloPath := fmt.Sprintf("/v2/%s/hello/tags/list", repoName)
+ worldPath := fmt.Sprintf("/v2/%s/world/tags/list", repoName)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case rootPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+
+ w.Write(tc.responseBody)
+ case helloPath:
+ w.Write([]byte(`{"tags":["hello"]}`))
+ case worldPath:
+ w.Write([]byte(`{"tags":["world"]}`))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewRepository(%v) = %v", repoName, err)
+ }
+
+ r := recorder{}
+ if err := Walk(repo, r.walk, WithAuth(authn.Anonymous)); err != nil {
+ t.Errorf("unexpected err: %v", err)
+ }
+
+ if diff := cmp.Diff(tc.wantResult, r); diff != "" {
+ t.Errorf("Walk() wrong tags (-want +got) = %s", diff)
+ }
+ })
+ }
+}
+
+// Copied shamelessly from remote.
+func TestCancelledList(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+
+ repoName := "doesnotmatter"
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewRepository(%v) = %v", repoName, err)
+ }
+
+ _, err = List(repo, WithContext(ctx))
+ if !strings.Contains(err.Error(), context.Canceled.Error()) {
+ t.Errorf("wanted %q to contain %q", err.Error(), context.Canceled.Error())
+ }
+}
+
+func makeResp(hdr string) *http.Response {
+ return &http.Response{
+ Header: http.Header{
+ "Link": []string{hdr},
+ },
+ }
+}
+
+func TestGetNextPageURL(t *testing.T) {
+ for _, hdr := range []string{
+ "",
+ "<",
+ "><",
+ "<>",
+ fmt.Sprintf("<%c>", 0x7f), // makes url.Parse fail
+ } {
+ u, err := getNextPageURL(makeResp(hdr))
+ if err == nil && u != nil {
+ t.Errorf("Expected err, got %+v", u)
+ }
+ }
+
+ good := &http.Response{
+ Header: http.Header{
+ "Link": []string{"<example.com>"},
+ },
+ Request: &http.Request{
+ URL: &url.URL{
+ Scheme: "https",
+ },
+ },
+ }
+ u, err := getNextPageURL(good)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if u.Scheme != "https" {
+ t.Errorf("expected scheme to match request, got %s", u.Scheme)
+ }
+}
diff --git a/pkg/v1/google/options.go b/pkg/v1/google/options.go
new file mode 100644
index 0000000..604808c
--- /dev/null
+++ b/pkg/v1/google/options.go
@@ -0,0 +1,73 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+)
+
+// WithTransport is a functional option for overriding the default transport
+// on a remote image
+func WithTransport(t http.RoundTripper) Option {
+ return func(l *lister) error {
+ l.transport = t
+ return nil
+ }
+}
+
+// WithAuth is a functional option for overriding the default authenticator
+// on a remote image
+func WithAuth(auth authn.Authenticator) Option {
+ return func(l *lister) error {
+ l.auth = auth
+ return nil
+ }
+}
+
+// WithAuthFromKeychain is a functional option for overriding the default
+// authenticator on a remote image using an authn.Keychain
+func WithAuthFromKeychain(keys authn.Keychain) Option {
+ return func(l *lister) error {
+ auth, err := keys.Resolve(l.repo.Registry)
+ if err != nil {
+ return err
+ }
+ l.auth = auth
+ return nil
+ }
+}
+
+// WithContext is a functional option for overriding the default
+// context.Context for HTTP request to list remote images
+func WithContext(ctx context.Context) Option {
+ return func(l *lister) error {
+ l.ctx = ctx
+ return nil
+ }
+}
+
+// WithUserAgent adds the given string to the User-Agent header for any HTTP
+// requests. This header will also include "go-containerregistry/${version}".
+//
+// If you want to completely overwrite the User-Agent header, use WithTransport.
+func WithUserAgent(ua string) Option {
+ return func(l *lister) error {
+ l.userAgent = ua
+ return nil
+ }
+}
diff --git a/pkg/v1/google/testdata/README.md b/pkg/v1/google/testdata/README.md
new file mode 100644
index 0000000..12222aa
--- /dev/null
+++ b/pkg/v1/google/testdata/README.md
@@ -0,0 +1,4 @@
+# testdata
+
+This key is cribbed from [here](https://github.com/golang/oauth2/blob/d668ce993890a79bda886613ee587a69dd5da7a6/google/testdata/gcloud/credentials).
+It's invalid but parses sufficiently to test `NewEnvAuthenticator`.
diff --git a/pkg/v1/google/testdata/key.json b/pkg/v1/google/testdata/key.json
new file mode 100644
index 0000000..c2d23ce
--- /dev/null
+++ b/pkg/v1/google/testdata/key.json
@@ -0,0 +1,35 @@
+{
+ "_class": "OAuth2Credentials",
+ "_module": "oauth2client.client",
+ "access_token": "foo_access_token",
+ "client_id": "foo_client_id",
+ "client_secret": "foo_client_secret",
+ "id_token": {
+ "at_hash": "foo_at_hash",
+ "aud": "foo_aud",
+ "azp": "foo_azp",
+ "cid": "foo_cid",
+ "email": "foo@example.com",
+ "email_verified": true,
+ "exp": 1420573614,
+ "iat": 1420569714,
+ "id": "1337",
+ "iss": "accounts.google.com",
+ "sub": "1337",
+ "token_hash": "foo_token_hash",
+ "verified_email": true
+ },
+ "invalid": false,
+ "refresh_token": "foo_refresh_token",
+ "revoke_uri": "https://accounts.google.com/o/oauth2/revoke",
+ "token_expiry": "3015-01-09T00:51:51Z",
+ "token_response": {
+ "access_token": "foo_access_token",
+ "expires_in": 3600,
+ "id_token": "foo_id_token",
+ "token_type": "Bearer"
+ },
+ "token_uri": "https://accounts.google.com/o/oauth2/token",
+ "user_agent": "Cloud SDK Command Line Tool",
+ "type": "authorized_user"
+}
diff --git a/pkg/v1/hash.go b/pkg/v1/hash.go
new file mode 100644
index 0000000..f78a5fa
--- /dev/null
+++ b/pkg/v1/hash.go
@@ -0,0 +1,123 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "crypto"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "hash"
+ "io"
+ "strconv"
+ "strings"
+)
+
+// Hash is an unqualified digest of some content, e.g. sha256:deadbeef
+type Hash struct {
+ // Algorithm holds the algorithm used to compute the hash.
+ Algorithm string
+
+ // Hex holds the hex portion of the content hash.
+ Hex string
+}
+
+// String reverses NewHash returning the string-form of the hash.
+func (h Hash) String() string {
+ return fmt.Sprintf("%s:%s", h.Algorithm, h.Hex)
+}
+
+// NewHash validates the input string is a hash and returns a strongly type Hash object.
+func NewHash(s string) (Hash, error) {
+ h := Hash{}
+ if err := h.parse(s); err != nil {
+ return Hash{}, err
+ }
+ return h, nil
+}
+
+// MarshalJSON implements json.Marshaler
+func (h Hash) MarshalJSON() ([]byte, error) {
+ return json.Marshal(h.String())
+}
+
+// UnmarshalJSON implements json.Unmarshaler
+func (h *Hash) UnmarshalJSON(data []byte) error {
+ s, err := strconv.Unquote(string(data))
+ if err != nil {
+ return err
+ }
+ return h.parse(s)
+}
+
+// MarshalText implements encoding.TextMarshaler. This is required to use
+// v1.Hash as a key in a map when marshalling JSON.
+func (h Hash) MarshalText() (text []byte, err error) {
+ return []byte(h.String()), nil
+}
+
+// UnmarshalText implements encoding.TextUnmarshaler. This is required to use
+// v1.Hash as a key in a map when unmarshalling JSON.
+func (h *Hash) UnmarshalText(text []byte) error {
+ return h.parse(string(text))
+}
+
+// Hasher returns a hash.Hash for the named algorithm (e.g. "sha256")
+func Hasher(name string) (hash.Hash, error) {
+ switch name {
+ case "sha256":
+ return crypto.SHA256.New(), nil
+ default:
+ return nil, fmt.Errorf("unsupported hash: %q", name)
+ }
+}
+
+func (h *Hash) parse(unquoted string) error {
+ parts := strings.Split(unquoted, ":")
+ if len(parts) != 2 {
+ return fmt.Errorf("cannot parse hash: %q", unquoted)
+ }
+
+ rest := strings.TrimLeft(parts[1], "0123456789abcdef")
+ if len(rest) != 0 {
+ return fmt.Errorf("found non-hex character in hash: %c", rest[0])
+ }
+
+ hasher, err := Hasher(parts[0])
+ if err != nil {
+ return err
+ }
+ // Compare the hex to the expected size (2 hex characters per byte)
+ if len(parts[1]) != hasher.Size()*2 {
+ return fmt.Errorf("wrong number of hex digits for %s: %s", parts[0], parts[1])
+ }
+
+ h.Algorithm = parts[0]
+ h.Hex = parts[1]
+ return nil
+}
+
+// SHA256 computes the Hash of the provided io.Reader's content.
+func SHA256(r io.Reader) (Hash, int64, error) {
+ hasher := crypto.SHA256.New()
+ n, err := io.Copy(hasher, r)
+ if err != nil {
+ return Hash{}, 0, err
+ }
+ return Hash{
+ Algorithm: "sha256",
+ Hex: hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))),
+ }, n, nil
+}
diff --git a/pkg/v1/hash_test.go b/pkg/v1/hash_test.go
new file mode 100644
index 0000000..df1be77
--- /dev/null
+++ b/pkg/v1/hash_test.go
@@ -0,0 +1,115 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "encoding/json"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+func TestGoodHashes(t *testing.T) {
+ good := []string{
+ "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ }
+
+ for _, s := range good {
+ h, err := NewHash(s)
+ if err != nil {
+ t.Error("Unexpected error parsing hash:", err)
+ }
+ if got, want := h.String(), s; got != want {
+ t.Errorf("String(); got %q, want %q", got, want)
+ }
+ bytes, err := json.Marshal(h)
+ if err != nil {
+ t.Error("Unexpected error json.Marshaling hash:", err)
+ }
+ if got, want := string(bytes), strconv.Quote(h.String()); got != want {
+ t.Errorf("json.Marshal(); got %q, want %q", got, want)
+ }
+ }
+}
+
+func TestBadHashes(t *testing.T) {
+ bad := []string{
+ // Too short
+ "sha256:deadbeef",
+ // Bad character
+ "sha256:o123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ // Unknown algorithm
+ "md5:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ // Too few parts
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ // Too many parts
+ "md5:sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ }
+
+ for _, s := range bad {
+ h, err := NewHash(s)
+ if err == nil {
+ t.Error("Expected error, got:", h)
+ }
+ }
+}
+
+func TestSHA256(t *testing.T) {
+ input := "asdf"
+ h, n, err := SHA256(strings.NewReader(input))
+ if err != nil {
+ t.Error("SHA256(asdf) =", err)
+ }
+ if got, want := h.Algorithm, "sha256"; got != want {
+ t.Errorf("Algorithm; got %v, want %v", got, want)
+ }
+ if got, want := h.Hex, "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b"; got != want {
+ t.Errorf("Hex; got %v, want %v", got, want)
+ }
+ if got, want := n, int64(len(input)); got != want {
+ t.Errorf("n; got %v, want %v", got, want)
+ }
+}
+
+// This tests that you can use Hash as a key in a map (needs to implement both
+// MarshalText and UnmarshalText).
+func TestTextMarshalling(t *testing.T) {
+ foo := make(map[Hash]string)
+ b, err := json.Marshal(foo)
+ if err != nil {
+ t.Fatal("could not marshal:", err)
+ }
+ if err := json.Unmarshal(b, &foo); err != nil {
+ t.Error("could not unmarshal:", err)
+ }
+
+ h := &Hash{
+ Algorithm: "sha256",
+ Hex: strings.Repeat("a", 64),
+ }
+ g := &Hash{}
+ text, err := h.MarshalText()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := g.UnmarshalText(text); err != nil {
+ t.Fatal(err)
+ }
+
+ if h.String() != g.String() {
+ t.Errorf("mismatched hash: %s != %s", h, g)
+ }
+}
diff --git a/pkg/v1/image.go b/pkg/v1/image.go
new file mode 100644
index 0000000..8de9e47
--- /dev/null
+++ b/pkg/v1/image.go
@@ -0,0 +1,59 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Image defines the interface for interacting with an OCI v1 image.
+type Image interface {
+ // Layers returns the ordered collection of filesystem layers that comprise this image.
+ // The order of the list is oldest/base layer first, and most-recent/top layer last.
+ Layers() ([]Layer, error)
+
+ // MediaType of this image's manifest.
+ MediaType() (types.MediaType, error)
+
+ // Size returns the size of the manifest.
+ Size() (int64, error)
+
+ // ConfigName returns the hash of the image's config file, also known as
+ // the Image ID.
+ ConfigName() (Hash, error)
+
+ // ConfigFile returns this image's config file.
+ ConfigFile() (*ConfigFile, error)
+
+ // RawConfigFile returns the serialized bytes of ConfigFile().
+ RawConfigFile() ([]byte, error)
+
+ // Digest returns the sha256 of this image's manifest.
+ Digest() (Hash, error)
+
+ // Manifest returns this image's Manifest object.
+ Manifest() (*Manifest, error)
+
+ // RawManifest returns the serialized bytes of Manifest()
+ RawManifest() ([]byte, error)
+
+ // LayerByDigest returns a Layer for interacting with a particular layer of
+ // the image, looking it up by "digest" (the compressed hash).
+ LayerByDigest(Hash) (Layer, error)
+
+ // LayerByDiffID is an analog to LayerByDigest, looking up by "diff id"
+ // (the uncompressed hash).
+ LayerByDiffID(Hash) (Layer, error)
+}
diff --git a/pkg/v1/index.go b/pkg/v1/index.go
new file mode 100644
index 0000000..8e7bc8e
--- /dev/null
+++ b/pkg/v1/index.go
@@ -0,0 +1,43 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// ImageIndex defines the interface for interacting with an OCI image index.
+type ImageIndex interface {
+ // MediaType of this image's manifest.
+ MediaType() (types.MediaType, error)
+
+ // Digest returns the sha256 of this index's manifest.
+ Digest() (Hash, error)
+
+ // Size returns the size of the manifest.
+ Size() (int64, error)
+
+ // IndexManifest returns this image index's manifest object.
+ IndexManifest() (*IndexManifest, error)
+
+ // RawManifest returns the serialized bytes of IndexManifest().
+ RawManifest() ([]byte, error)
+
+ // Image returns a v1.Image that this ImageIndex references.
+ Image(Hash) (Image, error)
+
+ // ImageIndex returns a v1.ImageIndex that this ImageIndex references.
+ ImageIndex(Hash) (ImageIndex, error)
+}
diff --git a/pkg/v1/layer.go b/pkg/v1/layer.go
new file mode 100644
index 0000000..57447d2
--- /dev/null
+++ b/pkg/v1/layer.go
@@ -0,0 +1,42 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "io"
+
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Layer is an interface for accessing the properties of a particular layer of a v1.Image
+type Layer interface {
+ // Digest returns the Hash of the compressed layer.
+ Digest() (Hash, error)
+
+ // DiffID returns the Hash of the uncompressed layer.
+ DiffID() (Hash, error)
+
+ // Compressed returns an io.ReadCloser for the compressed layer contents.
+ Compressed() (io.ReadCloser, error)
+
+ // Uncompressed returns an io.ReadCloser for the uncompressed layer contents.
+ Uncompressed() (io.ReadCloser, error)
+
+ // Size returns the compressed size of the Layer.
+ Size() (int64, error)
+
+ // MediaType returns the media type of the Layer.
+ MediaType() (types.MediaType, error)
+}
diff --git a/pkg/v1/layout/README.md b/pkg/v1/layout/README.md
new file mode 100644
index 0000000..54bee6d
--- /dev/null
+++ b/pkg/v1/layout/README.md
@@ -0,0 +1,5 @@
+# `layout`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout)
+
+The `layout` package implements support for interacting with an [OCI Image Layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md).
diff --git a/pkg/v1/layout/blob.go b/pkg/v1/layout/blob.go
new file mode 100644
index 0000000..2e5f435
--- /dev/null
+++ b/pkg/v1/layout/blob.go
@@ -0,0 +1,37 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+ "io"
+ "os"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// Blob returns a blob with the given hash from the Path.
+func (l Path) Blob(h v1.Hash) (io.ReadCloser, error) {
+ return os.Open(l.blobPath(h))
+}
+
+// Bytes is a convenience function to return a blob from the Path as
+// a byte slice.
+func (l Path) Bytes(h v1.Hash) ([]byte, error) {
+ return os.ReadFile(l.blobPath(h))
+}
+
+func (l Path) blobPath(h v1.Hash) string {
+ return l.path("blobs", h.Algorithm, h.Hex)
+}
diff --git a/pkg/v1/layout/doc.go b/pkg/v1/layout/doc.go
new file mode 100644
index 0000000..d80d273
--- /dev/null
+++ b/pkg/v1/layout/doc.go
@@ -0,0 +1,19 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package layout provides facilities for reading/writing artifacts from/to
+// an OCI image layout on disk, see:
+//
+// https://github.com/opencontainers/image-spec/blob/master/image-layout.md
+package layout
diff --git a/pkg/v1/layout/image.go b/pkg/v1/layout/image.go
new file mode 100644
index 0000000..c9ae966
--- /dev/null
+++ b/pkg/v1/layout/image.go
@@ -0,0 +1,139 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "sync"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type layoutImage struct {
+ path Path
+ desc v1.Descriptor
+ manifestLock sync.Mutex // Protects rawManifest
+ rawManifest []byte
+}
+
+var _ partial.CompressedImageCore = (*layoutImage)(nil)
+
+// Image reads a v1.Image with digest h from the Path.
+func (l Path) Image(h v1.Hash) (v1.Image, error) {
+ ii, err := l.ImageIndex()
+ if err != nil {
+ return nil, err
+ }
+
+ return ii.Image(h)
+}
+
+func (li *layoutImage) MediaType() (types.MediaType, error) {
+ return li.desc.MediaType, nil
+}
+
+// Implements WithManifest for partial.Blobset.
+func (li *layoutImage) Manifest() (*v1.Manifest, error) {
+ return partial.Manifest(li)
+}
+
+func (li *layoutImage) RawManifest() ([]byte, error) {
+ li.manifestLock.Lock()
+ defer li.manifestLock.Unlock()
+ if li.rawManifest != nil {
+ return li.rawManifest, nil
+ }
+
+ b, err := li.path.Bytes(li.desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+
+ li.rawManifest = b
+ return li.rawManifest, nil
+}
+
+func (li *layoutImage) RawConfigFile() ([]byte, error) {
+ manifest, err := li.Manifest()
+ if err != nil {
+ return nil, err
+ }
+
+ return li.path.Bytes(manifest.Config.Digest)
+}
+
+func (li *layoutImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
+ manifest, err := li.Manifest()
+ if err != nil {
+ return nil, err
+ }
+
+ if h == manifest.Config.Digest {
+ return &compressedBlob{
+ path: li.path,
+ desc: manifest.Config,
+ }, nil
+ }
+
+ for _, desc := range manifest.Layers {
+ if h == desc.Digest {
+ return &compressedBlob{
+ path: li.path,
+ desc: desc,
+ }, nil
+ }
+ }
+
+ return nil, fmt.Errorf("could not find layer in image: %s", h)
+}
+
+type compressedBlob struct {
+ path Path
+ desc v1.Descriptor
+}
+
+func (b *compressedBlob) Digest() (v1.Hash, error) {
+ return b.desc.Digest, nil
+}
+
+func (b *compressedBlob) Compressed() (io.ReadCloser, error) {
+ return b.path.Blob(b.desc.Digest)
+}
+
+func (b *compressedBlob) Size() (int64, error) {
+ return b.desc.Size, nil
+}
+
+func (b *compressedBlob) MediaType() (types.MediaType, error) {
+ return b.desc.MediaType, nil
+}
+
+// Descriptor implements partial.withDescriptor.
+func (b *compressedBlob) Descriptor() (*v1.Descriptor, error) {
+ return &b.desc, nil
+}
+
+// See partial.Exists.
+func (b *compressedBlob) Exists() (bool, error) {
+ _, err := os.Stat(b.path.blobPath(b.desc.Digest))
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return err == nil, err
+}
diff --git a/pkg/v1/layout/image_test.go b/pkg/v1/layout/image_test.go
new file mode 100644
index 0000000..3614920
--- /dev/null
+++ b/pkg/v1/layout/image_test.go
@@ -0,0 +1,181 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+ "path/filepath"
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+var (
+ indexDigest = v1.Hash{
+ Algorithm: "sha256",
+ Hex: "05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5",
+ }
+ manifestDigest = v1.Hash{
+ Algorithm: "sha256",
+ Hex: "eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650",
+ }
+ configDigest = v1.Hash{
+ Algorithm: "sha256",
+ Hex: "6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e",
+ }
+ bogusDigest = v1.Hash{
+ Algorithm: "sha256",
+ Hex: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ }
+ customManifestDigest = v1.Hash{
+ Algorithm: "sha256",
+ Hex: "b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e",
+ }
+ bogusPath = filepath.Join("testdata", "does_not_exist")
+ testPath = filepath.Join("testdata", "test_index")
+ testPathOneImage = filepath.Join("testdata", "test_index_one_image")
+ testPathMediaType = filepath.Join("testdata", "test_index_media_type")
+ customMediaType types.MediaType = "application/tar+gzip"
+)
+
+func TestImage(t *testing.T) {
+ lp, err := FromPath(testPath)
+ if err != nil {
+ t.Fatalf("FromPath() = %v", err)
+ }
+ img, err := lp.Image(manifestDigest)
+ if err != nil {
+ t.Fatalf("Image() = %v", err)
+ }
+
+ if err := validate.Image(img); err != nil {
+ t.Errorf("validate.Image() = %v", err)
+ }
+
+ mt, err := img.MediaType()
+ if err != nil {
+ t.Errorf("MediaType() = %v", err)
+ } else if got, want := mt, types.OCIManifestSchema1; got != want {
+ t.Errorf("MediaType(); want: %v got: %v", want, got)
+ }
+
+ cfg, err := img.LayerByDigest(configDigest)
+ if err != nil {
+ t.Fatalf("LayerByDigest(%s) = %v", configDigest, err)
+ }
+
+ cfgName, err := img.ConfigName()
+ if err != nil {
+ t.Fatalf("ConfigName() = %v", err)
+ }
+
+ cfgDigest, err := cfg.Digest()
+ if err != nil {
+ t.Fatalf("cfg.Digest() = %v", err)
+ }
+
+ if got, want := cfgDigest, cfgName; got != want {
+ t.Errorf("ConfigName(); want: %v got: %v", want, got)
+ }
+
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatalf("img.Layers() = %v", err)
+ }
+
+ mediaType, err := layers[0].MediaType()
+ if err != nil {
+ t.Fatalf("img.Layers() = %v", err)
+ }
+
+ // Fixture is a DockerLayer
+ if got, want := mediaType, types.DockerLayer; got != want {
+ t.Fatalf("MediaType(); want: %q got: %q", want, got)
+ }
+
+ if ok, err := partial.Exists(layers[0]); err != nil {
+ t.Fatal(err)
+ } else if got, want := ok, true; got != want {
+ t.Errorf("Exists() = %t != %t", got, want)
+ }
+}
+
+func TestImageWithEmptyHash(t *testing.T) {
+ lp, err := FromPath(testPathOneImage)
+ if err != nil {
+ t.Fatalf("FromPath() = %v", err)
+ }
+ img, err := lp.Image(v1.Hash{})
+ if err != nil {
+ t.Fatalf("Image() = %v", err)
+ }
+
+ if err := validate.Image(img); err != nil {
+ t.Errorf("validate.Image() = %v", err)
+ }
+}
+
+func TestImageErrors(t *testing.T) {
+ lp, err := FromPath(testPath)
+ if err != nil {
+ t.Fatalf("FromPath() = %v", err)
+ }
+ img, err := lp.Image(manifestDigest)
+ if err != nil {
+ t.Fatalf("Image() = %v", err)
+ }
+
+ if _, err := img.LayerByDigest(bogusDigest); err == nil {
+ t.Errorf("LayerByDigest(%s) = nil, expected err", bogusDigest)
+ }
+
+ if _, err := lp.Image(bogusDigest); err == nil {
+ t.Errorf("Image(%s) = nil, expected err", bogusDigest)
+ }
+
+ if _, err := lp.Image(bogusDigest); err == nil {
+ t.Errorf("Image(%s, %s) = nil, expected err", bogusPath, bogusDigest)
+ }
+}
+
+func TestImageCustomMediaType(t *testing.T) {
+ lp, err := FromPath(testPathMediaType)
+ if err != nil {
+ t.Fatalf("FromPath() = %v", err)
+ }
+ img, err := lp.Image(customManifestDigest)
+ if err != nil {
+ t.Fatalf("Image() = %v", err)
+ }
+ mt, err := img.MediaType()
+ if err != nil {
+ t.Errorf("MediaType() = %v", err)
+ } else if got, want := mt, types.OCIManifestSchema1; got != want {
+ t.Errorf("MediaType(); want: %v got: %v", want, got)
+ }
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatalf("img.Layers() = %v", err)
+ }
+ mediaType, err := layers[0].MediaType()
+ if err != nil {
+ t.Fatalf("img.Layers() = %v", err)
+ }
+ if got, want := mediaType, customMediaType; got != want {
+ t.Fatalf("MediaType(); want: %q got: %q", want, got)
+ }
+}
diff --git a/pkg/v1/layout/index.go b/pkg/v1/layout/index.go
new file mode 100644
index 0000000..7404f18
--- /dev/null
+++ b/pkg/v1/layout/index.go
@@ -0,0 +1,161 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+var _ v1.ImageIndex = (*layoutIndex)(nil)
+
+type layoutIndex struct {
+ mediaType types.MediaType
+ path Path
+ rawIndex []byte
+}
+
+// ImageIndexFromPath is a convenience function which constructs a Path and returns its v1.ImageIndex.
+func ImageIndexFromPath(path string) (v1.ImageIndex, error) {
+ lp, err := FromPath(path)
+ if err != nil {
+ return nil, err
+ }
+ return lp.ImageIndex()
+}
+
+// ImageIndex returns a v1.ImageIndex for the Path.
+func (l Path) ImageIndex() (v1.ImageIndex, error) {
+ rawIndex, err := os.ReadFile(l.path("index.json"))
+ if err != nil {
+ return nil, err
+ }
+
+ idx := &layoutIndex{
+ mediaType: types.OCIImageIndex,
+ path: l,
+ rawIndex: rawIndex,
+ }
+
+ return idx, nil
+}
+
+func (i *layoutIndex) MediaType() (types.MediaType, error) {
+ return i.mediaType, nil
+}
+
+func (i *layoutIndex) Digest() (v1.Hash, error) {
+ return partial.Digest(i)
+}
+
+func (i *layoutIndex) Size() (int64, error) {
+ return partial.Size(i)
+}
+
+func (i *layoutIndex) IndexManifest() (*v1.IndexManifest, error) {
+ var index v1.IndexManifest
+ err := json.Unmarshal(i.rawIndex, &index)
+ return &index, err
+}
+
+func (i *layoutIndex) RawManifest() ([]byte, error) {
+ return i.rawIndex, nil
+}
+
+func (i *layoutIndex) Image(h v1.Hash) (v1.Image, error) {
+ // Look up the digest in our manifest first to return a better error.
+ desc, err := i.findDescriptor(h)
+ if err != nil {
+ return nil, err
+ }
+
+ if !isExpectedMediaType(desc.MediaType, types.OCIManifestSchema1, types.DockerManifestSchema2) {
+ return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType)
+ }
+
+ img := &layoutImage{
+ path: i.path,
+ desc: *desc,
+ }
+ return partial.CompressedToImage(img)
+}
+
+func (i *layoutIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
+ // Look up the digest in our manifest first to return a better error.
+ desc, err := i.findDescriptor(h)
+ if err != nil {
+ return nil, err
+ }
+
+ if !isExpectedMediaType(desc.MediaType, types.OCIImageIndex, types.DockerManifestList) {
+ return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType)
+ }
+
+ rawIndex, err := i.path.Bytes(h)
+ if err != nil {
+ return nil, err
+ }
+
+ return &layoutIndex{
+ mediaType: desc.MediaType,
+ path: i.path,
+ rawIndex: rawIndex,
+ }, nil
+}
+
+func (i *layoutIndex) Blob(h v1.Hash) (io.ReadCloser, error) {
+ return i.path.Blob(h)
+}
+
+func (i *layoutIndex) findDescriptor(h v1.Hash) (*v1.Descriptor, error) {
+ im, err := i.IndexManifest()
+ if err != nil {
+ return nil, err
+ }
+
+ if h == (v1.Hash{}) {
+ if len(im.Manifests) != 1 {
+ return nil, errors.New("oci layout must contain only a single image to be used with layout.Image")
+ }
+ return &(im.Manifests)[0], nil
+ }
+
+ for _, desc := range im.Manifests {
+ if desc.Digest == h {
+ return &desc, nil
+ }
+ }
+
+ return nil, fmt.Errorf("could not find descriptor in index: %s", h)
+}
+
+// TODO: Pull this out into methods on types.MediaType? e.g. instead, have:
+// * mt.IsIndex()
+// * mt.IsImage()
+func isExpectedMediaType(mt types.MediaType, expected ...types.MediaType) bool {
+ for _, allowed := range expected {
+ if mt == allowed {
+ return true
+ }
+ }
+ return false
+}
diff --git a/pkg/v1/layout/index_test.go b/pkg/v1/layout/index_test.go
new file mode 100644
index 0000000..4478831
--- /dev/null
+++ b/pkg/v1/layout/index_test.go
@@ -0,0 +1,81 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestIndex(t *testing.T) {
+ idx, err := ImageIndexFromPath(testPath)
+ if err != nil {
+ t.Fatalf("ImageIndexFromPath() = %v", err)
+ }
+
+ if err := validate.Index(idx); err != nil {
+ t.Errorf("validate.Index() = %v", err)
+ }
+
+ mt, err := idx.MediaType()
+ if err != nil {
+ t.Fatalf("MediaType() = %v", err)
+ }
+
+ if got, want := mt, types.OCIImageIndex; got != want {
+ t.Errorf("MediaType(); want: %v got: %v", want, got)
+ }
+
+ indexHash, _ := v1.NewHash("sha256:2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb")
+ ii, err := idx.ImageIndex(indexHash)
+ if err != nil {
+ t.Fatalf("ImageIndex() = %v", err)
+ }
+
+ mt, err = ii.MediaType()
+ if err != nil {
+ t.Fatalf("MediaType() = %v", err)
+ }
+
+ if got, want := mt, types.DockerManifestList; got != want {
+ t.Errorf("MediaType(); want: %v got: %v", want, got)
+ }
+}
+
+func TestIndexErrors(t *testing.T) {
+ idx, err := ImageIndexFromPath(testPath)
+ if err != nil {
+ t.Fatalf("ImageIndexFromPath() = %v", err)
+ }
+
+ if _, err := idx.Image(bogusDigest); err == nil {
+ t.Errorf("idx.Image(%s) = nil, expected err", bogusDigest)
+ }
+
+ if _, err := idx.Image(indexDigest); err == nil {
+ t.Errorf("idx.Image(%s) = nil, expected err", bogusDigest)
+ }
+
+ if _, err := idx.ImageIndex(bogusDigest); err == nil {
+ t.Errorf("idx.ImageIndex(%s) = nil, expected err", bogusDigest)
+ }
+
+ if _, err := idx.ImageIndex(manifestDigest); err == nil {
+ t.Errorf("idx.ImageIndex(%s) = nil, expected err", bogusDigest)
+ }
+}
diff --git a/pkg/v1/layout/layoutpath.go b/pkg/v1/layout/layoutpath.go
new file mode 100644
index 0000000..a031ff5
--- /dev/null
+++ b/pkg/v1/layout/layoutpath.go
@@ -0,0 +1,25 @@
+// Copyright 2019 The original author or authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import "path/filepath"
+
+// Path represents an OCI image layout rooted in a file system path
+type Path string
+
+func (l Path) path(elem ...string) string {
+ complete := []string{string(l)}
+ return filepath.Join(append(complete, elem...)...)
+}
diff --git a/pkg/v1/layout/options.go b/pkg/v1/layout/options.go
new file mode 100644
index 0000000..a26f9f3
--- /dev/null
+++ b/pkg/v1/layout/options.go
@@ -0,0 +1,71 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import v1 "github.com/google/go-containerregistry/pkg/v1"
+
+// Option is a functional option for Layout.
+type Option func(*options)
+
+type options struct {
+ descOpts []descriptorOption
+}
+
+func makeOptions(opts ...Option) *options {
+ o := &options{
+ descOpts: []descriptorOption{},
+ }
+ for _, apply := range opts {
+ apply(o)
+ }
+ return o
+}
+
+type descriptorOption func(*v1.Descriptor)
+
+// WithAnnotations adds annotations to the artifact descriptor.
+func WithAnnotations(annotations map[string]string) Option {
+ return func(o *options) {
+ o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
+ if desc.Annotations == nil {
+ desc.Annotations = make(map[string]string)
+ }
+ for k, v := range annotations {
+ desc.Annotations[k] = v
+ }
+ })
+ }
+}
+
+// WithURLs adds urls to the artifact descriptor.
+func WithURLs(urls []string) Option {
+ return func(o *options) {
+ o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
+ if desc.URLs == nil {
+ desc.URLs = []string{}
+ }
+ desc.URLs = append(desc.URLs, urls...)
+ })
+ }
+}
+
+// WithPlatform sets the platform of the artifact descriptor.
+func WithPlatform(platform v1.Platform) Option {
+ return func(o *options) {
+ o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
+ desc.Platform = &platform
+ })
+ }
+}
diff --git a/pkg/v1/layout/read.go b/pkg/v1/layout/read.go
new file mode 100644
index 0000000..796abc7
--- /dev/null
+++ b/pkg/v1/layout/read.go
@@ -0,0 +1,32 @@
+// Copyright 2019 The original author or authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+ "os"
+ "path/filepath"
+)
+
+// FromPath reads an OCI image layout at path and constructs a layout.Path.
+func FromPath(path string) (Path, error) {
+ // TODO: check oci-layout exists
+
+ _, err := os.Stat(filepath.Join(path, "index.json"))
+ if err != nil {
+ return "", err
+ }
+
+ return Path(path), nil
+}
diff --git a/pkg/v1/layout/read_test.go b/pkg/v1/layout/read_test.go
new file mode 100644
index 0000000..281fa29
--- /dev/null
+++ b/pkg/v1/layout/read_test.go
@@ -0,0 +1,42 @@
+// Copyright 2019 The original author or authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+ "testing"
+)
+
+func TestRead(t *testing.T) {
+ lp, err := FromPath(testPath)
+ if err != nil {
+ t.Fatalf("FromPath() = %v", err)
+ }
+ if testPath != lp.path() {
+ t.Errorf("unexpected path %s", lp.path())
+ }
+}
+
+func TestReadErrors(t *testing.T) {
+ if _, err := FromPath(bogusPath); err == nil {
+ t.Errorf("FromPath(%s) = nil, expected err", bogusPath)
+ }
+
+ // Found this here:
+ // https://github.com/golang/go/issues/24195
+ invalidPath := "double-null-padded-string\x00\x00"
+ if _, err := FromPath(invalidPath); err == nil {
+ t.Errorf("FromPath(%s) = nil, expected err", bogusPath)
+ }
+}
diff --git a/pkg/v1/layout/testdata/README.md b/pkg/v1/layout/testdata/README.md
new file mode 100644
index 0000000..449ff1c
--- /dev/null
+++ b/pkg/v1/layout/testdata/README.md
@@ -0,0 +1,5 @@
+# Where did this data come from?
+
+These were manually produced from the pkg/v1/tarball/testadata tarballs.
+
+TODO: Make this reproducible. There's not currently an easy way to do this.
diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5 b/pkg/v1/layout/testdata/test_index/blobs/sha256/05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5
new file mode 100644
index 0000000..1597d07
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5
@@ -0,0 +1,13 @@
+{
+ "schemaVersion": 2,
+ "manifests": [
+ {
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
+ "size": 423,
+ "digest": "sha256:eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650",
+ "annotations": {
+ "org.opencontainers.image.ref.name": "1"
+ }
+ }
+ ]
+}
diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb b/pkg/v1/layout/testdata/test_index/blobs/sha256/2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb
new file mode 100644
index 0000000..e6587e2
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb
@@ -0,0 +1,13 @@
+{
+ "schemaVersion": 2,
+ "manifests": [
+ {
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
+ "size": 423,
+ "digest": "sha256:eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650",
+ "annotations": {
+ "org.opencontainers.image.ref.name": "4"
+ }
+ }
+ ]
+}
diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/321460fa87fd42433950b42d04b7aff249f4ed960d43404a9f699886906cc9d3 b/pkg/v1/layout/testdata/test_index/blobs/sha256/321460fa87fd42433950b42d04b7aff249f4ed960d43404a9f699886906cc9d3
new file mode 100644
index 0000000..096f21f
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/321460fa87fd42433950b42d04b7aff249f4ed960d43404a9f699886906cc9d3
Binary files differ
diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b003850720 b/pkg/v1/layout/testdata/test_index/blobs/sha256/32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b003850720
new file mode 100644
index 0000000..48609c6
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b003850720
@@ -0,0 +1 @@
+{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":330,"digest":"sha256:930705ce23e3b6ed4c08746b6fe880089c864fbaf62482702ae3fdd66b8c7fe9"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":165,"digest":"sha256:321460fa87fd42433950b42d04b7aff249f4ed960d43404a9f699886906cc9d3"}]} \ No newline at end of file
diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e b/pkg/v1/layout/testdata/test_index/blobs/sha256/6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e
new file mode 100644
index 0000000..4228c89
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e
@@ -0,0 +1 @@
+{"architecture": "amd64", "author": "Bazel", "config": {}, "created": "1970-01-01T00:00:00Z", "history": [{"author": "Bazel", "created": "1970-01-01T00:00:00Z", "created_by": "bazel build ..."}], "os": "linux", "rootfs": {"diff_ids": ["sha256:8897395fd26dc44ad0e2a834335b33198cb41ac4d98dfddf58eced3853fa7b17"], "type": "layers"}}
diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/930705ce23e3b6ed4c08746b6fe880089c864fbaf62482702ae3fdd66b8c7fe9 b/pkg/v1/layout/testdata/test_index/blobs/sha256/930705ce23e3b6ed4c08746b6fe880089c864fbaf62482702ae3fdd66b8c7fe9
new file mode 100644
index 0000000..425c2d0
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/930705ce23e3b6ed4c08746b6fe880089c864fbaf62482702ae3fdd66b8c7fe9
@@ -0,0 +1 @@
+{"architecture": "amd64", "author": "Bazel", "config": {}, "created": "1970-01-01T00:00:00Z", "history": [{"author": "Bazel", "created": "1970-01-01T00:00:00Z", "created_by": "bazel build ..."}], "os": "linux", "rootfs": {"diff_ids": ["sha256:3610aa5267a210147ba6ca02cdd87610dfc08522de9c5f5015edd8ee14853fd8"], "type": "layers"}}
diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b b/pkg/v1/layout/testdata/test_index/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b
new file mode 100644
index 0000000..05c6321
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b
Binary files differ
diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650 b/pkg/v1/layout/testdata/test_index/blobs/sha256/eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650
new file mode 100644
index 0000000..21dc412
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650
@@ -0,0 +1 @@
+{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":330,"digest":"sha256:6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":167,"digest":"sha256:dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b"}]} \ No newline at end of file
diff --git a/pkg/v1/layout/testdata/test_index/index.json b/pkg/v1/layout/testdata/test_index/index.json
new file mode 100644
index 0000000..28df736
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/index.json
@@ -0,0 +1,37 @@
+{
+ "schemaVersion": 2,
+ "manifests": [
+ {
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
+ "size": 423,
+ "digest": "sha256:eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650",
+ "annotations": {
+ "org.opencontainers.image.ref.name": "1"
+ }
+ },
+ {
+ "mediaType": "application/vnd.oci.descriptor.v1+json",
+ "size": 423,
+ "digest": "sha256:32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b003850720",
+ "annotations": {
+ "org.opencontainers.image.ref.name": "2"
+ }
+ },
+ {
+ "mediaType": "application/vnd.oci.image.index.v1+json",
+ "size": 314,
+ "digest": "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5",
+ "annotations": {
+ "org.opencontainers.image.ref.name": "3"
+ }
+ },
+ {
+ "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
+ "size": 314,
+ "digest": "sha256:2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb",
+ "annotations": {
+ "org.opencontainers.image.ref.name": "4"
+ }
+ }
+ ]
+}
diff --git a/pkg/v1/layout/testdata/test_index/oci-layout b/pkg/v1/layout/testdata/test_index/oci-layout
new file mode 100644
index 0000000..10ff2f3
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index/oci-layout
@@ -0,0 +1,3 @@
+{
+ "imageLayoutVersion": "1.0.0"
+}
diff --git a/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e
new file mode 100644
index 0000000..53aea8f
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e
@@ -0,0 +1,15 @@
+{
+ "schemaVersion": 2,
+ "config": {
+ "mediaType": "application/vnd.cncf.helm.config.v1+json",
+ "digest": "sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
+ "size": 3
+ },
+ "layers": [
+ {
+ "mediaType": "application/tar+gzip",
+ "digest": "sha256:dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b",
+ "size": 167
+ }
+ ]
+}
diff --git a/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356 b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356
@@ -0,0 +1 @@
+{}
diff --git a/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b
new file mode 100644
index 0000000..05c6321
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b
Binary files differ
diff --git a/pkg/v1/layout/testdata/test_index_media_type/index.json b/pkg/v1/layout/testdata/test_index_media_type/index.json
new file mode 100644
index 0000000..ffe4d3e
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_media_type/index.json
@@ -0,0 +1,10 @@
+{
+ "schemaVersion": 2,
+ "manifests": [
+ {
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
+ "size": 391,
+ "digest": "sha256:b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e"
+ }
+ ]
+}
diff --git a/pkg/v1/layout/testdata/test_index_media_type/oci-layout b/pkg/v1/layout/testdata/test_index_media_type/oci-layout
new file mode 100644
index 0000000..10ff2f3
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_media_type/oci-layout
@@ -0,0 +1,3 @@
+{
+ "imageLayoutVersion": "1.0.0"
+}
diff --git a/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/381d958b555884ba59574ab5c066e9f6116b5aec3567675aa13bec63331f0810 b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/381d958b555884ba59574ab5c066e9f6116b5aec3567675aa13bec63331f0810
new file mode 100644
index 0000000..e49e018
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/381d958b555884ba59574ab5c066e9f6116b5aec3567675aa13bec63331f0810
@@ -0,0 +1 @@
+{"created":"2020-04-12T10:58:48.626858334Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs":{"type":"layers","diff_ids":["sha256:59cd31f50f7442a662d7c31b7a12079ade16892bfb465b33da49918e7d13e747"]},"history":[{"created":"2020-04-12T10:58:48.626858334Z","created_by":"/bin/sh -c #(nop) COPY file:a34f6c104b4cb0668083b4de122deebb3e3629e212f82c32fec316dd8e3a1931 in / "}]} \ No newline at end of file
diff --git a/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0 b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0
new file mode 100644
index 0000000..1e4eb22
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0
Binary files differ
diff --git a/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/98ceaf93e482fe91b9bfd6bba07137c098e49ee2d55e69f09fb6c951e75e0e46 b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/98ceaf93e482fe91b9bfd6bba07137c098e49ee2d55e69f09fb6c951e75e0e46
new file mode 100644
index 0000000..f02779b
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/98ceaf93e482fe91b9bfd6bba07137c098e49ee2d55e69f09fb6c951e75e0e46
@@ -0,0 +1 @@
+{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:381d958b555884ba59574ab5c066e9f6116b5aec3567675aa13bec63331f0810","size":452},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0","size":114}]} \ No newline at end of file
diff --git a/pkg/v1/layout/testdata/test_index_one_image/index.json b/pkg/v1/layout/testdata/test_index_one_image/index.json
new file mode 100644
index 0000000..4ec03cc
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_one_image/index.json
@@ -0,0 +1 @@
+{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:98ceaf93e482fe91b9bfd6bba07137c098e49ee2d55e69f09fb6c951e75e0e46","size":344}]} \ No newline at end of file
diff --git a/pkg/v1/layout/testdata/test_index_one_image/oci-layout b/pkg/v1/layout/testdata/test_index_one_image/oci-layout
new file mode 100644
index 0000000..21b1439
--- /dev/null
+++ b/pkg/v1/layout/testdata/test_index_one_image/oci-layout
@@ -0,0 +1 @@
+{"imageLayoutVersion": "1.0.0"} \ No newline at end of file
diff --git a/pkg/v1/layout/write.go b/pkg/v1/layout/write.go
new file mode 100644
index 0000000..906b12a
--- /dev/null
+++ b/pkg/v1/layout/write.go
@@ -0,0 +1,481 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/google/go-containerregistry/pkg/logs"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/match"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "golang.org/x/sync/errgroup"
+)
+
+var layoutFile = `{
+ "imageLayoutVersion": "1.0.0"
+}`
+
+// AppendImage writes a v1.Image to the Path and updates
+// the index.json to reference it.
+func (l Path) AppendImage(img v1.Image, options ...Option) error {
+ if err := l.WriteImage(img); err != nil {
+ return err
+ }
+
+ desc, err := partial.Descriptor(img)
+ if err != nil {
+ return err
+ }
+
+ o := makeOptions(options...)
+ for _, opt := range o.descOpts {
+ opt(desc)
+ }
+
+ return l.AppendDescriptor(*desc)
+}
+
+// AppendIndex writes a v1.ImageIndex to the Path and updates
+// the index.json to reference it.
+func (l Path) AppendIndex(ii v1.ImageIndex, options ...Option) error {
+ if err := l.WriteIndex(ii); err != nil {
+ return err
+ }
+
+ desc, err := partial.Descriptor(ii)
+ if err != nil {
+ return err
+ }
+
+ o := makeOptions(options...)
+ for _, opt := range o.descOpts {
+ opt(desc)
+ }
+
+ return l.AppendDescriptor(*desc)
+}
+
+// AppendDescriptor adds a descriptor to the index.json of the Path.
+func (l Path) AppendDescriptor(desc v1.Descriptor) error {
+ ii, err := l.ImageIndex()
+ if err != nil {
+ return err
+ }
+
+ index, err := ii.IndexManifest()
+ if err != nil {
+ return err
+ }
+
+ index.Manifests = append(index.Manifests, desc)
+
+ rawIndex, err := json.MarshalIndent(index, "", " ")
+ if err != nil {
+ return err
+ }
+
+ return l.WriteFile("index.json", rawIndex, os.ModePerm)
+}
+
+// ReplaceImage writes a v1.Image to the Path and updates
+// the index.json to reference it, replacing any existing one that matches matcher, if found.
+func (l Path) ReplaceImage(img v1.Image, matcher match.Matcher, options ...Option) error {
+ if err := l.WriteImage(img); err != nil {
+ return err
+ }
+
+ return l.replaceDescriptor(img, matcher, options...)
+}
+
+// ReplaceIndex writes a v1.ImageIndex to the Path and updates
+// the index.json to reference it, replacing any existing one that matches matcher, if found.
+func (l Path) ReplaceIndex(ii v1.ImageIndex, matcher match.Matcher, options ...Option) error {
+ if err := l.WriteIndex(ii); err != nil {
+ return err
+ }
+
+ return l.replaceDescriptor(ii, matcher, options...)
+}
+
+// replaceDescriptor adds a descriptor to the index.json of the Path, replacing
+// any one matching matcher, if found.
+func (l Path) replaceDescriptor(append mutate.Appendable, matcher match.Matcher, options ...Option) error {
+ ii, err := l.ImageIndex()
+ if err != nil {
+ return err
+ }
+
+ desc, err := partial.Descriptor(append)
+ if err != nil {
+ return err
+ }
+
+ o := makeOptions(options...)
+ for _, opt := range o.descOpts {
+ opt(desc)
+ }
+
+ add := mutate.IndexAddendum{
+ Add: append,
+ Descriptor: *desc,
+ }
+ ii = mutate.AppendManifests(mutate.RemoveManifests(ii, matcher), add)
+
+ index, err := ii.IndexManifest()
+ if err != nil {
+ return err
+ }
+
+ rawIndex, err := json.MarshalIndent(index, "", " ")
+ if err != nil {
+ return err
+ }
+
+ return l.WriteFile("index.json", rawIndex, os.ModePerm)
+}
+
+// RemoveDescriptors removes any descriptors that match the match.Matcher from the index.json of the Path.
+func (l Path) RemoveDescriptors(matcher match.Matcher) error {
+ ii, err := l.ImageIndex()
+ if err != nil {
+ return err
+ }
+ ii = mutate.RemoveManifests(ii, matcher)
+
+ index, err := ii.IndexManifest()
+ if err != nil {
+ return err
+ }
+
+ rawIndex, err := json.MarshalIndent(index, "", " ")
+ if err != nil {
+ return err
+ }
+
+ return l.WriteFile("index.json", rawIndex, os.ModePerm)
+}
+
+// WriteFile write a file with arbitrary data at an arbitrary location in a v1
+// layout. Used mostly internally to write files like "oci-layout" and
+// "index.json", also can be used to write other arbitrary files. Do *not* use
+// this to write blobs. Use only WriteBlob() for that.
+func (l Path) WriteFile(name string, data []byte, perm os.FileMode) error {
+ if err := os.MkdirAll(l.path(), os.ModePerm); err != nil && !os.IsExist(err) {
+ return err
+ }
+
+ return os.WriteFile(l.path(name), data, perm)
+}
+
+// WriteBlob copies a file to the blobs/ directory in the Path from the given ReadCloser at
+// blobs/{hash.Algorithm}/{hash.Hex}.
+func (l Path) WriteBlob(hash v1.Hash, r io.ReadCloser) error {
+ return l.writeBlob(hash, -1, r, nil)
+}
+
+func (l Path) writeBlob(hash v1.Hash, size int64, rc io.ReadCloser, renamer func() (v1.Hash, error)) error {
+ if hash.Hex == "" && renamer == nil {
+ panic("writeBlob called an invalid hash and no renamer")
+ }
+
+ dir := l.path("blobs", hash.Algorithm)
+ if err := os.MkdirAll(dir, os.ModePerm); err != nil && !os.IsExist(err) {
+ return err
+ }
+
+ // Check if blob already exists and is the correct size
+ file := filepath.Join(dir, hash.Hex)
+ if s, err := os.Stat(file); err == nil && !s.IsDir() && (s.Size() == size || size == -1) {
+ return nil
+ }
+
+ // If a renamer func was provided write to a temporary file
+ open := func() (*os.File, error) { return os.Create(file) }
+ if renamer != nil {
+ open = func() (*os.File, error) { return os.CreateTemp(dir, hash.Hex) }
+ }
+ w, err := open()
+ if err != nil {
+ return err
+ }
+ if renamer != nil {
+ // Delete temp file if an error is encountered before renaming
+ defer func() {
+ if err := os.Remove(w.Name()); err != nil && !errors.Is(err, os.ErrNotExist) {
+ logs.Warn.Printf("error removing temporary file after encountering an error while writing blob: %v", err)
+ }
+ }()
+ }
+ defer w.Close()
+
+ // Write to file and exit if not renaming
+ if n, err := io.Copy(w, rc); err != nil || renamer == nil {
+ return err
+ } else if size != -1 && n != size {
+ return fmt.Errorf("expected blob size %d, but only wrote %d", size, n)
+ }
+
+ // Always close reader before renaming, since Close computes the digest in
+ // the case of streaming layers. If Close is not called explicitly, it will
+ // occur in a goroutine that is not guaranteed to succeed before renamer is
+ // called. When renamer is the layer's Digest method, it can return
+ // ErrNotComputed.
+ if err := rc.Close(); err != nil {
+ return err
+ }
+
+ // Always close file before renaming
+ if err := w.Close(); err != nil {
+ return err
+ }
+
+ // Rename file based on the final hash
+ finalHash, err := renamer()
+ if err != nil {
+ return fmt.Errorf("error getting final digest of layer: %w", err)
+ }
+
+ renamePath := l.path("blobs", finalHash.Algorithm, finalHash.Hex)
+ return os.Rename(w.Name(), renamePath)
+}
+
+// writeLayer writes the compressed layer to a blob. Unlike WriteBlob it will
+// write to a temporary file (suffixed with .tmp) within the layout until the
+// compressed reader is fully consumed and written to disk. Also unlike
+// WriteBlob, it will not skip writing and exit without error when a blob file
+// exists, but does not have the correct size. (The blob hash is not
+// considered, because it may be expensive to compute.)
+func (l Path) writeLayer(layer v1.Layer) error {
+ d, err := layer.Digest()
+ if errors.Is(err, stream.ErrNotComputed) {
+ // Allow digest errors, since streams may not have calculated the hash
+ // yet. Instead, use an empty value, which will be transformed into a
+ // random file name with `os.CreateTemp` and the final digest will be
+ // calculated after writing to a temp file and before renaming to the
+ // final path.
+ d = v1.Hash{Algorithm: "sha256", Hex: ""}
+ } else if err != nil {
+ return err
+ }
+
+ s, err := layer.Size()
+ if errors.Is(err, stream.ErrNotComputed) {
+ // Allow size errors, since streams may not have calculated the size
+ // yet. Instead, use zero as a sentinel value meaning that no size
+ // comparison can be done and any sized blob file should be considered
+ // valid and not overwritten.
+ //
+ // TODO: Provide an option to always overwrite blobs.
+ s = -1
+ } else if err != nil {
+ return err
+ }
+
+ r, err := layer.Compressed()
+ if err != nil {
+ return err
+ }
+
+ if err := l.writeBlob(d, s, r, layer.Digest); err != nil {
+ return fmt.Errorf("error writing layer: %w", err)
+ }
+ return nil
+}
+
+// RemoveBlob removes a file from the blobs directory in the Path
+// at blobs/{hash.Algorithm}/{hash.Hex}
+// It does *not* remove any reference to it from other manifests or indexes, or
+// from the root index.json.
+func (l Path) RemoveBlob(hash v1.Hash) error {
+ dir := l.path("blobs", hash.Algorithm)
+ err := os.Remove(filepath.Join(dir, hash.Hex))
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+}
+
+// WriteImage writes an image, including its manifest, config and all of its
+// layers, to the blobs directory. If any blob already exists, as determined by
+// the hash filename, does not write it.
+// This function does *not* update the `index.json` file. If you want to write the
+// image and also update the `index.json`, call AppendImage(), which wraps this
+// and also updates the `index.json`.
+func (l Path) WriteImage(img v1.Image) error {
+ layers, err := img.Layers()
+ if err != nil {
+ return err
+ }
+
+ // Write the layers concurrently.
+ var g errgroup.Group
+ for _, layer := range layers {
+ layer := layer
+ g.Go(func() error {
+ return l.writeLayer(layer)
+ })
+ }
+ if err := g.Wait(); err != nil {
+ return err
+ }
+
+ // Write the config.
+ cfgName, err := img.ConfigName()
+ if err != nil {
+ return err
+ }
+ cfgBlob, err := img.RawConfigFile()
+ if err != nil {
+ return err
+ }
+ if err := l.WriteBlob(cfgName, io.NopCloser(bytes.NewReader(cfgBlob))); err != nil {
+ return err
+ }
+
+ // Write the img manifest.
+ d, err := img.Digest()
+ if err != nil {
+ return err
+ }
+ manifest, err := img.RawManifest()
+ if err != nil {
+ return err
+ }
+
+ return l.WriteBlob(d, io.NopCloser(bytes.NewReader(manifest)))
+}
+
+type withLayer interface {
+ Layer(v1.Hash) (v1.Layer, error)
+}
+
+type withBlob interface {
+ Blob(v1.Hash) (io.ReadCloser, error)
+}
+
+func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
+ index, err := ii.IndexManifest()
+ if err != nil {
+ return err
+ }
+
+ // Walk the descriptors and write any v1.Image or v1.ImageIndex that we find.
+ // If we come across something we don't expect, just write it as a blob.
+ for _, desc := range index.Manifests {
+ switch desc.MediaType {
+ case types.OCIImageIndex, types.DockerManifestList:
+ ii, err := ii.ImageIndex(desc.Digest)
+ if err != nil {
+ return err
+ }
+ if err := l.WriteIndex(ii); err != nil {
+ return err
+ }
+ case types.OCIManifestSchema1, types.DockerManifestSchema2:
+ img, err := ii.Image(desc.Digest)
+ if err != nil {
+ return err
+ }
+ if err := l.WriteImage(img); err != nil {
+ return err
+ }
+ default:
+ // TODO: The layout could reference arbitrary things, which we should
+ // probably just pass through.
+
+ var blob io.ReadCloser
+ // Workaround for #819.
+ if wl, ok := ii.(withLayer); ok {
+ layer, lerr := wl.Layer(desc.Digest)
+ if lerr != nil {
+ return lerr
+ }
+ blob, err = layer.Compressed()
+ } else if wb, ok := ii.(withBlob); ok {
+ blob, err = wb.Blob(desc.Digest)
+ }
+ if err != nil {
+ return err
+ }
+ if err := l.WriteBlob(desc.Digest, blob); err != nil {
+ return err
+ }
+ }
+ }
+
+ rawIndex, err := ii.RawManifest()
+ if err != nil {
+ return err
+ }
+
+ return l.WriteFile(indexFile, rawIndex, os.ModePerm)
+}
+
+// WriteIndex writes an index to the blobs directory. Walks down the children,
+// including its children manifests and/or indexes, and down the tree until all of
+// config and all layers, have been written. If any blob already exists, as determined by
+// the hash filename, does not write it.
+// This function does *not* update the `index.json` file. If you want to write the
+// index and also update the `index.json`, call AppendIndex(), which wraps this
+// and also updates the `index.json`.
+func (l Path) WriteIndex(ii v1.ImageIndex) error {
+ // Always just write oci-layout file, since it's small.
+ if err := l.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil {
+ return err
+ }
+
+ h, err := ii.Digest()
+ if err != nil {
+ return err
+ }
+
+ indexFile := filepath.Join("blobs", h.Algorithm, h.Hex)
+ return l.writeIndexToFile(indexFile, ii)
+}
+
+// Write constructs a Path at path from an ImageIndex.
+//
+// The contents are written in the following format:
+// At the top level, there is:
+//
+// One oci-layout file containing the version of this image-layout.
+// One index.json file listing descriptors for the contained images.
+//
+// Under blobs/, there is, for each image:
+//
+// One file for each layer, named after the layer's SHA.
+// One file for each config blob, named after its SHA.
+// One file for each manifest blob, named after its SHA.
+func Write(path string, ii v1.ImageIndex) (Path, error) {
+ lp := Path(path)
+ // Always just write oci-layout file, since it's small.
+ if err := lp.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil {
+ return "", err
+ }
+
+ // TODO create blobs/ in case there is a blobs file which would prevent the directory from being created
+
+ return lp, lp.writeIndexToFile("index.json", ii)
+}
diff --git a/pkg/v1/layout/write_test.go b/pkg/v1/layout/write_test.go
new file mode 100644
index 0000000..530e0e8
--- /dev/null
+++ b/pkg/v1/layout/write_test.go
@@ -0,0 +1,672 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package layout
+
+import (
+ "archive/tar"
+ "bytes"
+ "io"
+ "log"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/match"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestWrite(t *testing.T) {
+ tmp := t.TempDir()
+
+ original, err := ImageIndexFromPath(testPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if layoutPath, err := Write(tmp, original); err != nil {
+ t.Fatalf("Write(%s) = %v", tmp, err)
+ } else if tmp != layoutPath.path() {
+ t.Fatalf("unexpected file system path %v", layoutPath)
+ }
+
+ written, err := ImageIndexFromPath(tmp)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Index(written); err != nil {
+ t.Fatalf("validate.Index() = %v", err)
+ }
+}
+
+func TestWriteErrors(t *testing.T) {
+ idx, err := ImageIndexFromPath(testPath)
+ if err != nil {
+ t.Fatalf("ImageIndexFromPath() = %v", err)
+ }
+
+ // Found this here:
+ // https://github.com/golang/go/issues/24195
+ invalidPath := "double-null-padded-string\x00\x00"
+ if _, err := Write(invalidPath, idx); err == nil {
+ t.Fatalf("Write(%s) = nil, expected err", invalidPath)
+ }
+}
+
+func TestAppendDescriptorInitializesIndex(t *testing.T) {
+ tmp := t.TempDir()
+ temp, err := Write(tmp, empty.Index)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Append a descriptor to a non-existent layout.
+ desc := v1.Descriptor{
+ Digest: bogusDigest,
+ Size: 1337,
+ MediaType: types.MediaType("not real"),
+ }
+ if err := temp.AppendDescriptor(desc); err != nil {
+ t.Fatalf("AppendDescriptor(%s) = %v", tmp, err)
+ }
+
+ // Read that layout from disk and make sure the descriptor is there.
+ idx, err := ImageIndexFromPath(tmp)
+ if err != nil {
+ t.Fatalf("ImageIndexFromPath() = %v", err)
+ }
+
+ manifest, err := idx.IndexManifest()
+ if err != nil {
+ t.Fatalf("IndexManifest() = %v", err)
+ }
+ if diff := cmp.Diff(manifest.Manifests[0], desc); diff != "" {
+ t.Fatalf("bad descriptor: (-got +want) %s", diff)
+ }
+}
+
+func TestRoundtrip(t *testing.T) {
+ tmp := t.TempDir()
+
+ original, err := ImageIndexFromPath(testPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ originalManifest, err := original.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Write it back.
+ if _, err := Write(tmp, original); err != nil {
+ t.Fatal(err)
+ }
+ reconstructed, err := ImageIndexFromPath(tmp)
+ if err != nil {
+ t.Fatalf("ImageIndexFromPath() = %v", err)
+ }
+ reconstructedManifest, err := reconstructed.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff := cmp.Diff(originalManifest, reconstructedManifest); diff != "" {
+ t.Fatalf("bad manifest: (-got +want) %s", diff)
+ }
+}
+
+func TestOptions(t *testing.T) {
+ tmp := t.TempDir()
+ temp, err := Write(tmp, empty.Index)
+ if err != nil {
+ t.Fatal(err)
+ }
+ annotations := map[string]string{
+ "foo": "bar",
+ }
+ urls := []string{"https://example.com"}
+ platform := v1.Platform{
+ Architecture: "mill",
+ OS: "haiku",
+ }
+ img, err := random.Image(5, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ options := []Option{
+ WithAnnotations(annotations),
+ WithURLs(urls),
+ WithPlatform(platform),
+ }
+ if err := temp.AppendImage(img, options...); err != nil {
+ t.Fatal(err)
+ }
+ idx, err := temp.ImageIndex()
+ if err != nil {
+ t.Fatal(err)
+ }
+ indexManifest, err := idx.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ desc := indexManifest.Manifests[0]
+ if got, want := desc.Annotations["foo"], "bar"; got != want {
+ t.Fatalf("wrong annotation; got: %v, want: %v", got, want)
+ }
+ if got, want := desc.URLs[0], "https://example.com"; got != want {
+ t.Fatalf("wrong urls; got: %v, want: %v", got, want)
+ }
+ if got, want := desc.Platform.Architecture, "mill"; got != want {
+ t.Fatalf("wrong Architecture; got: %v, want: %v", got, want)
+ }
+ if got, want := desc.Platform.OS, "haiku"; got != want {
+ t.Fatalf("wrong OS; got: %v, want: %v", got, want)
+ }
+}
+
+func TestDeduplicatedWrites(t *testing.T) {
+ lp, err := FromPath(testPath)
+ if err != nil {
+ t.Fatalf("FromPath() = %v", err)
+ }
+
+ b, err := lp.Blob(configDigest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ buf := bytes.NewBuffer([]byte{})
+ if _, err := io.Copy(buf, b); err != nil {
+ log.Fatal(err)
+ }
+
+ if err := lp.WriteBlob(configDigest, io.NopCloser(bytes.NewBuffer(buf.Bytes()))); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := lp.WriteBlob(configDigest, io.NopCloser(bytes.NewBuffer(buf.Bytes()))); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRemoveDescriptor(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex
+ ii = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // add two images
+ image1, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendImage(image1); err != nil {
+ t.Fatal(err)
+ }
+ image2, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendImage(image2); err != nil {
+ t.Fatal(err)
+ }
+
+ // remove one of the images by descriptor and ensure it is correct
+ digest1, err := image1.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest2, err := image2.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.RemoveDescriptors(match.Digests(digest1)); err != nil {
+ t.Fatal(err)
+ }
+ // ensure we only have one
+ ii, err = l.ImageIndex()
+ if err != nil {
+ t.Fatal(err)
+ }
+ manifest, err := ii.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(manifest.Manifests) != 1 {
+ t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 1)
+ }
+ if manifest.Manifests[0].Digest != digest2 {
+ t.Fatal("removed wrong digest")
+ }
+}
+
+func TestReplaceIndex(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex
+ ii = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // add two indexes
+ index1, err := random.Index(1024, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendIndex(index1); err != nil {
+ t.Fatal(err)
+ }
+ index2, err := random.Index(1024, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendIndex(index2); err != nil {
+ t.Fatal(err)
+ }
+ index3, err := random.Index(1024, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // remove one of the indexes by descriptor and ensure it is correct
+ digest1, err := index1.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest3, err := index3.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.ReplaceIndex(index3, match.Digests(digest1)); err != nil {
+ t.Fatal(err)
+ }
+ // ensure we only have one
+ ii, err = l.ImageIndex()
+ if err != nil {
+ t.Fatal(err)
+ }
+ manifest, err := ii.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(manifest.Manifests) != 2 {
+ t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 2)
+ }
+ // we should have digest3, and *not* have digest1
+ var have3 bool
+ for _, m := range manifest.Manifests {
+ if m.Digest == digest1 {
+ t.Fatal("found digest1 still not replaced", digest1)
+ }
+ if m.Digest == digest3 {
+ have3 = true
+ }
+ }
+ if !have3 {
+ t.Fatal("could not find digest3", digest3)
+ }
+}
+
+func TestReplaceImage(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex
+ ii = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // add two images
+ image1, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendImage(image1); err != nil {
+ t.Fatal(err)
+ }
+ image2, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendImage(image2); err != nil {
+ t.Fatal(err)
+ }
+ image3, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // remove one of the images by descriptor and ensure it is correct
+ digest1, err := image1.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest3, err := image3.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.ReplaceImage(image3, match.Digests(digest1)); err != nil {
+ t.Fatal(err)
+ }
+ // ensure we only have one
+ ii, err = l.ImageIndex()
+ if err != nil {
+ t.Fatal(err)
+ }
+ manifest, err := ii.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(manifest.Manifests) != 2 {
+ t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 2)
+ }
+ // we should have digest3, and *not* have digest1
+ var have3 bool
+ for _, m := range manifest.Manifests {
+ if m.Digest == digest1 {
+ t.Fatal("found digest1 still not replaced", digest1)
+ }
+ if m.Digest == digest3 {
+ have3 = true
+ }
+ }
+ if !have3 {
+ t.Fatal("could not find digest3", digest3)
+ }
+}
+
+func TestRemoveBlob(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create a random blob
+ b := []byte("abcdefghijklmnop")
+ hash, _, err := v1.SHA256(bytes.NewReader(b))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := l.WriteBlob(hash, io.NopCloser(bytes.NewReader(b))); err != nil {
+ t.Fatal(err)
+ }
+ // make sure it exists
+ b2, err := l.Bytes(hash)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(b, b2) {
+ t.Fatal("mismatched bytes")
+ }
+ // now the real test, delete it
+ if err := l.RemoveBlob(hash); err != nil {
+ t.Fatal(err)
+ }
+ // now it should not exist
+ if _, err = l.Bytes(hash); err == nil {
+ t.Fatal("still existed after deletion")
+ }
+}
+
+func TestStreamingWriteLayer(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create a random streaming image and persist
+ pr, pw := io.Pipe()
+ tw := tar.NewWriter(pw)
+ go func() {
+ pw.CloseWithError(func() error {
+ body := "test file"
+ if err := tw.WriteHeader(&tar.Header{
+ Name: "test.txt",
+ Mode: 0600,
+ Size: int64(len(body)),
+ Typeflag: tar.TypeReg,
+ }); err != nil {
+ return err
+ }
+ if _, err := tw.Write([]byte(body)); err != nil {
+ return err
+ }
+ return tw.Close()
+ }())
+ }()
+ img, err := mutate.Append(empty.Image, mutate.Addendum{
+ Layer: stream.NewLayer(pr),
+ })
+ if err != nil {
+ t.Fatalf("creating random streaming image failed: %v", err)
+ }
+ if _, err := img.Digest(); err == nil {
+ t.Fatal("digesting image before stream is consumed; (v1.Image).Digest() = nil, expected err")
+ }
+ // AppendImage uses writeLayer
+ if err := l.AppendImage(img); err != nil {
+ t.Fatalf("(Path).AppendImage() = %v", err)
+ }
+
+ // Check that image was persisted and is valid
+ imgDigest, err := img.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Image).Digest() = %v", err)
+ }
+ img, err = l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading image after writeLayer for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validate.Image() = %v", err)
+ }
+}
+
+func TestOverwriteWithWriteLayer(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create a random image and persist
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatalf("random.Image() = %v", err)
+ }
+ imgDigest, err := img.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Image).Digest() = %v", err)
+ }
+ if err := l.AppendImage(img); err != nil {
+ t.Fatalf("(Path).AppendImage() = %v", err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validate.Image() = %v", err)
+ }
+
+ // get the random image's layer
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n := len(layers); n != 1 {
+ t.Fatalf("expected image with 1 layer, got %d", n)
+ }
+
+ layer := layers[0]
+ layerDigest, err := layer.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Layer).Digest() = %v", err)
+ }
+
+ // truncate the layer contents on disk
+ completeLayerBytes, err := l.Bytes(layerDigest)
+ if err != nil {
+ t.Fatalf("(Path).Bytes() = %v", err)
+ }
+ truncatedLayerBytes := completeLayerBytes[:512]
+
+ path := l.path("blobs", layerDigest.Algorithm, layerDigest.Hex)
+ if err := os.WriteFile(path, truncatedLayerBytes, os.ModePerm); err != nil {
+ t.Fatalf("os.WriteFile(layerPath, truncated) = %v", err)
+ }
+
+ // ensure validation fails
+ img, err = l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(img); err == nil {
+ t.Fatal("validating image after truncating layer; validate.Image() = nil, expected err")
+ }
+
+ // try writing expected contents with WriteBlob
+ if err := l.WriteBlob(layerDigest, io.NopCloser(bytes.NewBuffer(completeLayerBytes))); err != nil {
+ t.Fatalf("error attempting to overwrite truncated layer with valid layer; (Path).WriteBlob = %v", err)
+ }
+
+ // validation should still fail
+ img, err = l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image after WriteBlob for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(img); err == nil {
+ t.Fatal("validating image after attempting repair of truncated layer with WriteBlob; validate.Image() = nil, expected err")
+ }
+
+ // try writing expected contents with writeLayer
+ if err := l.writeLayer(layer); err != nil {
+ t.Fatalf("error attempting to overwrite truncated layer with valid layer; (Path).writeLayer = %v", err)
+ }
+
+ // validation should now succeed
+ img, err = l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image after writeLayer for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validating image after attempting repair of truncated layer with writeLayer; validate.Image() = %v", err)
+ }
+}
+
+func TestOverwriteWithReplaceImage(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create a random image and persist
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatalf("random.Image() = %v", err)
+ }
+ imgDigest, err := img.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Image).Digest() = %v", err)
+ }
+ if err := l.AppendImage(img); err != nil {
+ t.Fatalf("(Path).AppendImage() = %v", err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validate.Image() = %v", err)
+ }
+
+ // get the random image's layer
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n := len(layers); n != 1 {
+ t.Fatalf("expected image with 1 layer, got %d", n)
+ }
+
+ layer := layers[0]
+ layerDigest, err := layer.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Layer).Digest() = %v", err)
+ }
+
+ // truncate the layer contents on disk
+ completeLayerBytes, err := l.Bytes(layerDigest)
+ if err != nil {
+ t.Fatalf("(Path).Bytes() = %v", err)
+ }
+ truncatedLayerBytes := completeLayerBytes[:512]
+
+ path := l.path("blobs", layerDigest.Algorithm, layerDigest.Hex)
+ if err := os.WriteFile(path, truncatedLayerBytes, os.ModePerm); err != nil {
+ t.Fatalf("os.WriteFile(layerPath, truncated) = %v", err)
+ }
+
+ // ensure validation fails
+ truncatedImg, err := l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(truncatedImg); err == nil {
+ t.Fatal("validating image after truncating layer; validate.Image() = nil, expected err")
+ } else if strings.Contains(err.Error(), "unexpected EOF") {
+ t.Fatalf("validating image after truncating layer; validate.Image() error is not helpful: %v", err)
+ }
+
+ // try writing expected contents with ReplaceImage
+ if err := l.ReplaceImage(img, match.Digests(imgDigest)); err != nil {
+ t.Fatalf("error attempting to overwrite truncated layer with valid layer; (Path).ReplaceImage = %v", err)
+ }
+
+ // validation should now succeed
+ repairedImg, err := l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image after ReplaceImage for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(repairedImg); err != nil {
+ t.Fatalf("validating image after attempting repair of truncated layer with ReplaceImage; validate.Image() = %v", err)
+ }
+}
diff --git a/pkg/v1/manifest.go b/pkg/v1/manifest.go
new file mode 100644
index 0000000..22d483f
--- /dev/null
+++ b/pkg/v1/manifest.go
@@ -0,0 +1,71 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "encoding/json"
+ "io"
+
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Manifest represents the OCI image manifest in a structured way.
+type Manifest struct {
+ SchemaVersion int64 `json:"schemaVersion"`
+ MediaType types.MediaType `json:"mediaType,omitempty"`
+ Config Descriptor `json:"config"`
+ Layers []Descriptor `json:"layers"`
+ Annotations map[string]string `json:"annotations,omitempty"`
+ Subject *Descriptor `json:"subject,omitempty"`
+}
+
+// IndexManifest represents an OCI image index in a structured way.
+type IndexManifest struct {
+ SchemaVersion int64 `json:"schemaVersion"`
+ MediaType types.MediaType `json:"mediaType,omitempty"`
+ Manifests []Descriptor `json:"manifests"`
+ Annotations map[string]string `json:"annotations,omitempty"`
+ Subject *Descriptor `json:"subject,omitempty"`
+}
+
+// Descriptor holds a reference from the manifest to one of its constituent elements.
+type Descriptor struct {
+ MediaType types.MediaType `json:"mediaType"`
+ Size int64 `json:"size"`
+ Digest Hash `json:"digest"`
+ Data []byte `json:"data,omitempty"`
+ URLs []string `json:"urls,omitempty"`
+ Annotations map[string]string `json:"annotations,omitempty"`
+ Platform *Platform `json:"platform,omitempty"`
+ ArtifactType string `json:"artifactType,omitempty"`
+}
+
+// ParseManifest parses the io.Reader's contents into a Manifest.
+func ParseManifest(r io.Reader) (*Manifest, error) {
+ m := Manifest{}
+ if err := json.NewDecoder(r).Decode(&m); err != nil {
+ return nil, err
+ }
+ return &m, nil
+}
+
+// ParseIndexManifest parses the io.Reader's contents into an IndexManifest.
+func ParseIndexManifest(r io.Reader) (*IndexManifest, error) {
+ im := IndexManifest{}
+ if err := json.NewDecoder(r).Decode(&im); err != nil {
+ return nil, err
+ }
+ return &im, nil
+}
diff --git a/pkg/v1/manifest_test.go b/pkg/v1/manifest_test.go
new file mode 100644
index 0000000..5cd5526
--- /dev/null
+++ b/pkg/v1/manifest_test.go
@@ -0,0 +1,76 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestGoodManifestSimple(t *testing.T) {
+ got, err := ParseManifest(strings.NewReader(`{}`))
+ if err != nil {
+ t.Errorf("Unexpected error parsing manifest: %v", err)
+ }
+
+ want := Manifest{}
+ if diff := cmp.Diff(want, *got); diff != "" {
+ t.Errorf("ParseManifest({}); (-want +got) %s", diff)
+ }
+}
+
+func TestGoodManifestWithHash(t *testing.T) {
+ good, err := ParseManifest(strings.NewReader(`{
+ "config": {
+ "digest": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
+ }
+}`))
+ if err != nil {
+ t.Errorf("Unexpected error parsing manifest: %v", err)
+ }
+
+ if got, want := good.Config.Digest.Algorithm, "sha256"; got != want {
+ t.Errorf("ParseManifest().Config.Digest.Algorithm; got %v, want %v", got, want)
+ }
+}
+
+func TestManifestWithBadHash(t *testing.T) {
+ bad, err := ParseManifest(strings.NewReader(`{
+ "config": {
+ "digest": "sha256:deadbeed"
+ }
+}`))
+ if err == nil {
+ t.Errorf("Expected error parsing manifest, but got: %v", bad)
+ }
+}
+
+func TestParseIndexManifest(t *testing.T) {
+ got, err := ParseIndexManifest(strings.NewReader(`{}`))
+ if err != nil {
+ t.Errorf("Unexpected error parsing manifest: %v", err)
+ }
+
+ want := IndexManifest{}
+ if diff := cmp.Diff(want, *got); diff != "" {
+ t.Errorf("ParseIndexManifest({}); (-want +got) %s", diff)
+ }
+
+ if got, err := ParseIndexManifest(strings.NewReader("{")); err == nil {
+ t.Errorf("expected error, got: %v", got)
+ }
+}
diff --git a/pkg/v1/match/match.go b/pkg/v1/match/match.go
new file mode 100644
index 0000000..98b1ff9
--- /dev/null
+++ b/pkg/v1/match/match.go
@@ -0,0 +1,92 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package match provides functionality for conveniently matching a v1.Descriptor.
+package match
+
+import (
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ imagespec "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// Matcher function that is given a v1.Descriptor, and returns whether or
+// not it matches a given rule. Can match on anything it wants in the Descriptor.
+type Matcher func(desc v1.Descriptor) bool
+
+// Name returns a match.Matcher that matches based on the value of the
+//
+// "org.opencontainers.image.ref.name" annotation:
+//
+// github.com/opencontainers/image-spec/blob/v1.0.1/annotations.md#pre-defined-annotation-keys
+func Name(name string) Matcher {
+ return Annotation(imagespec.AnnotationRefName, name)
+}
+
+// Annotation returns a match.Matcher that matches based on the provided annotation.
+func Annotation(key, value string) Matcher {
+ return func(desc v1.Descriptor) bool {
+ if desc.Annotations == nil {
+ return false
+ }
+ if aValue, ok := desc.Annotations[key]; ok && aValue == value {
+ return true
+ }
+ return false
+ }
+}
+
+// Platforms returns a match.Matcher that matches on any one of the provided platforms.
+// Ignores any descriptors that do not have a platform.
+func Platforms(platforms ...v1.Platform) Matcher {
+ return func(desc v1.Descriptor) bool {
+ if desc.Platform == nil {
+ return false
+ }
+ for _, platform := range platforms {
+ if desc.Platform.Equals(platform) {
+ return true
+ }
+ }
+ return false
+ }
+}
+
+// MediaTypes returns a match.Matcher that matches at least one of the provided media types.
+func MediaTypes(mediaTypes ...string) Matcher {
+ mts := map[string]bool{}
+ for _, media := range mediaTypes {
+ mts[media] = true
+ }
+ return func(desc v1.Descriptor) bool {
+ if desc.MediaType == "" {
+ return false
+ }
+ if _, ok := mts[string(desc.MediaType)]; ok {
+ return true
+ }
+ return false
+ }
+}
+
+// Digests returns a match.Matcher that matches at least one of the provided Digests
+func Digests(digests ...v1.Hash) Matcher {
+ digs := map[v1.Hash]bool{}
+ for _, digest := range digests {
+ digs[digest] = true
+ }
+ return func(desc v1.Descriptor) bool {
+ _, ok := digs[desc.Digest]
+ return ok
+ }
+}
diff --git a/pkg/v1/match/match_test.go b/pkg/v1/match/match_test.go
new file mode 100644
index 0000000..c54319d
--- /dev/null
+++ b/pkg/v1/match/match_test.go
@@ -0,0 +1,131 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package match_test
+
+import (
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/match"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ imagespec "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+func TestName(t *testing.T) {
+ tests := []struct {
+ desc v1.Descriptor
+ name string
+ match bool
+ }{
+ {v1.Descriptor{Annotations: map[string]string{imagespec.AnnotationRefName: "foo"}}, "foo", true},
+ {v1.Descriptor{Annotations: map[string]string{imagespec.AnnotationRefName: "foo"}}, "bar", false},
+ {v1.Descriptor{Annotations: map[string]string{}}, "bar", false},
+ {v1.Descriptor{Annotations: nil}, "bar", false},
+ {v1.Descriptor{}, "bar", false},
+ }
+ for i, tt := range tests {
+ f := match.Name(tt.name)
+ if match := f(tt.desc); match != tt.match {
+ t.Errorf("%d: mismatched, got %v expected %v for desc %#v name %s", i, match, tt.match, tt.desc, tt.name)
+ }
+ }
+}
+
+func TestAnnotation(t *testing.T) {
+ tests := []struct {
+ desc v1.Descriptor
+ key string
+ value string
+ match bool
+ }{
+ {v1.Descriptor{Annotations: map[string]string{"foo": "bar"}}, "foo", "bar", true},
+ {v1.Descriptor{Annotations: map[string]string{"foo": "bar"}}, "bar", "foo", false},
+ {v1.Descriptor{Annotations: map[string]string{}}, "foo", "bar", false},
+ {v1.Descriptor{Annotations: nil}, "foo", "bar", false},
+ {v1.Descriptor{}, "foo", "bar", false},
+ }
+ for i, tt := range tests {
+ f := match.Annotation(tt.key, tt.value)
+ if match := f(tt.desc); match != tt.match {
+ t.Errorf("%d: mismatched, got %v expected %v for desc %#v annotation %s:%s", i, match, tt.match, tt.desc, tt.key, tt.value)
+ }
+ }
+}
+
+func TestPlatforms(t *testing.T) {
+ tests := []struct {
+ desc v1.Descriptor
+ platforms []v1.Platform
+ match bool
+ }{
+ {v1.Descriptor{Platform: &v1.Platform{Architecture: "amd64", OS: "linux"}}, []v1.Platform{{Architecture: "amd64", OS: "darwin"}, {Architecture: "amd64", OS: "linux"}}, true},
+ {v1.Descriptor{Platform: &v1.Platform{Architecture: "amd64", OS: "linux"}}, []v1.Platform{{Architecture: "arm64", OS: "linux"}, {Architecture: "s390x", OS: "linux"}}, false},
+ {v1.Descriptor{Platform: &v1.Platform{OS: "linux"}}, []v1.Platform{{Architecture: "arm64", OS: "linux"}}, false},
+ {v1.Descriptor{Platform: &v1.Platform{}}, []v1.Platform{{Architecture: "arm64", OS: "linux"}}, false},
+ {v1.Descriptor{Platform: nil}, []v1.Platform{{Architecture: "arm64", OS: "linux"}}, false},
+ {v1.Descriptor{}, []v1.Platform{{Architecture: "arm64", OS: "linux"}}, false},
+ }
+ for i, tt := range tests {
+ f := match.Platforms(tt.platforms...)
+ if match := f(tt.desc); match != tt.match {
+ t.Errorf("%d: mismatched, got %v expected %v for desc %#v platform %#v", i, match, tt.match, tt.desc, tt.platforms)
+ }
+ }
+}
+
+func TestMediaTypes(t *testing.T) {
+ tests := []struct {
+ desc v1.Descriptor
+ mediaTypes []string
+ match bool
+ }{
+ {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{string(types.OCIImageIndex)}, true},
+ {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{string(types.OCIManifestSchema1)}, false},
+ {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{string(types.OCIManifestSchema1), string(types.OCIImageIndex)}, true},
+ {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{"a", "b"}, false},
+ {v1.Descriptor{}, []string{string(types.OCIManifestSchema1), string(types.OCIImageIndex)}, false},
+ }
+ for i, tt := range tests {
+ f := match.MediaTypes(tt.mediaTypes...)
+ if match := f(tt.desc); match != tt.match {
+ t.Errorf("%d: mismatched, got %v expected %v for desc %#v mediaTypes %#v", i, match, tt.match, tt.desc, tt.mediaTypes)
+ }
+ }
+}
+
+func TestDigests(t *testing.T) {
+ hashes := []string{
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "abcde1111111222f0123456789abcdef0123456789abcdef0123456789abcdef",
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ }
+ algo := "sha256"
+
+ tests := []struct {
+ desc v1.Descriptor
+ digests []v1.Hash
+ match bool
+ }{
+ {v1.Descriptor{Digest: v1.Hash{Algorithm: algo, Hex: hashes[0]}}, []v1.Hash{{Algorithm: algo, Hex: hashes[0]}, {Algorithm: algo, Hex: hashes[1]}}, true},
+ {v1.Descriptor{Digest: v1.Hash{Algorithm: algo, Hex: hashes[1]}}, []v1.Hash{{Algorithm: algo, Hex: hashes[0]}, {Algorithm: algo, Hex: hashes[1]}}, true},
+ {v1.Descriptor{Digest: v1.Hash{Algorithm: algo, Hex: hashes[2]}}, []v1.Hash{{Algorithm: algo, Hex: hashes[0]}, {Algorithm: algo, Hex: hashes[1]}}, false},
+ }
+ for i, tt := range tests {
+ f := match.Digests(tt.digests...)
+ if match := f(tt.desc); match != tt.match {
+ t.Errorf("%d: mismatched, got %v expected %v for desc %#v digests %#v", i, match, tt.match, tt.desc, tt.digests)
+ }
+ }
+}
diff --git a/pkg/v1/mutate/README.md b/pkg/v1/mutate/README.md
new file mode 100644
index 0000000..19e1612
--- /dev/null
+++ b/pkg/v1/mutate/README.md
@@ -0,0 +1,56 @@
+# `mutate`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate)
+
+The `v1.Image`, `v1.ImageIndex`, and `v1.Layer` interfaces provide only
+accessor methods, so they are essentially immutable. If you want to change
+something about them, you need to produce a new instance of that interface.
+
+A common use case for this library is to read an image from somewhere (a source),
+change something about it, and write the image somewhere else (a sink).
+
+Graphically, this looks something like:
+
+<p align="center">
+ <img src="/images/mutate.dot.svg" />
+</p>
+
+## Mutations
+
+This is obviously not a comprehensive set of useful transformations (PRs welcome!),
+but a rough summary of what the `mutate` package currently does:
+
+### `Config` and `ConfigFile`
+
+These allow you to change the [image configuration](https://github.com/opencontainers/image-spec/blob/master/config.md#properties),
+e.g. to change the entrypoint, environment, author, etc.
+
+### `Time`, `Canonical`, and `CreatedAt`
+
+These are useful in the context of [reproducible builds](https://reproducible-builds.org/),
+where you may want to strip timestamps and other non-reproducible information.
+
+### `Append`, `AppendLayers`, and `AppendManifests`
+
+These functions allow the extension of a `v1.Image` or `v1.ImageIndex` with
+new layers or manifests.
+
+For constructing an image `FROM scratch`, see the [`empty`](/pkg/v1/empty) package.
+
+### `MediaType` and `IndexMediaType`
+
+Sometimes, it is necessary to change the media type of an image or index,
+e.g. to appease a registry with strict validation of images (_looking at you, GCR_).
+
+### `Rebase`
+
+Rebase has [its own README](/cmd/crane/rebase.md).
+
+This is the underlying implementation of [`crane rebase`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane_rebase.md).
+
+### `Extract`
+
+Extract will flatten an image filesystem into a single tar stream,
+respecting whiteout files.
+
+This is the underlying implementation of [`crane export`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane_export.md).
diff --git a/pkg/v1/mutate/doc.go b/pkg/v1/mutate/doc.go
new file mode 100644
index 0000000..dfbd995
--- /dev/null
+++ b/pkg/v1/mutate/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package mutate provides facilities for mutating v1.Images of any kind.
+package mutate
diff --git a/pkg/v1/mutate/image.go b/pkg/v1/mutate/image.go
new file mode 100644
index 0000000..727abe2
--- /dev/null
+++ b/pkg/v1/mutate/image.go
@@ -0,0 +1,287 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mutate
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type image struct {
+ base v1.Image
+ adds []Addendum
+
+ computed bool
+ configFile *v1.ConfigFile
+ manifest *v1.Manifest
+ annotations map[string]string
+ mediaType *types.MediaType
+ configMediaType *types.MediaType
+ diffIDMap map[v1.Hash]v1.Layer
+ digestMap map[v1.Hash]v1.Layer
+ subject *v1.Descriptor
+}
+
+var _ v1.Image = (*image)(nil)
+
+func (i *image) MediaType() (types.MediaType, error) {
+ if i.mediaType != nil {
+ return *i.mediaType, nil
+ }
+ return i.base.MediaType()
+}
+
+func (i *image) compute() error {
+ // Don't re-compute if already computed.
+ if i.computed {
+ return nil
+ }
+ var configFile *v1.ConfigFile
+ if i.configFile != nil {
+ configFile = i.configFile
+ } else {
+ cf, err := i.base.ConfigFile()
+ if err != nil {
+ return err
+ }
+ configFile = cf.DeepCopy()
+ }
+ diffIDs := configFile.RootFS.DiffIDs
+ history := configFile.History
+
+ diffIDMap := make(map[v1.Hash]v1.Layer)
+ digestMap := make(map[v1.Hash]v1.Layer)
+
+ for _, add := range i.adds {
+ history = append(history, add.History)
+ if add.Layer != nil {
+ diffID, err := add.Layer.DiffID()
+ if err != nil {
+ return err
+ }
+ diffIDs = append(diffIDs, diffID)
+ diffIDMap[diffID] = add.Layer
+ }
+ }
+
+ m, err := i.base.Manifest()
+ if err != nil {
+ return err
+ }
+ manifest := m.DeepCopy()
+ manifestLayers := manifest.Layers
+ for _, add := range i.adds {
+ if add.Layer == nil {
+ // Empty layers include only history in manifest.
+ continue
+ }
+
+ desc, err := partial.Descriptor(add.Layer)
+ if err != nil {
+ return err
+ }
+
+ // Fields in the addendum override the original descriptor.
+ if len(add.Annotations) != 0 {
+ desc.Annotations = add.Annotations
+ }
+ if len(add.URLs) != 0 {
+ desc.URLs = add.URLs
+ }
+
+ if add.MediaType != "" {
+ desc.MediaType = add.MediaType
+ }
+
+ manifestLayers = append(manifestLayers, *desc)
+ digestMap[desc.Digest] = add.Layer
+ }
+
+ configFile.RootFS.DiffIDs = diffIDs
+ configFile.History = history
+
+ manifest.Layers = manifestLayers
+
+ rcfg, err := json.Marshal(configFile)
+ if err != nil {
+ return err
+ }
+ d, sz, err := v1.SHA256(bytes.NewBuffer(rcfg))
+ if err != nil {
+ return err
+ }
+ manifest.Config.Digest = d
+ manifest.Config.Size = sz
+
+ // If Data was set in the base image, we need to update it in the mutated image.
+ if m.Config.Data != nil {
+ manifest.Config.Data = rcfg
+ }
+
+ // If the user wants to mutate the media type of the config
+ if i.configMediaType != nil {
+ manifest.Config.MediaType = *i.configMediaType
+ }
+
+ if i.mediaType != nil {
+ manifest.MediaType = *i.mediaType
+ }
+
+ if i.annotations != nil {
+ if manifest.Annotations == nil {
+ manifest.Annotations = map[string]string{}
+ }
+
+ for k, v := range i.annotations {
+ manifest.Annotations[k] = v
+ }
+ }
+ manifest.Subject = i.subject
+
+ i.configFile = configFile
+ i.manifest = manifest
+ i.diffIDMap = diffIDMap
+ i.digestMap = digestMap
+ i.computed = true
+ return nil
+}
+
+// Layers returns the ordered collection of filesystem layers that comprise this image.
+// The order of the list is oldest/base layer first, and most-recent/top layer last.
+func (i *image) Layers() ([]v1.Layer, error) {
+ if err := i.compute(); errors.Is(err, stream.ErrNotComputed) {
+ // Image contains a streamable layer which has not yet been
+ // consumed. Just return the layers we have in case the caller
+ // is going to consume the layers.
+ layers, err := i.base.Layers()
+ if err != nil {
+ return nil, err
+ }
+ for _, add := range i.adds {
+ layers = append(layers, add.Layer)
+ }
+ return layers, nil
+ } else if err != nil {
+ return nil, err
+ }
+
+ diffIDs, err := partial.DiffIDs(i)
+ if err != nil {
+ return nil, err
+ }
+ ls := make([]v1.Layer, 0, len(diffIDs))
+ for _, h := range diffIDs {
+ l, err := i.LayerByDiffID(h)
+ if err != nil {
+ return nil, err
+ }
+ ls = append(ls, l)
+ }
+ return ls, nil
+}
+
+// ConfigName returns the hash of the image's config file.
+func (i *image) ConfigName() (v1.Hash, error) {
+ if err := i.compute(); err != nil {
+ return v1.Hash{}, err
+ }
+ return partial.ConfigName(i)
+}
+
+// ConfigFile returns this image's config file.
+func (i *image) ConfigFile() (*v1.ConfigFile, error) {
+ if err := i.compute(); err != nil {
+ return nil, err
+ }
+ return i.configFile.DeepCopy(), nil
+}
+
+// RawConfigFile returns the serialized bytes of ConfigFile()
+func (i *image) RawConfigFile() ([]byte, error) {
+ if err := i.compute(); err != nil {
+ return nil, err
+ }
+ return json.Marshal(i.configFile)
+}
+
+// Digest returns the sha256 of this image's manifest.
+func (i *image) Digest() (v1.Hash, error) {
+ if err := i.compute(); err != nil {
+ return v1.Hash{}, err
+ }
+ return partial.Digest(i)
+}
+
+// Size implements v1.Image.
+func (i *image) Size() (int64, error) {
+ if err := i.compute(); err != nil {
+ return -1, err
+ }
+ return partial.Size(i)
+}
+
+// Manifest returns this image's Manifest object.
+func (i *image) Manifest() (*v1.Manifest, error) {
+ if err := i.compute(); err != nil {
+ return nil, err
+ }
+ return i.manifest.DeepCopy(), nil
+}
+
+// RawManifest returns the serialized bytes of Manifest()
+func (i *image) RawManifest() ([]byte, error) {
+ if err := i.compute(); err != nil {
+ return nil, err
+ }
+ return json.Marshal(i.manifest)
+}
+
+// LayerByDigest returns a Layer for interacting with a particular layer of
+// the image, looking it up by "digest" (the compressed hash).
+func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) {
+ if cn, err := i.ConfigName(); err != nil {
+ return nil, err
+ } else if h == cn {
+ return partial.ConfigLayer(i)
+ }
+ if layer, ok := i.digestMap[h]; ok {
+ return layer, nil
+ }
+ return i.base.LayerByDigest(h)
+}
+
+// LayerByDiffID is an analog to LayerByDigest, looking up by "diff id"
+// (the uncompressed hash).
+func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
+ if layer, ok := i.diffIDMap[h]; ok {
+ return layer, nil
+ }
+ return i.base.LayerByDiffID(h)
+}
+
+func validate(adds []Addendum) error {
+ for _, add := range adds {
+ if add.Layer == nil && !add.History.EmptyLayer {
+ return errors.New("unable to add a nil layer to the image")
+ }
+ }
+ return nil
+}
diff --git a/pkg/v1/mutate/index.go b/pkg/v1/mutate/index.go
new file mode 100644
index 0000000..ba062f9
--- /dev/null
+++ b/pkg/v1/mutate/index.go
@@ -0,0 +1,204 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mutate
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/google/go-containerregistry/pkg/logs"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/match"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func computeDescriptor(ia IndexAddendum) (*v1.Descriptor, error) {
+ desc, err := partial.Descriptor(ia.Add)
+ if err != nil {
+ return nil, err
+ }
+
+ // The IndexAddendum allows overriding Descriptor values.
+ if ia.Descriptor.Size != 0 {
+ desc.Size = ia.Descriptor.Size
+ }
+ if string(ia.Descriptor.MediaType) != "" {
+ desc.MediaType = ia.Descriptor.MediaType
+ }
+ if ia.Descriptor.Digest != (v1.Hash{}) {
+ desc.Digest = ia.Descriptor.Digest
+ }
+ if ia.Descriptor.Platform != nil {
+ desc.Platform = ia.Descriptor.Platform
+ }
+ if len(ia.Descriptor.URLs) != 0 {
+ desc.URLs = ia.Descriptor.URLs
+ }
+ if len(ia.Descriptor.Annotations) != 0 {
+ desc.Annotations = ia.Descriptor.Annotations
+ }
+ if ia.Descriptor.Data != nil {
+ desc.Data = ia.Descriptor.Data
+ }
+
+ return desc, nil
+}
+
+type index struct {
+ base v1.ImageIndex
+ adds []IndexAddendum
+ // remove is removed before adds
+ remove match.Matcher
+
+ computed bool
+ manifest *v1.IndexManifest
+ annotations map[string]string
+ mediaType *types.MediaType
+ imageMap map[v1.Hash]v1.Image
+ indexMap map[v1.Hash]v1.ImageIndex
+ layerMap map[v1.Hash]v1.Layer
+ subject *v1.Descriptor
+}
+
+var _ v1.ImageIndex = (*index)(nil)
+
+func (i *index) MediaType() (types.MediaType, error) {
+ if i.mediaType != nil {
+ return *i.mediaType, nil
+ }
+ return i.base.MediaType()
+}
+
+func (i *index) Size() (int64, error) { return partial.Size(i) }
+
+func (i *index) compute() error {
+ // Don't re-compute if already computed.
+ if i.computed {
+ return nil
+ }
+
+ i.imageMap = make(map[v1.Hash]v1.Image)
+ i.indexMap = make(map[v1.Hash]v1.ImageIndex)
+ i.layerMap = make(map[v1.Hash]v1.Layer)
+
+ m, err := i.base.IndexManifest()
+ if err != nil {
+ return err
+ }
+ manifest := m.DeepCopy()
+ manifests := manifest.Manifests
+
+ if i.remove != nil {
+ var cleanedManifests []v1.Descriptor
+ for _, m := range manifests {
+ if !i.remove(m) {
+ cleanedManifests = append(cleanedManifests, m)
+ }
+ }
+ manifests = cleanedManifests
+ }
+
+ for _, add := range i.adds {
+ desc, err := computeDescriptor(add)
+ if err != nil {
+ return err
+ }
+
+ manifests = append(manifests, *desc)
+ if idx, ok := add.Add.(v1.ImageIndex); ok {
+ i.indexMap[desc.Digest] = idx
+ } else if img, ok := add.Add.(v1.Image); ok {
+ i.imageMap[desc.Digest] = img
+ } else if l, ok := add.Add.(v1.Layer); ok {
+ i.layerMap[desc.Digest] = l
+ } else {
+ logs.Warn.Printf("Unexpected index addendum: %T", add.Add)
+ }
+ }
+
+ manifest.Manifests = manifests
+
+ if i.mediaType != nil {
+ manifest.MediaType = *i.mediaType
+ }
+
+ if i.annotations != nil {
+ if manifest.Annotations == nil {
+ manifest.Annotations = map[string]string{}
+ }
+ for k, v := range i.annotations {
+ manifest.Annotations[k] = v
+ }
+ }
+ manifest.Subject = i.subject
+
+ i.manifest = manifest
+ i.computed = true
+ return nil
+}
+
+func (i *index) Image(h v1.Hash) (v1.Image, error) {
+ if img, ok := i.imageMap[h]; ok {
+ return img, nil
+ }
+ return i.base.Image(h)
+}
+
+func (i *index) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
+ if idx, ok := i.indexMap[h]; ok {
+ return idx, nil
+ }
+ return i.base.ImageIndex(h)
+}
+
+type withLayer interface {
+ Layer(v1.Hash) (v1.Layer, error)
+}
+
+// Workaround for #819.
+func (i *index) Layer(h v1.Hash) (v1.Layer, error) {
+ if layer, ok := i.layerMap[h]; ok {
+ return layer, nil
+ }
+ if wl, ok := i.base.(withLayer); ok {
+ return wl.Layer(h)
+ }
+ return nil, fmt.Errorf("layer not found: %s", h)
+}
+
+// Digest returns the sha256 of this image's manifest.
+func (i *index) Digest() (v1.Hash, error) {
+ if err := i.compute(); err != nil {
+ return v1.Hash{}, err
+ }
+ return partial.Digest(i)
+}
+
+// Manifest returns this image's Manifest object.
+func (i *index) IndexManifest() (*v1.IndexManifest, error) {
+ if err := i.compute(); err != nil {
+ return nil, err
+ }
+ return i.manifest.DeepCopy(), nil
+}
+
+// RawManifest returns the serialized bytes of Manifest()
+func (i *index) RawManifest() ([]byte, error) {
+ if err := i.compute(); err != nil {
+ return nil, err
+ }
+ return json.Marshal(i.manifest)
+}
diff --git a/pkg/v1/mutate/index_test.go b/pkg/v1/mutate/index_test.go
new file mode 100644
index 0000000..1f542d1
--- /dev/null
+++ b/pkg/v1/mutate/index_test.go
@@ -0,0 +1,235 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mutate_test
+
+import (
+ "log"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestAppendIndex(t *testing.T) {
+ base, err := random.Index(1024, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ idx, err := random.Index(2048, 1, 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ img, err := random.Image(4096, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ l, err := random.Layer(1024, types.OCIUncompressedRestrictedLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ weirdHash := v1.Hash{
+ Algorithm: "sha256",
+ Hex: strings.Repeat("0", 64),
+ }
+
+ add := mutate.AppendManifests(base, mutate.IndexAddendum{
+ Add: idx,
+ Descriptor: v1.Descriptor{
+ URLs: []string{"index.example.com"},
+ },
+ }, mutate.IndexAddendum{
+ Add: img,
+ Descriptor: v1.Descriptor{
+ URLs: []string{"image.example.com"},
+ },
+ }, mutate.IndexAddendum{
+ Add: l,
+ Descriptor: v1.Descriptor{
+ MediaType: types.MediaType("application/xml"),
+ URLs: []string{"blob.example.com"},
+ },
+ }, mutate.IndexAddendum{
+ Add: l,
+ Descriptor: v1.Descriptor{
+ URLs: []string{"layer.example.com"},
+ Size: 1337,
+ Digest: weirdHash,
+ Platform: &v1.Platform{
+ OS: "haiku",
+ Architecture: "toaster",
+ },
+ Annotations: map[string]string{"weird": "true"},
+ },
+ })
+
+ if err := validate.Index(add); err != nil {
+ t.Errorf("Validate() = %v", err)
+ }
+
+ got, err := add.MediaType()
+ if err != nil {
+ t.Fatal(err)
+ }
+ want, err := base.MediaType()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got != want {
+ t.Errorf("MediaType() = %s != %s", got, want)
+ }
+
+ // TODO(jonjohnsonjr): There's no way to grab layers from v1.ImageIndex.
+ m, err := add.IndexManifest()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for i, want := range map[int]string{
+ 3: "index.example.com",
+ 4: "image.example.com",
+ 5: "blob.example.com",
+ 6: "layer.example.com",
+ } {
+ if got := m.Manifests[i].URLs[0]; got != want {
+ t.Errorf("wrong URLs[0] for Manifests[%d]: %s != %s", i, got, want)
+ }
+ }
+
+ if got, want := m.Manifests[5].MediaType, types.MediaType("application/xml"); got != want {
+ t.Errorf("wrong MediaType for layer: %s != %s", got, want)
+ }
+
+ if got, want := m.Manifests[6].MediaType, types.OCIUncompressedRestrictedLayer; got != want {
+ t.Errorf("wrong MediaType for layer: %s != %s", got, want)
+ }
+
+ // Append the index to itself and make sure it still validates.
+ add = mutate.AppendManifests(add, mutate.IndexAddendum{
+ Add: add,
+ })
+ if err := validate.Index(add); err != nil {
+ t.Errorf("Validate() = %v", err)
+ }
+
+ // Wrap the whole thing in another index and make sure it still validates.
+ add = mutate.AppendManifests(empty.Index, mutate.IndexAddendum{
+ Add: add,
+ })
+ if err := validate.Index(add); err != nil {
+ t.Errorf("Validate() = %v", err)
+ }
+}
+
+func TestIndexImmutability(t *testing.T) {
+ base, err := random.Index(1024, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ii, err := random.Index(2048, 1, 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ i, err := random.Image(4096, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ idx := mutate.AppendManifests(base, mutate.IndexAddendum{
+ Add: ii,
+ Descriptor: v1.Descriptor{
+ URLs: []string{"index.example.com"},
+ },
+ }, mutate.IndexAddendum{
+ Add: i,
+ Descriptor: v1.Descriptor{
+ URLs: []string{"image.example.com"},
+ },
+ })
+
+ t.Run("index manifest", func(t *testing.T) {
+ // Check that Manifest is immutable.
+ changed, err := idx.IndexManifest()
+ if err != nil {
+ t.Errorf("IndexManifest() = %v", err)
+ }
+ want := changed.DeepCopy() // Create a copy of original before mutating it.
+ changed.MediaType = types.DockerManifestList
+
+ if got, err := idx.IndexManifest(); err != nil {
+ t.Errorf("IndexManifest() = %v", err)
+ } else if !cmp.Equal(got, want) {
+ t.Errorf("IndexManifest changed! %s", cmp.Diff(got, want))
+ }
+ })
+}
+
+// TestAppend_ArtifactType tests that appending an image manifest that has a
+// non-standard config.mediaType to an index, results in the image's
+// config.mediaType being hoisted into the descriptor inside the index,
+// as artifactType.
+func TestAppend_ArtifactType(t *testing.T) {
+ for _, c := range []struct {
+ desc, configMediaType, wantArtifactType string
+ }{{
+ desc: "standard config.mediaType, no artifactType",
+ configMediaType: string(types.DockerConfigJSON),
+ wantArtifactType: "",
+ }, {
+ desc: "non-standard config.mediaType, want artifactType",
+ configMediaType: "application/vnd.custom.something",
+ wantArtifactType: "application/vnd.custom.something",
+ }} {
+ t.Run(c.desc, func(t *testing.T) {
+ img, err := random.Image(1, 1)
+ if err != nil {
+ t.Fatalf("random.Image: %v", err)
+ }
+ img = mutate.ConfigMediaType(img, types.MediaType(c.configMediaType))
+ idx := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{
+ Add: img,
+ })
+ mf, err := idx.IndexManifest()
+ if err != nil {
+ t.Fatalf("IndexManifest: %v", err)
+ }
+ if got := mf.Manifests[0].ArtifactType; got != c.wantArtifactType {
+ t.Errorf("manifest artifactType: got %q, want %q", got, c.wantArtifactType)
+ }
+
+ desc, err := partial.Descriptor(img)
+ if err != nil {
+ t.Fatalf("partial.Descriptor: %v", err)
+ }
+ if got := desc.ArtifactType; got != c.wantArtifactType {
+ t.Errorf("descriptor artifactType: got %q, want %q", got, c.wantArtifactType)
+ }
+
+ gotAT, err := partial.ArtifactType(img)
+ if err != nil {
+ t.Fatalf("partial.ArtifactType: %v", err)
+ }
+ if gotAT != c.wantArtifactType {
+ t.Errorf("partial.ArtifactType: got %q, want %q", gotAT, c.wantArtifactType)
+ }
+ })
+ }
+}
diff --git a/pkg/v1/mutate/mutate.go b/pkg/v1/mutate/mutate.go
new file mode 100644
index 0000000..e4a0e52
--- /dev/null
+++ b/pkg/v1/mutate/mutate.go
@@ -0,0 +1,553 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mutate
+
+import (
+ "archive/tar"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/google/go-containerregistry/internal/gzip"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/match"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+const whiteoutPrefix = ".wh."
+
+// Addendum contains layers and history to be appended
+// to a base image
+type Addendum struct {
+ Layer v1.Layer
+ History v1.History
+ URLs []string
+ Annotations map[string]string
+ MediaType types.MediaType
+}
+
+// AppendLayers applies layers to a base image.
+func AppendLayers(base v1.Image, layers ...v1.Layer) (v1.Image, error) {
+ additions := make([]Addendum, 0, len(layers))
+ for _, layer := range layers {
+ additions = append(additions, Addendum{Layer: layer})
+ }
+
+ return Append(base, additions...)
+}
+
+// Append will apply the list of addendums to the base image
+func Append(base v1.Image, adds ...Addendum) (v1.Image, error) {
+ if len(adds) == 0 {
+ return base, nil
+ }
+ if err := validate(adds); err != nil {
+ return nil, err
+ }
+
+ return &image{
+ base: base,
+ adds: adds,
+ }, nil
+}
+
+// Appendable is an interface that represents something that can be appended
+// to an ImageIndex. We need to be able to construct a v1.Descriptor in order
+// to append something, and this is the minimum required information for that.
+type Appendable interface {
+ MediaType() (types.MediaType, error)
+ Digest() (v1.Hash, error)
+ Size() (int64, error)
+}
+
+// IndexAddendum represents an appendable thing and all the properties that
+// we may want to override in the resulting v1.Descriptor.
+type IndexAddendum struct {
+ Add Appendable
+ v1.Descriptor
+}
+
+// AppendManifests appends a manifest to the ImageIndex.
+func AppendManifests(base v1.ImageIndex, adds ...IndexAddendum) v1.ImageIndex {
+ return &index{
+ base: base,
+ adds: adds,
+ }
+}
+
+// RemoveManifests removes any descriptors that match the match.Matcher.
+func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) v1.ImageIndex {
+ return &index{
+ base: base,
+ remove: matcher,
+ }
+}
+
+// Config mutates the provided v1.Image to have the provided v1.Config
+func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
+ cf, err := base.ConfigFile()
+ if err != nil {
+ return nil, err
+ }
+
+ cf.Config = cfg
+
+ return ConfigFile(base, cf)
+}
+
+// Subject mutates the subject on an image or index manifest.
+//
+// The input is expected to be a v1.Image or v1.ImageIndex, and
+// returns the same type. You can type-assert the result like so:
+//
+// img := Subject(empty.Image, subj).(v1.Image)
+//
+// Or for an index:
+//
+// idx := Subject(empty.Index, subj).(v1.ImageIndex)
+//
+// If the input is not an Image or ImageIndex, the result will
+// attempt to lazily annotate the raw manifest.
+func Subject(f partial.WithRawManifest, subject v1.Descriptor) partial.WithRawManifest {
+ if img, ok := f.(v1.Image); ok {
+ return &image{
+ base: img,
+ subject: &subject,
+ }
+ }
+ if idx, ok := f.(v1.ImageIndex); ok {
+ return &index{
+ base: idx,
+ subject: &subject,
+ }
+ }
+ return arbitraryRawManifest{a: f, subject: &subject}
+}
+
+// Annotations mutates the annotations on an annotatable image or index manifest.
+//
+// The annotatable input is expected to be a v1.Image or v1.ImageIndex, and
+// returns the same type. You can type-assert the result like so:
+//
+// img := Annotations(empty.Image, map[string]string{
+// "foo": "bar",
+// }).(v1.Image)
+//
+// Or for an index:
+//
+// idx := Annotations(empty.Index, map[string]string{
+// "foo": "bar",
+// }).(v1.ImageIndex)
+//
+// If the input Annotatable is not an Image or ImageIndex, the result will
+// attempt to lazily annotate the raw manifest.
+func Annotations(f partial.WithRawManifest, anns map[string]string) partial.WithRawManifest {
+ if img, ok := f.(v1.Image); ok {
+ return &image{
+ base: img,
+ annotations: anns,
+ }
+ }
+ if idx, ok := f.(v1.ImageIndex); ok {
+ return &index{
+ base: idx,
+ annotations: anns,
+ }
+ }
+ return arbitraryRawManifest{a: f, anns: anns}
+}
+
+type arbitraryRawManifest struct {
+ a partial.WithRawManifest
+ anns map[string]string
+ subject *v1.Descriptor
+}
+
+func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
+ b, err := a.a.RawManifest()
+ if err != nil {
+ return nil, err
+ }
+ var m map[string]any
+ if err := json.Unmarshal(b, &m); err != nil {
+ return nil, err
+ }
+ if ann, ok := m["annotations"]; ok {
+ if annm, ok := ann.(map[string]string); ok {
+ for k, v := range a.anns {
+ annm[k] = v
+ }
+ } else {
+ return nil, fmt.Errorf(".annotations is not a map: %T", ann)
+ }
+ } else {
+ m["annotations"] = a.anns
+ }
+ if a.subject != nil {
+ m["subject"] = a.subject
+ }
+ return json.Marshal(m)
+}
+
+// ConfigFile mutates the provided v1.Image to have the provided v1.ConfigFile
+func ConfigFile(base v1.Image, cfg *v1.ConfigFile) (v1.Image, error) {
+ m, err := base.Manifest()
+ if err != nil {
+ return nil, err
+ }
+
+ image := &image{
+ base: base,
+ manifest: m.DeepCopy(),
+ configFile: cfg,
+ }
+
+ return image, nil
+}
+
+// CreatedAt mutates the provided v1.Image to have the provided v1.Time
+func CreatedAt(base v1.Image, created v1.Time) (v1.Image, error) {
+ cf, err := base.ConfigFile()
+ if err != nil {
+ return nil, err
+ }
+
+ cfg := cf.DeepCopy()
+ cfg.Created = created
+
+ return ConfigFile(base, cfg)
+}
+
+// Extract takes an image and returns an io.ReadCloser containing the image's
+// flattened filesystem.
+//
+// Callers can read the filesystem contents by passing the reader to
+// tar.NewReader, or io.Copy it directly to some output.
+//
+// If a caller doesn't read the full contents, they should Close it to free up
+// resources used during extraction.
+func Extract(img v1.Image) io.ReadCloser {
+ pr, pw := io.Pipe()
+
+ go func() {
+ // Close the writer with any errors encountered during
+ // extraction. These errors will be returned by the reader end
+ // on subsequent reads. If err == nil, the reader will return
+ // EOF.
+ pw.CloseWithError(extract(img, pw))
+ }()
+
+ return pr
+}
+
+// Adapted from https://github.com/google/containerregistry/blob/da03b395ccdc4e149e34fbb540483efce962dc64/client/v2_2/docker_image_.py#L816
+func extract(img v1.Image, w io.Writer) error {
+ tarWriter := tar.NewWriter(w)
+ defer tarWriter.Close()
+
+ fileMap := map[string]bool{}
+
+ layers, err := img.Layers()
+ if err != nil {
+ return fmt.Errorf("retrieving image layers: %w", err)
+ }
+
+ // we iterate through the layers in reverse order because it makes handling
+ // whiteout layers more efficient, since we can just keep track of the removed
+ // files as we see .wh. layers and ignore those in previous layers.
+ for i := len(layers) - 1; i >= 0; i-- {
+ layer := layers[i]
+ layerReader, err := layer.Uncompressed()
+ if err != nil {
+ return fmt.Errorf("reading layer contents: %w", err)
+ }
+ defer layerReader.Close()
+ tarReader := tar.NewReader(layerReader)
+ for {
+ header, err := tarReader.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return fmt.Errorf("reading tar: %w", err)
+ }
+
+ // Some tools prepend everything with "./", so if we don't Clean the
+ // name, we may have duplicate entries, which angers tar-split.
+ header.Name = filepath.Clean(header.Name)
+ // force PAX format to remove Name/Linkname length limit of 100 characters
+ // required by USTAR and to not depend on internal tar package guess which
+ // prefers USTAR over PAX
+ header.Format = tar.FormatPAX
+
+ basename := filepath.Base(header.Name)
+ dirname := filepath.Dir(header.Name)
+ tombstone := strings.HasPrefix(basename, whiteoutPrefix)
+ if tombstone {
+ basename = basename[len(whiteoutPrefix):]
+ }
+
+ // check if we have seen value before
+ // if we're checking a directory, don't filepath.Join names
+ var name string
+ if header.Typeflag == tar.TypeDir {
+ name = header.Name
+ } else {
+ name = filepath.Join(dirname, basename)
+ }
+
+ if _, ok := fileMap[name]; ok {
+ continue
+ }
+
+ // check for a whited out parent directory
+ if inWhiteoutDir(fileMap, name) {
+ continue
+ }
+
+ // mark file as handled. non-directory implicitly tombstones
+ // any entries with a matching (or child) name
+ fileMap[name] = tombstone || !(header.Typeflag == tar.TypeDir)
+ if !tombstone {
+ if err := tarWriter.WriteHeader(header); err != nil {
+ return err
+ }
+ if header.Size > 0 {
+ if _, err := io.CopyN(tarWriter, tarReader, header.Size); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func inWhiteoutDir(fileMap map[string]bool, file string) bool {
+ for {
+ if file == "" {
+ break
+ }
+ dirname := filepath.Dir(file)
+ if file == dirname {
+ break
+ }
+ if val, ok := fileMap[dirname]; ok && val {
+ return true
+ }
+ file = dirname
+ }
+ return false
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+// Time sets all timestamps in an image to the given timestamp.
+func Time(img v1.Image, t time.Time) (v1.Image, error) {
+ newImage := empty.Image
+
+ layers, err := img.Layers()
+ if err != nil {
+ return nil, fmt.Errorf("getting image layers: %w", err)
+ }
+
+ ocf, err := img.ConfigFile()
+ if err != nil {
+ return nil, fmt.Errorf("getting original config file: %w", err)
+ }
+
+ addendums := make([]Addendum, max(len(ocf.History), len(layers)))
+ var historyIdx, addendumIdx int
+ for layerIdx := 0; layerIdx < len(layers); addendumIdx, layerIdx = addendumIdx+1, layerIdx+1 {
+ newLayer, err := layerTime(layers[layerIdx], t)
+ if err != nil {
+ return nil, fmt.Errorf("setting layer times: %w", err)
+ }
+
+ // try to search for the history entry that corresponds to this layer
+ for ; historyIdx < len(ocf.History); historyIdx++ {
+ addendums[addendumIdx].History = ocf.History[historyIdx]
+ // if it's an EmptyLayer, do not set the Layer and have the Addendum with just the History
+ // and move on to the next History entry
+ if ocf.History[historyIdx].EmptyLayer {
+ addendumIdx++
+ continue
+ }
+ // otherwise, we can exit from the cycle
+ historyIdx++
+ break
+ }
+ addendums[addendumIdx].Layer = newLayer
+ }
+
+ // add all leftover History entries
+ for ; historyIdx < len(ocf.History); historyIdx, addendumIdx = historyIdx+1, addendumIdx+1 {
+ addendums[addendumIdx].History = ocf.History[historyIdx]
+ }
+
+ newImage, err = Append(newImage, addendums...)
+ if err != nil {
+ return nil, fmt.Errorf("appending layers: %w", err)
+ }
+
+ cf, err := newImage.ConfigFile()
+ if err != nil {
+ return nil, fmt.Errorf("setting config file: %w", err)
+ }
+
+ cfg := cf.DeepCopy()
+
+ // Copy basic config over
+ cfg.Architecture = ocf.Architecture
+ cfg.OS = ocf.OS
+ cfg.OSVersion = ocf.OSVersion
+ cfg.Config = ocf.Config
+
+ // Strip away timestamps from the config file
+ cfg.Created = v1.Time{Time: t}
+
+ for i, h := range cfg.History {
+ h.Created = v1.Time{Time: t}
+ h.CreatedBy = ocf.History[i].CreatedBy
+ h.Comment = ocf.History[i].Comment
+ h.EmptyLayer = ocf.History[i].EmptyLayer
+ // Explicitly ignore Author field; which hinders reproducibility
+ h.Author = ""
+ cfg.History[i] = h
+ }
+
+ return ConfigFile(newImage, cfg)
+}
+
+func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
+ layerReader, err := layer.Uncompressed()
+ if err != nil {
+ return nil, fmt.Errorf("getting layer: %w", err)
+ }
+ defer layerReader.Close()
+ w := new(bytes.Buffer)
+ tarWriter := tar.NewWriter(w)
+ defer tarWriter.Close()
+
+ tarReader := tar.NewReader(layerReader)
+ for {
+ header, err := tarReader.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("reading layer: %w", err)
+ }
+
+ header.ModTime = t
+
+ //PAX and GNU Format support additional timestamps in the header
+ if header.Format == tar.FormatPAX || header.Format == tar.FormatGNU {
+ header.AccessTime = t
+ header.ChangeTime = t
+ }
+
+ if err := tarWriter.WriteHeader(header); err != nil {
+ return nil, fmt.Errorf("writing tar header: %w", err)
+ }
+
+ if header.Typeflag == tar.TypeReg {
+ // TODO(#1168): This should be lazy, and not buffer the entire layer contents.
+ if _, err = io.CopyN(tarWriter, tarReader, header.Size); err != nil {
+ return nil, fmt.Errorf("writing layer file: %w", err)
+ }
+ }
+ }
+
+ if err := tarWriter.Close(); err != nil {
+ return nil, err
+ }
+
+ b := w.Bytes()
+ // gzip the contents, then create the layer
+ opener := func() (io.ReadCloser, error) {
+ return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil
+ }
+ layer, err = tarball.LayerFromOpener(opener)
+ if err != nil {
+ return nil, fmt.Errorf("creating layer: %w", err)
+ }
+
+ return layer, nil
+}
+
+// Canonical is a helper function to combine Time and configFile
+// to remove any randomness during a docker build.
+func Canonical(img v1.Image) (v1.Image, error) {
+ // Set all timestamps to 0
+ created := time.Time{}
+ img, err := Time(img, created)
+ if err != nil {
+ return nil, err
+ }
+
+ cf, err := img.ConfigFile()
+ if err != nil {
+ return nil, err
+ }
+
+ // Get rid of host-dependent random config
+ cfg := cf.DeepCopy()
+
+ cfg.Container = ""
+ cfg.Config.Hostname = ""
+ cfg.DockerVersion = ""
+
+ return ConfigFile(img, cfg)
+}
+
+// MediaType modifies the MediaType() of the given image.
+func MediaType(img v1.Image, mt types.MediaType) v1.Image {
+ return &image{
+ base: img,
+ mediaType: &mt,
+ }
+}
+
+// ConfigMediaType modifies the MediaType() of the given image's Config.
+//
+// If !mt.IsConfig(), this will be the image's artifactType in any indexes it's a part of.
+func ConfigMediaType(img v1.Image, mt types.MediaType) v1.Image {
+ return &image{
+ base: img,
+ configMediaType: &mt,
+ }
+}
+
+// IndexMediaType modifies the MediaType() of the given index.
+func IndexMediaType(idx v1.ImageIndex, mt types.MediaType) v1.ImageIndex {
+ return &index{
+ base: idx,
+ mediaType: &mt,
+ }
+}
diff --git a/pkg/v1/mutate/mutate_test.go b/pkg/v1/mutate/mutate_test.go
new file mode 100644
index 0000000..c4fdba6
--- /dev/null
+++ b/pkg/v1/mutate/mutate_test.go
@@ -0,0 +1,770 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mutate_test
+
+import (
+ "archive/tar"
+ "bytes"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/match"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestExtractWhiteout(t *testing.T) {
+ img, err := tarball.ImageFromPath("testdata/whiteout_image.tar", nil)
+ if err != nil {
+ t.Errorf("Error loading image: %v", err)
+ }
+ tarPath, _ := filepath.Abs("img.tar")
+ defer os.Remove(tarPath)
+ tr := tar.NewReader(mutate.Extract(img))
+ for {
+ header, err := tr.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ name := header.Name
+ for _, part := range filepath.SplitList(name) {
+ if part == "foo" {
+ t.Errorf("whiteout file found in tar: %v", name)
+ }
+ }
+ }
+}
+
+func TestExtractOverwrittenFile(t *testing.T) {
+ img, err := tarball.ImageFromPath("testdata/overwritten_file.tar", nil)
+ if err != nil {
+ t.Fatalf("Error loading image: %v", err)
+ }
+ tr := tar.NewReader(mutate.Extract(img))
+ for {
+ header, err := tr.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ name := header.Name
+ if strings.Contains(name, "foo.txt") {
+ var buf bytes.Buffer
+ buf.ReadFrom(tr)
+ if strings.Contains(buf.String(), "foo") {
+ t.Errorf("Contents of file were not correctly overwritten")
+ }
+ }
+ }
+}
+
+// TestExtractError tests that if there are any errors encountered
+func TestExtractError(t *testing.T) {
+ rc := mutate.Extract(invalidImage{})
+ if _, err := io.Copy(io.Discard, rc); err == nil {
+ t.Errorf("rc.Read; got nil error")
+ } else if !strings.Contains(err.Error(), errInvalidImage.Error()) {
+ t.Errorf("rc.Read; got %v, want %v", err, errInvalidImage)
+ }
+}
+
+// TestExtractPartialRead tests that the reader can be partially read (e.g.,
+// tar headers) and closed without error.
+func TestExtractPartialRead(t *testing.T) {
+ rc := mutate.Extract(invalidImage{})
+ if _, err := io.Copy(io.Discard, io.LimitReader(rc, 1)); err != nil {
+ t.Errorf("Could not read one byte from reader")
+ }
+ if err := rc.Close(); err != nil {
+ t.Errorf("rc.Close: %v", err)
+ }
+}
+
+// invalidImage is an image which returns an error when Layers() is called.
+type invalidImage struct {
+ v1.Image
+}
+
+var errInvalidImage = errors.New("invalid image")
+
+func (invalidImage) Layers() ([]v1.Layer, error) {
+ return nil, errInvalidImage
+}
+
+func TestNoopCondition(t *testing.T) {
+ source := sourceImage(t)
+
+ result, err := mutate.AppendLayers(source, []v1.Layer{}...)
+ if err != nil {
+ t.Fatalf("Unexpected error creating a writable image: %v", err)
+ }
+
+ if !manifestsAreEqual(t, source, result) {
+ t.Error("manifests are not the same")
+ }
+
+ if !configFilesAreEqual(t, source, result) {
+ t.Fatal("config files are not the same")
+ }
+}
+
+func TestAppendWithAddendum(t *testing.T) {
+ source := sourceImage(t)
+
+ addendum := mutate.Addendum{
+ Layer: mockLayer{},
+ History: v1.History{
+ Author: "dave",
+ },
+ URLs: []string{
+ "example.com",
+ },
+ Annotations: map[string]string{
+ "foo": "bar",
+ },
+ MediaType: types.MediaType("foo"),
+ }
+
+ result, err := mutate.Append(source, addendum)
+ if err != nil {
+ t.Fatalf("failed to append: %v", err)
+ }
+
+ layers := getLayers(t, result)
+
+ if diff := cmp.Diff(layers[1], mockLayer{}); diff != "" {
+ t.Fatalf("correct layer was not appended (-got, +want) %v", diff)
+ }
+
+ if configSizesAreEqual(t, source, result) {
+ t.Fatal("adding a layer MUST change the config file size")
+ }
+
+ cf := getConfigFile(t, result)
+
+ if diff := cmp.Diff(cf.History[1], addendum.History); diff != "" {
+ t.Fatalf("the appended history is not the same (-got, +want) %s", diff)
+ }
+
+ m, err := result.Manifest()
+ if err != nil {
+ t.Fatalf("failed to get manifest: %v", err)
+ }
+
+ if diff := cmp.Diff(m.Layers[1].URLs, addendum.URLs); diff != "" {
+ t.Fatalf("the appended URLs is not the same (-got, +want) %s", diff)
+ }
+
+ if diff := cmp.Diff(m.Layers[1].Annotations, addendum.Annotations); diff != "" {
+ t.Fatalf("the appended Annotations is not the same (-got, +want) %s", diff)
+ }
+ if diff := cmp.Diff(m.Layers[1].MediaType, addendum.MediaType); diff != "" {
+ t.Fatalf("the appended MediaType is not the same (-got, +want) %s", diff)
+ }
+}
+
+func TestAppendLayers(t *testing.T) {
+ source := sourceImage(t)
+ layer, err := random.Layer(100, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ result, err := mutate.AppendLayers(source, layer)
+ if err != nil {
+ t.Fatalf("failed to append a layer: %v", err)
+ }
+
+ if manifestsAreEqual(t, source, result) {
+ t.Fatal("appending a layer did not mutate the manifest")
+ }
+
+ if configFilesAreEqual(t, source, result) {
+ t.Fatal("appending a layer did not mutate the config file")
+ }
+
+ if configSizesAreEqual(t, source, result) {
+ t.Fatal("adding a layer MUST change the config file size")
+ }
+
+ layers := getLayers(t, result)
+
+ if got, want := len(layers), 2; got != want {
+ t.Fatalf("Layers did not return the appended layer "+
+ "- got size %d; expected 2", len(layers))
+ }
+
+ if layers[1] != layer {
+ t.Errorf("correct layer was not appended: got %v; want %v", layers[1], layer)
+ }
+
+ if err := validate.Image(result); err != nil {
+ t.Errorf("validate.Image() = %v", err)
+ }
+}
+
+func TestMutateConfig(t *testing.T) {
+ source := sourceImage(t)
+ cfg, err := source.ConfigFile()
+ if err != nil {
+ t.Fatalf("error getting source config file")
+ }
+
+ newEnv := []string{"foo=bar"}
+ cfg.Config.Env = newEnv
+ result, err := mutate.Config(source, cfg.Config)
+ if err != nil {
+ t.Fatalf("failed to mutate a config: %v", err)
+ }
+
+ if manifestsAreEqual(t, source, result) {
+ t.Error("mutating the config MUST mutate the manifest")
+ }
+
+ if configFilesAreEqual(t, source, result) {
+ t.Error("mutating the config did not mutate the config file")
+ }
+
+ if configSizesAreEqual(t, source, result) {
+ t.Error("adding an environment variable MUST change the config file size")
+ }
+
+ if configDigestsAreEqual(t, source, result) {
+ t.Errorf("mutating the config MUST mutate the config digest")
+ }
+
+ if !reflect.DeepEqual(cfg.Config.Env, newEnv) {
+ t.Errorf("incorrect environment set %v!=%v", cfg.Config.Env, newEnv)
+ }
+
+ if err := validate.Image(result); err != nil {
+ t.Errorf("validate.Image() = %v", err)
+ }
+}
+
+type arbitrary struct {
+}
+
+func (arbitrary) RawManifest() ([]byte, error) {
+ return []byte(`{"hello":"world"}`), nil
+}
+func TestAnnotations(t *testing.T) {
+ anns := map[string]string{
+ "foo": "bar",
+ }
+
+ for _, c := range []struct {
+ desc string
+ in partial.WithRawManifest
+ want string
+ }{{
+ desc: "image",
+ in: empty.Image,
+ want: `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":115,"digest":"sha256:5b943e2b943f6c81dbbd4e2eca5121f4fcc39139e3d1219d6d89bd925b77d9fe"},"layers":[],"annotations":{"foo":"bar"}}`,
+ }, {
+ desc: "index",
+ in: empty.Index,
+ want: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":null,"annotations":{"foo":"bar"}}`,
+ }, {
+ desc: "arbitrary",
+ in: arbitrary{},
+ want: `{"annotations":{"foo":"bar"},"hello":"world"}`,
+ }} {
+ t.Run(c.desc, func(t *testing.T) {
+ got, err := mutate.Annotations(c.in, anns).RawManifest()
+ if err != nil {
+ t.Fatalf("Annotations: %v", err)
+ }
+ if d := cmp.Diff(c.want, string(got)); d != "" {
+ t.Errorf("Diff(-want,+got): %s", d)
+ }
+ })
+ }
+}
+
+func TestMutateCreatedAt(t *testing.T) {
+ source := sourceImage(t)
+ want := time.Now().Add(-2 * time.Minute)
+ result, err := mutate.CreatedAt(source, v1.Time{Time: want})
+ if err != nil {
+ t.Fatalf("CreatedAt: %v", err)
+ }
+
+ if configDigestsAreEqual(t, source, result) {
+ t.Errorf("mutating the created time MUST mutate the config digest")
+ }
+
+ got := getConfigFile(t, result).Created.Time
+ if got != want {
+ t.Errorf("mutating the created time MUST mutate the time from %v to %v", got, want)
+ }
+}
+
+func TestMutateTime(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ source v1.Image
+ }{
+ {
+ name: "image with matching history and layers",
+ source: sourceImage(t),
+ },
+ {
+ name: "image with empty_layer history entries",
+ source: sourceImagePath(t, "testdata/source_image_with_empty_layer_history.tar"),
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ want := time.Time{}
+ result, err := mutate.Time(tc.source, want)
+ if err != nil {
+ t.Fatalf("failed to mutate a config: %v", err)
+ }
+
+ if configDigestsAreEqual(t, tc.source, result) {
+ t.Fatal("mutating the created time MUST mutate the config digest")
+ }
+
+ mutatedOriginalConfig := getConfigFile(t, tc.source).DeepCopy()
+ gotConfig := getConfigFile(t, result)
+
+ // manually change the fields we expect to be changed by mutate.Time
+ mutatedOriginalConfig.Author = ""
+ mutatedOriginalConfig.Created = v1.Time{Time: want}
+ for i := range mutatedOriginalConfig.History {
+ mutatedOriginalConfig.History[i].Created = v1.Time{Time: want}
+ mutatedOriginalConfig.History[i].Author = ""
+ }
+
+ if diff := cmp.Diff(mutatedOriginalConfig, gotConfig,
+ cmpopts.IgnoreFields(v1.RootFS{}, "DiffIDs"),
+ ); diff != "" {
+ t.Errorf("configFile() mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
+func TestMutateMediaType(t *testing.T) {
+ want := types.OCIManifestSchema1
+ wantCfg := types.OCIConfigJSON
+ img := mutate.MediaType(empty.Image, want)
+ img = mutate.ConfigMediaType(img, wantCfg)
+ got, err := img.MediaType()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want != got {
+ t.Errorf("%q != %q", want, got)
+ }
+ manifest, err := img.Manifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if manifest.MediaType == "" {
+ t.Error("MediaType should be set for OCI media types")
+ }
+ if gotCfg := manifest.Config.MediaType; gotCfg != wantCfg {
+ t.Errorf("manifest.Config.MediaType = %v, wanted %v", gotCfg, wantCfg)
+ }
+
+ want = types.DockerManifestSchema2
+ wantCfg = types.DockerConfigJSON
+ img = mutate.MediaType(img, want)
+ img = mutate.ConfigMediaType(img, wantCfg)
+ got, err = img.MediaType()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want != got {
+ t.Errorf("%q != %q", want, got)
+ }
+ manifest, err = img.Manifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if manifest.MediaType != want {
+ t.Errorf("MediaType should be set for Docker media types: %v", manifest.MediaType)
+ }
+ if gotCfg := manifest.Config.MediaType; gotCfg != wantCfg {
+ t.Errorf("manifest.Config.MediaType = %v, wanted %v", gotCfg, wantCfg)
+ }
+
+ want = types.OCIImageIndex
+ idx := mutate.IndexMediaType(empty.Index, want)
+ got, err = idx.MediaType()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want != got {
+ t.Errorf("%q != %q", want, got)
+ }
+ im, err := idx.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if im.MediaType == "" {
+ t.Error("MediaType should be set for OCI media types")
+ }
+
+ want = types.DockerManifestList
+ idx = mutate.IndexMediaType(idx, want)
+ got, err = idx.MediaType()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want != got {
+ t.Errorf("%q != %q", want, got)
+ }
+ im, err = idx.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if im.MediaType != want {
+ t.Errorf("MediaType should be set for Docker media types: %v", im.MediaType)
+ }
+}
+
+func TestAppendStreamableLayer(t *testing.T) {
+ img, err := mutate.AppendLayers(
+ sourceImage(t),
+ stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("a", 100)))),
+ stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("b", 100)))),
+ stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("c", 100)))),
+ )
+ if err != nil {
+ t.Fatalf("AppendLayers: %v", err)
+ }
+
+ // Until the streams are consumed, the image manifest is not yet computed.
+ if _, err := img.Manifest(); !errors.Is(err, stream.ErrNotComputed) {
+ t.Errorf("Manifest: got %v, want %v", err, stream.ErrNotComputed)
+ }
+
+ // We can still get Layers while some are not yet computed.
+ ls, err := img.Layers()
+ if err != nil {
+ t.Errorf("Layers: %v", err)
+ }
+ wantDigests := []string{
+ "sha256:bfa1c600931132f55789459e2f5a5eb85659ac91bc5a54ce09e3ed14809f8a7f",
+ "sha256:77a52b9a141dcc4d3d277d053193765dca725626f50eaf56b903ac2439cf7fd1",
+ "sha256:b78472d63f6e3d31059819173b56fcb0d9479a2b13c097d4addd84889f6aff06",
+ }
+ for i, l := range ls[1:] {
+ rc, err := l.Compressed()
+ if err != nil {
+ t.Errorf("Layer %d Compressed: %v", i, err)
+ }
+
+ // Consume the layer's stream and close it to compute the
+ // layer's metadata.
+ if _, err := io.Copy(io.Discard, rc); err != nil {
+ t.Errorf("Reading layer %d: %v", i, err)
+ }
+ if err := rc.Close(); err != nil {
+ t.Errorf("Closing layer %d: %v", i, err)
+ }
+
+ // The layer's metadata is now available.
+ h, err := l.Digest()
+ if err != nil {
+ t.Errorf("Digest after consuming layer %d: %v", i, err)
+ }
+ if h.String() != wantDigests[i] {
+ t.Errorf("Layer %d digest got %q, want %q", i, h, wantDigests[i])
+ }
+ }
+
+ // Now that the streamable layers have been consumed, the image's
+ // manifest can be computed.
+ if _, err := img.Manifest(); err != nil {
+ t.Errorf("Manifest: %v", err)
+ }
+
+ h, err := img.Digest()
+ if err != nil {
+ t.Errorf("Digest: %v", err)
+ }
+ wantDigest := "sha256:14d140947afedc6901b490265a08bc8ebe7f9d9faed6fdf19a451f054a7dd746"
+ if h.String() != wantDigest {
+ t.Errorf("Image digest got %q, want %q", h, wantDigest)
+ }
+}
+
+func TestCanonical(t *testing.T) {
+ source := sourceImage(t)
+ img, err := mutate.Canonical(source)
+ if err != nil {
+ t.Fatal(err)
+ }
+ sourceCf, err := source.ConfigFile()
+ if err != nil {
+ t.Fatal(err)
+ }
+ cf, err := img.ConfigFile()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, h := range cf.History {
+ want := "bazel build ..."
+ got := h.CreatedBy
+ if want != got {
+ t.Errorf("%q != %q", want, got)
+ }
+ }
+ var want, got string
+ want = cf.Architecture
+ got = sourceCf.Architecture
+ if want != got {
+ t.Errorf("%q != %q", want, got)
+ }
+ want = cf.OS
+ got = sourceCf.OS
+ if want != got {
+ t.Errorf("%q != %q", want, got)
+ }
+ want = cf.OSVersion
+ got = sourceCf.OSVersion
+ if want != got {
+ t.Errorf("%q != %q", want, got)
+ }
+ for _, s := range []string{
+ cf.Container,
+ cf.Config.Hostname,
+ cf.DockerVersion,
+ } {
+ if s != "" {
+ t.Errorf("non-zeroed string: %v", s)
+ }
+ }
+
+ expectedLayerTime := time.Unix(0, 0)
+ layers := getLayers(t, img)
+ for _, layer := range layers {
+ assertMTime(t, layer, expectedLayerTime)
+ }
+}
+
+func TestRemoveManifests(t *testing.T) {
+ // Load up the registry.
+ count := 3
+ for i := 0; i < count; i++ {
+ ii, err := random.Index(1024, int64(count), int64(count))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // test removing the first layer, second layer or the third layer
+ manifest, err := ii.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(manifest.Manifests) != count {
+ t.Fatalf("mismatched manifests on setup, had %d, expected %d", len(manifest.Manifests), count)
+ }
+ digest := manifest.Manifests[i].Digest
+ ii = mutate.RemoveManifests(ii, match.Digests(digest))
+ manifest, err = ii.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(manifest.Manifests) != (count - 1) {
+ t.Fatalf("mismatched manifests after removal, had %d, expected %d", len(manifest.Manifests), count-1)
+ }
+ for j, m := range manifest.Manifests {
+ if m.Digest == digest {
+ t.Fatalf("unexpectedly found removed hash %v at position %d", digest, j)
+ }
+ }
+ }
+}
+
+func TestImageImmutability(t *testing.T) {
+ img := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
+
+ t.Run("manifest", func(t *testing.T) {
+ // Check that Manifest is immutable.
+ changed, err := img.Manifest()
+ if err != nil {
+ t.Errorf("Manifest() = %v", err)
+ }
+ want := changed.DeepCopy() // Create a copy of original before mutating it.
+ changed.MediaType = types.DockerManifestList
+
+ if got, err := img.Manifest(); err != nil {
+ t.Errorf("Manifest() = %v", err)
+ } else if !cmp.Equal(got, want) {
+ t.Errorf("manifest changed! %s", cmp.Diff(got, want))
+ }
+ })
+
+ t.Run("config file", func(t *testing.T) {
+ // Check that ConfigFile is immutable.
+ changed, err := img.ConfigFile()
+ if err != nil {
+ t.Errorf("ConfigFile() = %v", err)
+ }
+ want := changed.DeepCopy() // Create a copy of original before mutating it.
+ changed.Author = "Jay Pegg"
+
+ if got, err := img.ConfigFile(); err != nil {
+ t.Errorf("ConfigFile() = %v", err)
+ } else if !cmp.Equal(got, want) {
+ t.Errorf("ConfigFile changed! %s", cmp.Diff(got, want))
+ }
+ })
+}
+
+func assertMTime(t *testing.T, layer v1.Layer, expectedTime time.Time) {
+ l, err := layer.Uncompressed()
+
+ if err != nil {
+ t.Fatalf("reading layer failed: %v", err)
+ }
+
+ tr := tar.NewReader(l)
+ for {
+ header, err := tr.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ t.Fatalf("Error reading layer: %v", err)
+ }
+
+ mtime := header.ModTime
+ if mtime.Equal(expectedTime) == false {
+ t.Errorf("unexpected mod time for layer. expected %v, got %v.", expectedTime, mtime)
+ }
+ }
+}
+
+func sourceImage(t *testing.T) v1.Image {
+ return sourceImagePath(t, "testdata/source_image.tar")
+}
+
+func sourceImagePath(t *testing.T, tarPath string) v1.Image {
+ t.Helper()
+
+ image, err := tarball.ImageFromPath(tarPath, nil)
+ if err != nil {
+ t.Fatalf("Error loading image: %v", err)
+ }
+ return image
+}
+
+func getManifest(t *testing.T, i v1.Image) *v1.Manifest {
+ t.Helper()
+
+ m, err := i.Manifest()
+ if err != nil {
+ t.Fatalf("Error fetching image manifest: %v", err)
+ }
+
+ return m
+}
+
+func getLayers(t *testing.T, i v1.Image) []v1.Layer {
+ t.Helper()
+
+ l, err := i.Layers()
+ if err != nil {
+ t.Fatalf("Error fetching image layers: %v", err)
+ }
+
+ return l
+}
+
+func getConfigFile(t *testing.T, i v1.Image) *v1.ConfigFile {
+ t.Helper()
+
+ c, err := i.ConfigFile()
+ if err != nil {
+ t.Fatalf("Error fetching image config file: %v", err)
+ }
+
+ return c
+}
+
+func configFilesAreEqual(t *testing.T, first, second v1.Image) bool {
+ t.Helper()
+
+ fc := getConfigFile(t, first)
+ sc := getConfigFile(t, second)
+
+ return cmp.Equal(fc, sc)
+}
+
+func configDigestsAreEqual(t *testing.T, first, second v1.Image) bool {
+ t.Helper()
+
+ fm := getManifest(t, first)
+ sm := getManifest(t, second)
+
+ return fm.Config.Digest == sm.Config.Digest
+}
+
+func configSizesAreEqual(t *testing.T, first, second v1.Image) bool {
+ t.Helper()
+
+ fm := getManifest(t, first)
+ sm := getManifest(t, second)
+
+ return fm.Config.Size == sm.Config.Size
+}
+
+func manifestsAreEqual(t *testing.T, first, second v1.Image) bool {
+ t.Helper()
+
+ fm := getManifest(t, first)
+ sm := getManifest(t, second)
+
+ return cmp.Equal(fm, sm)
+}
+
+type mockLayer struct{}
+
+func (m mockLayer) Digest() (v1.Hash, error) {
+ return v1.Hash{Algorithm: "fake", Hex: "digest"}, nil
+}
+
+func (m mockLayer) DiffID() (v1.Hash, error) {
+ return v1.Hash{Algorithm: "fake", Hex: "diff id"}, nil
+}
+
+func (m mockLayer) MediaType() (types.MediaType, error) {
+ return "some-media-type", nil
+}
+
+func (m mockLayer) Size() (int64, error) { return 137438691328, nil }
+func (m mockLayer) Compressed() (io.ReadCloser, error) {
+ return io.NopCloser(strings.NewReader("compressed times")), nil
+}
+func (m mockLayer) Uncompressed() (io.ReadCloser, error) {
+ return io.NopCloser(strings.NewReader("uncompressed")), nil
+}
diff --git a/pkg/v1/mutate/rebase.go b/pkg/v1/mutate/rebase.go
new file mode 100644
index 0000000..c606e0b
--- /dev/null
+++ b/pkg/v1/mutate/rebase.go
@@ -0,0 +1,144 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mutate
+
+import (
+ "fmt"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+)
+
+// Rebase returns a new v1.Image where the oldBase in orig is replaced by newBase.
+func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
+ // Verify that oldBase's layers are present in orig, otherwise orig is
+ // not based on oldBase at all.
+ origLayers, err := orig.Layers()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get layers for original: %w", err)
+ }
+ oldBaseLayers, err := oldBase.Layers()
+ if err != nil {
+ return nil, err
+ }
+ if len(oldBaseLayers) > len(origLayers) {
+ return nil, fmt.Errorf("image %q is not based on %q (too few layers)", orig, oldBase)
+ }
+ for i, l := range oldBaseLayers {
+ oldLayerDigest, err := l.Digest()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, oldBase, err)
+ }
+ origLayerDigest, err := origLayers[i].Digest()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, orig, err)
+ }
+ if oldLayerDigest != origLayerDigest {
+ return nil, fmt.Errorf("image %q is not based on %q (layer %d mismatch)", orig, oldBase, i)
+ }
+ }
+
+ oldConfig, err := oldBase.ConfigFile()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get config for old base: %w", err)
+ }
+
+ origConfig, err := orig.ConfigFile()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get config for original: %w", err)
+ }
+
+ newConfig, err := newBase.ConfigFile()
+ if err != nil {
+ return nil, fmt.Errorf("could not get config for new base: %w", err)
+ }
+
+ // Stitch together an image that contains:
+ // - original image's config
+ // - new base image's os/arch properties
+ // - new base image's layers + top of original image's layers
+ // - new base image's history + top of original image's history
+ rebasedImage, err := Config(empty.Image, *origConfig.Config.DeepCopy())
+ if err != nil {
+ return nil, fmt.Errorf("failed to create empty image with original config: %w", err)
+ }
+
+ // Add new config properties from existing images.
+ rebasedConfig, err := rebasedImage.ConfigFile()
+ if err != nil {
+ return nil, fmt.Errorf("could not get config for rebased image: %w", err)
+ }
+ // OS/Arch properties from new base
+ rebasedConfig.Architecture = newConfig.Architecture
+ rebasedConfig.OS = newConfig.OS
+ rebasedConfig.OSVersion = newConfig.OSVersion
+
+ // Apply config properties to rebased.
+ rebasedImage, err = ConfigFile(rebasedImage, rebasedConfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed to replace config for rebased image: %w", err)
+ }
+
+ // Get new base layers and config for history.
+ newBaseLayers, err := newBase.Layers()
+ if err != nil {
+ return nil, fmt.Errorf("could not get new base layers for new base: %w", err)
+ }
+ // Add new base layers.
+ rebasedImage, err = Append(rebasedImage, createAddendums(0, 0, newConfig.History, newBaseLayers)...)
+ if err != nil {
+ return nil, fmt.Errorf("failed to append new base image: %w", err)
+ }
+
+ // Add original layers above the old base.
+ rebasedImage, err = Append(rebasedImage, createAddendums(len(oldConfig.History), len(oldBaseLayers)+1, origConfig.History, origLayers)...)
+ if err != nil {
+ return nil, fmt.Errorf("failed to append original image: %w", err)
+ }
+
+ return rebasedImage, nil
+}
+
+// createAddendums makes a list of addendums from a history and layers starting from a specific history and layer
+// indexes.
+func createAddendums(startHistory, startLayer int, history []v1.History, layers []v1.Layer) []Addendum {
+ var adds []Addendum
+ // History should be a superset of layers; empty layers (e.g. ENV statements) only exist in history.
+ // They cannot be iterated identically but must be walked independently, only advancing the iterator for layers
+ // when a history entry for a non-empty layer is seen.
+ layerIndex := 0
+ for historyIndex := range history {
+ var layer v1.Layer
+ emptyLayer := history[historyIndex].EmptyLayer
+ if !emptyLayer {
+ layer = layers[layerIndex]
+ layerIndex++
+ }
+ if historyIndex >= startHistory || layerIndex >= startLayer {
+ adds = append(adds, Addendum{
+ Layer: layer,
+ History: history[historyIndex],
+ })
+ }
+ }
+ // In the event history was malformed or non-existent, append the remaining layers.
+ for i := layerIndex; i < len(layers); i++ {
+ if i >= startLayer {
+ adds = append(adds, Addendum{Layer: layers[layerIndex]})
+ }
+ }
+
+ return adds
+}
diff --git a/pkg/v1/mutate/rebase_test.go b/pkg/v1/mutate/rebase_test.go
new file mode 100644
index 0000000..250b6bf
--- /dev/null
+++ b/pkg/v1/mutate/rebase_test.go
@@ -0,0 +1,179 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mutate_test
+
+import (
+ "testing"
+ "time"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+)
+
+func layerDigests(t *testing.T, img v1.Image) []string {
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatalf("oldBase.Layers: %v", err)
+ }
+ layerDigests := make([]string, len(layers))
+ for i, l := range layers {
+ dig, err := l.Digest()
+ if err != nil {
+ t.Fatalf("layer.Digest %d: %v", i, err)
+ }
+ t.Log(dig)
+ layerDigests[i] = dig.String()
+ }
+ return layerDigests
+}
+
+// TestRebase tests that layer digests are expected when performing a rebase on
+// random.Image layers.
+func TestRebase(t *testing.T) {
+ // Create a random old base image of 5 layers and get those layers' digests.
+ const oldBaseLayerCount = 5
+ oldBase, err := random.Image(100, oldBaseLayerCount)
+ if err != nil {
+ t.Fatalf("random.Image (oldBase): %v", err)
+ }
+ t.Log("Old base:")
+ _ = layerDigests(t, oldBase)
+
+ // Construct an image with 2 layers on top of oldBase (an empty layer and a random layer).
+ top, err := random.Image(100, 1)
+ if err != nil {
+ t.Fatalf("random.Image (top): %v", err)
+ }
+ topLayers, err := top.Layers()
+ if err != nil {
+ t.Fatalf("top.Layers: %v", err)
+ }
+ orig, err := mutate.Append(oldBase,
+ mutate.Addendum{
+ Layer: nil,
+ History: v1.History{
+ Author: "me",
+ Created: v1.Time{Time: time.Now()},
+ CreatedBy: "test-empty",
+ Comment: "this is an empty test",
+ EmptyLayer: true,
+ },
+ },
+ mutate.Addendum{
+ Layer: topLayers[0],
+ History: v1.History{
+ Author: "me",
+ Created: v1.Time{Time: time.Now()},
+ CreatedBy: "test",
+ Comment: "this is a test",
+ },
+ },
+ )
+ if err != nil {
+ t.Fatalf("Append: %v", err)
+ }
+
+ t.Log("Original:")
+ origLayerDigests := layerDigests(t, orig)
+
+ // Create a random new base image of 3 layers.
+ newBase, err := random.Image(100, 3)
+ if err != nil {
+ t.Fatalf("random.Image (newBase): %v", err)
+ }
+ t.Log("New base:")
+ newBaseLayerDigests := layerDigests(t, newBase)
+
+ // Add config file os/arch property fields
+ newBaseConfigFile, err := newBase.ConfigFile()
+ if err != nil {
+ t.Fatalf("newBase.ConfigFile: %v", err)
+ }
+ newBaseConfigFile.Architecture = "arm"
+ newBaseConfigFile.OS = "windows"
+ newBaseConfigFile.OSVersion = "10.0.17763.1339"
+
+ newBase, err = mutate.ConfigFile(newBase, newBaseConfigFile)
+ if err != nil {
+ t.Fatalf("ConfigFile (newBase): %v", err)
+ }
+
+ // Rebase original image onto new base.
+ rebased, err := mutate.Rebase(orig, oldBase, newBase)
+ if err != nil {
+ t.Fatalf("Rebase: %v", err)
+ }
+
+ rebasedBaseLayers, err := rebased.Layers()
+ if err != nil {
+ t.Fatalf("rebased.Layers: %v", err)
+ }
+ rebasedLayerDigests := make([]string, len(rebasedBaseLayers))
+ t.Log("Rebased image layer digests:")
+ for i, l := range rebasedBaseLayers {
+ dig, err := l.Digest()
+ if err != nil {
+ t.Fatalf("layer.Digest (rebased base layer %d): %v", i, err)
+ }
+ t.Log(dig)
+ rebasedLayerDigests[i] = dig.String()
+ }
+
+ // Compare rebased layers.
+ wantLayerDigests := append(newBaseLayerDigests, origLayerDigests[len(origLayerDigests)-1])
+ if len(rebasedLayerDigests) != len(wantLayerDigests) {
+ t.Fatalf("Rebased image contained %d layers, want %d", len(rebasedLayerDigests), len(wantLayerDigests))
+ }
+ for i, rl := range rebasedLayerDigests {
+ if got, want := rl, wantLayerDigests[i]; got != want {
+ t.Errorf("Layer %d mismatch, got %q, want %q", i, got, want)
+ }
+ }
+
+ // Compare rebased history.
+ origConfig, err := orig.ConfigFile()
+ if err != nil {
+ t.Fatalf("orig.ConfigFile: %v", err)
+ }
+ newBaseConfig, err := newBase.ConfigFile()
+ if err != nil {
+ t.Fatalf("newBase.ConfigFile: %v", err)
+ }
+ rebasedConfig, err := rebased.ConfigFile()
+ if err != nil {
+ t.Fatalf("rebased.ConfigFile: %v", err)
+ }
+ wantHistories := append(newBaseConfig.History, origConfig.History[oldBaseLayerCount:]...)
+ if len(wantHistories) != len(rebasedConfig.History) {
+ t.Fatalf("Rebased image contained %d history, want %d", len(rebasedConfig.History), len(wantHistories))
+ }
+ for i, rh := range rebasedConfig.History {
+ if got, want := rh.Comment, wantHistories[i].Comment; got != want {
+ t.Errorf("Layer %d mismatch, got %q, want %q", i, got, want)
+ }
+ }
+
+ // Compare ConfigFile property fields copied from new base.
+ if rebasedConfig.Architecture != newBaseConfig.Architecture {
+ t.Errorf("ConfigFile property Architecture mismatch, got %q, want %q", rebasedConfig.Architecture, newBaseConfig.Architecture)
+ }
+ if rebasedConfig.OS != newBaseConfig.OS {
+ t.Errorf("ConfigFile property OS mismatch, got %q, want %q", rebasedConfig.OS, newBaseConfig.OS)
+ }
+ if rebasedConfig.OSVersion != newBaseConfig.OSVersion {
+ t.Errorf("ConfigFile property OSVersion mismatch, got %q, want %q", rebasedConfig.OSVersion, newBaseConfig.OSVersion)
+ }
+}
diff --git a/pkg/v1/mutate/testdata/README.md b/pkg/v1/mutate/testdata/README.md
new file mode 100644
index 0000000..a35d433
--- /dev/null
+++ b/pkg/v1/mutate/testdata/README.md
@@ -0,0 +1,10 @@
+# whiteout\_image.tar
+
+Including whiteout files in our source caused [issues](https://github.com/google/go-containerregistry/issues/305)
+when cloning this repo inside a docker build. Removing the whiteout file from
+this test data doesn't break anything (since we checked in the tar), but if you
+want to rebuild it for some reason:
+
+```
+touch whiteout/.wh.foo.txt
+```
diff --git a/pkg/v1/mutate/testdata/bar b/pkg/v1/mutate/testdata/bar
new file mode 100644
index 0000000..5716ca5
--- /dev/null
+++ b/pkg/v1/mutate/testdata/bar
@@ -0,0 +1 @@
+bar
diff --git a/pkg/v1/mutate/testdata/foo b/pkg/v1/mutate/testdata/foo
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/pkg/v1/mutate/testdata/foo
@@ -0,0 +1 @@
+foo
diff --git a/pkg/v1/mutate/testdata/overwritten_file.tar b/pkg/v1/mutate/testdata/overwritten_file.tar
new file mode 100755
index 0000000..7159556
--- /dev/null
+++ b/pkg/v1/mutate/testdata/overwritten_file.tar
Binary files differ
diff --git a/pkg/v1/mutate/testdata/source_image.tar b/pkg/v1/mutate/testdata/source_image.tar
new file mode 100755
index 0000000..7824a7b
--- /dev/null
+++ b/pkg/v1/mutate/testdata/source_image.tar
Binary files differ
diff --git a/pkg/v1/mutate/testdata/source_image_with_empty_layer_history.tar b/pkg/v1/mutate/testdata/source_image_with_empty_layer_history.tar
new file mode 100755
index 0000000..541cb37
--- /dev/null
+++ b/pkg/v1/mutate/testdata/source_image_with_empty_layer_history.tar
Binary files differ
diff --git a/pkg/v1/mutate/testdata/whiteout/bar.txt b/pkg/v1/mutate/testdata/whiteout/bar.txt
new file mode 100644
index 0000000..5716ca5
--- /dev/null
+++ b/pkg/v1/mutate/testdata/whiteout/bar.txt
@@ -0,0 +1 @@
+bar
diff --git a/pkg/v1/mutate/testdata/whiteout/foo.txt b/pkg/v1/mutate/testdata/whiteout/foo.txt
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/pkg/v1/mutate/testdata/whiteout/foo.txt
@@ -0,0 +1 @@
+foo
diff --git a/pkg/v1/mutate/testdata/whiteout_image.tar b/pkg/v1/mutate/testdata/whiteout_image.tar
new file mode 100755
index 0000000..748621e
--- /dev/null
+++ b/pkg/v1/mutate/testdata/whiteout_image.tar
Binary files differ
diff --git a/pkg/v1/mutate/whiteout_test.go b/pkg/v1/mutate/whiteout_test.go
new file mode 100644
index 0000000..d3e7a86
--- /dev/null
+++ b/pkg/v1/mutate/whiteout_test.go
@@ -0,0 +1,43 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mutate
+
+import (
+ "testing"
+)
+
+func TestWhiteoutDir(t *testing.T) {
+ fsMap := map[string]bool{
+ "baz": true,
+ "red/blue": true,
+ }
+ var tests = []struct {
+ path string
+ whiteout bool
+ }{
+ {"usr/bin", false},
+ {"baz/foo.txt", true},
+ {"baz/bar/foo.txt", true},
+ {"red/green", false},
+ {"red/yellow.txt", false},
+ }
+
+ for _, tt := range tests {
+ whiteout := inWhiteoutDir(fsMap, tt.path)
+ if whiteout != tt.whiteout {
+ t.Errorf("Whiteout %s: expected %v, but got %v", tt.path, tt.whiteout, whiteout)
+ }
+ }
+}
diff --git a/pkg/v1/partial/README.md b/pkg/v1/partial/README.md
new file mode 100644
index 0000000..53ebbc6
--- /dev/null
+++ b/pkg/v1/partial/README.md
@@ -0,0 +1,82 @@
+# `partial`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial)
+
+## Partial Implementations
+
+There are roughly two kinds of image representations: compressed and uncompressed.
+
+The implementations for these kinds of images are almost identical, with the only
+major difference being how blobs (config and layers) are fetched. This common
+code lives in this package, where you provide a _partial_ implementation of a
+compressed or uncompressed image, and you get back a full `v1.Image` implementation.
+
+### Examples
+
+In a registry, blobs are compressed, so it's easiest to implement a `v1.Image` in terms
+of compressed layers. `remote.remoteImage` does this by implementing `CompressedImageCore`:
+
+```go
+type CompressedImageCore interface {
+ RawConfigFile() ([]byte, error)
+ MediaType() (types.MediaType, error)
+ RawManifest() ([]byte, error)
+ LayerByDigest(v1.Hash) (CompressedLayer, error)
+}
+```
+
+In a tarball, blobs are (often) uncompressed, so it's easiest to implement a `v1.Image` in terms
+of uncompressed layers. `tarball.uncompressedImage` does this by implementing `UncompressedImageCore`:
+
+```go
+type UncompressedImageCore interface {
+ RawConfigFile() ([]byte, error)
+ MediaType() (types.MediaType, error)
+ LayerByDiffID(v1.Hash) (UncompressedLayer, error)
+}
+```
+
+## Optional Methods
+
+Where possible, we access some information via optional methods as an optimization.
+
+### [`partial.Descriptor`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#Descriptor)
+
+There are some properties of a [`Descriptor`](https://github.com/opencontainers/image-spec/blob/master/descriptor.md#properties) that aren't derivable from just image data:
+
+* `MediaType`
+* `Platform`
+* `URLs`
+* `Annotations`
+
+For example, in a `tarball.Image`, there is a `LayerSources` field that contains
+an entire layer descriptor with `URLs` information for foreign layers. This
+information can be passed through to callers by implementing this optional
+`Descriptor` method.
+
+See [`#654`](https://github.com/google/go-containerregistry/pull/654).
+
+### [`partial.UncompressedSize`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#UncompressedSize)
+
+Usually, you don't need to know the uncompressed size of a layer, since that
+information isn't stored in a config file (just he sha256 is needed); however,
+there are cases where it is very helpful to know the layer size, e.g. when
+writing the uncompressed layer into a tarball.
+
+See [`#655`](https://github.com/google/go-containerregistry/pull/655).
+
+### [`partial.Exists`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#Exists)
+
+We generally don't care about the existence of something as granular as a
+layer, and would rather ensure all the invariants of an image are upheld via
+the `validate` package. However, there are situations where we want to do a
+quick smoke test to ensure that the underlying storage engine hasn't been
+corrupted by something e.g. deleting files or blobs. Thus, we've exposed an
+optional `Exists` method that does an existence check without actually reading
+any bytes.
+
+The `remote` package implements this via `HEAD` requests.
+
+The `layout` package implements this via `os.Stat`.
+
+See [`#838`](https://github.com/google/go-containerregistry/pull/838).
diff --git a/pkg/v1/partial/compressed.go b/pkg/v1/partial/compressed.go
new file mode 100644
index 0000000..44989ac
--- /dev/null
+++ b/pkg/v1/partial/compressed.go
@@ -0,0 +1,188 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial
+
+import (
+ "io"
+
+ "github.com/google/go-containerregistry/internal/and"
+ "github.com/google/go-containerregistry/internal/compression"
+ "github.com/google/go-containerregistry/internal/gzip"
+ "github.com/google/go-containerregistry/internal/zstd"
+ comp "github.com/google/go-containerregistry/pkg/compression"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// CompressedLayer represents the bare minimum interface a natively
+// compressed layer must implement for us to produce a v1.Layer
+type CompressedLayer interface {
+ // Digest returns the Hash of the compressed layer.
+ Digest() (v1.Hash, error)
+
+ // Compressed returns an io.ReadCloser for the compressed layer contents.
+ Compressed() (io.ReadCloser, error)
+
+ // Size returns the compressed size of the Layer.
+ Size() (int64, error)
+
+ // Returns the mediaType for the compressed Layer
+ MediaType() (types.MediaType, error)
+}
+
+// compressedLayerExtender implements v1.Image using the compressed base properties.
+type compressedLayerExtender struct {
+ CompressedLayer
+}
+
+// Uncompressed implements v1.Layer
+func (cle *compressedLayerExtender) Uncompressed() (io.ReadCloser, error) {
+ rc, err := cle.Compressed()
+ if err != nil {
+ return nil, err
+ }
+
+ // Often, the "compressed" bytes are not actually-compressed.
+ // Peek at the first two bytes to determine whether it's correct to
+ // wrap this with gzip.UnzipReadCloser or zstd.UnzipReadCloser.
+ cp, pr, err := compression.PeekCompression(rc)
+ if err != nil {
+ return nil, err
+ }
+
+ prc := &and.ReadCloser{
+ Reader: pr,
+ CloseFunc: rc.Close,
+ }
+
+ switch cp {
+ case comp.GZip:
+ return gzip.UnzipReadCloser(prc)
+ case comp.ZStd:
+ return zstd.UnzipReadCloser(prc)
+ default:
+ return prc, nil
+ }
+}
+
+// DiffID implements v1.Layer
+func (cle *compressedLayerExtender) DiffID() (v1.Hash, error) {
+ // If our nested CompressedLayer implements DiffID,
+ // then delegate to it instead.
+ if wdi, ok := cle.CompressedLayer.(WithDiffID); ok {
+ return wdi.DiffID()
+ }
+ r, err := cle.Uncompressed()
+ if err != nil {
+ return v1.Hash{}, err
+ }
+ defer r.Close()
+ h, _, err := v1.SHA256(r)
+ return h, err
+}
+
+// CompressedToLayer fills in the missing methods from a CompressedLayer so that it implements v1.Layer
+func CompressedToLayer(ul CompressedLayer) (v1.Layer, error) {
+ return &compressedLayerExtender{ul}, nil
+}
+
+// CompressedImageCore represents the base minimum interface a natively
+// compressed image must implement for us to produce a v1.Image.
+type CompressedImageCore interface {
+ ImageCore
+
+ // RawManifest returns the serialized bytes of the manifest.
+ RawManifest() ([]byte, error)
+
+ // LayerByDigest is a variation on the v1.Image method, which returns
+ // a CompressedLayer instead.
+ LayerByDigest(v1.Hash) (CompressedLayer, error)
+}
+
+// compressedImageExtender implements v1.Image by extending CompressedImageCore with the
+// appropriate methods computed from the minimal core.
+type compressedImageExtender struct {
+ CompressedImageCore
+}
+
+// Assert that our extender type completes the v1.Image interface
+var _ v1.Image = (*compressedImageExtender)(nil)
+
+// Digest implements v1.Image
+func (i *compressedImageExtender) Digest() (v1.Hash, error) {
+ return Digest(i)
+}
+
+// ConfigName implements v1.Image
+func (i *compressedImageExtender) ConfigName() (v1.Hash, error) {
+ return ConfigName(i)
+}
+
+// Layers implements v1.Image
+func (i *compressedImageExtender) Layers() ([]v1.Layer, error) {
+ hs, err := FSLayers(i)
+ if err != nil {
+ return nil, err
+ }
+ ls := make([]v1.Layer, 0, len(hs))
+ for _, h := range hs {
+ l, err := i.LayerByDigest(h)
+ if err != nil {
+ return nil, err
+ }
+ ls = append(ls, l)
+ }
+ return ls, nil
+}
+
+// LayerByDigest implements v1.Image
+func (i *compressedImageExtender) LayerByDigest(h v1.Hash) (v1.Layer, error) {
+ cl, err := i.CompressedImageCore.LayerByDigest(h)
+ if err != nil {
+ return nil, err
+ }
+ return CompressedToLayer(cl)
+}
+
+// LayerByDiffID implements v1.Image
+func (i *compressedImageExtender) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
+ h, err := DiffIDToBlob(i, h)
+ if err != nil {
+ return nil, err
+ }
+ return i.LayerByDigest(h)
+}
+
+// ConfigFile implements v1.Image
+func (i *compressedImageExtender) ConfigFile() (*v1.ConfigFile, error) {
+ return ConfigFile(i)
+}
+
+// Manifest implements v1.Image
+func (i *compressedImageExtender) Manifest() (*v1.Manifest, error) {
+ return Manifest(i)
+}
+
+// Size implements v1.Image
+func (i *compressedImageExtender) Size() (int64, error) {
+ return Size(i)
+}
+
+// CompressedToImage fills in the missing methods from a CompressedImageCore so that it implements v1.Image
+func CompressedToImage(cic CompressedImageCore) (v1.Image, error) {
+ return &compressedImageExtender{
+ CompressedImageCore: cic,
+ }, nil
+}
diff --git a/pkg/v1/partial/compressed_test.go b/pkg/v1/partial/compressed_test.go
new file mode 100644
index 0000000..bf6bff4
--- /dev/null
+++ b/pkg/v1/partial/compressed_test.go
@@ -0,0 +1,193 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial_test
+
+import (
+ "io"
+ "net/http/httptest"
+ "net/url"
+ "path"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/internal/compare"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+// Remote leverages a lot of compressed partials.
+func TestRemote(t *testing.T) {
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rnd, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ src := path.Join(u.Host, "test/compressed")
+ ref, err := name.ParseReference(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := remote.Write(ref, rnd); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err := remote.Image(ref)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Fatal(err)
+ }
+
+ cf, err := img.ConfigFile()
+ if err != nil {
+ t.Fatal(err)
+ }
+ m, err := img.Manifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ layer, err := img.LayerByDiffID(cf.RootFS.DiffIDs[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ d, err := layer.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := cmp.Diff(d, m.Layers[0].Digest); diff != "" {
+ t.Errorf("mismatched digest: %v", diff)
+ }
+
+ ok, err := partial.Exists(layer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, want := ok, true; got != want {
+ t.Errorf("Exists() = %t != %t", got, want)
+ }
+
+ cl, err := partial.ConfigLayer(img)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, ok := cl.(*remote.MountableLayer); !ok {
+ t.Errorf("ConfigLayer() expected to be MountableLayer, got %T", cl)
+ }
+}
+
+type noDiffID struct {
+ l v1.Layer
+}
+
+func (l *noDiffID) Digest() (v1.Hash, error) {
+ return l.l.Digest()
+}
+func (l *noDiffID) Compressed() (io.ReadCloser, error) {
+ return l.l.Compressed()
+}
+func (l *noDiffID) Size() (int64, error) {
+ return l.l.Size()
+}
+func (l *noDiffID) MediaType() (types.MediaType, error) {
+ return l.l.MediaType()
+}
+func (l *noDiffID) Descriptor() (*v1.Descriptor, error) {
+ return partial.Descriptor(l.l)
+}
+func (l *noDiffID) UncompressedSize() (int64, error) {
+ return partial.UncompressedSize(l.l)
+}
+
+func TestCompressedLayerExtender(t *testing.T) {
+ rnd, err := random.Layer(1000, types.OCILayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ l, err := partial.CompressedToLayer(&noDiffID{rnd})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := compare.Layers(rnd, l); err != nil {
+ t.Fatalf("compare.Layers: %v", err)
+ }
+ if _, err := partial.Descriptor(l); err != nil {
+ t.Fatalf("partial.Descriptor: %v", err)
+ }
+ if _, err := partial.UncompressedSize(l); err != nil {
+ t.Fatalf("partial.UncompressedSize: %v", err)
+ }
+}
+
+type compressedImage struct {
+ img v1.Image
+}
+
+func (i *compressedImage) RawConfigFile() ([]byte, error) {
+ return i.img.RawConfigFile()
+}
+
+func (i *compressedImage) MediaType() (types.MediaType, error) {
+ return i.img.MediaType()
+}
+
+func (i *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
+ return i.img.LayerByDigest(h)
+}
+
+func (i *compressedImage) RawManifest() ([]byte, error) {
+ return i.img.RawManifest()
+}
+
+func (i *compressedImage) Descriptor() (*v1.Descriptor, error) {
+ return partial.Descriptor(i.img)
+}
+
+func TestCompressed(t *testing.T) {
+ rnd, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ core := &compressedImage{rnd}
+
+ img, err := partial.CompressedToImage(core)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validate.Image: %v", err)
+ }
+ if _, err := partial.Descriptor(img); err != nil {
+ t.Fatalf("partial.Descriptor: %v", err)
+ }
+}
diff --git a/pkg/v1/partial/configlayer_test.go b/pkg/v1/partial/configlayer_test.go
new file mode 100644
index 0000000..decdcab
--- /dev/null
+++ b/pkg/v1/partial/configlayer_test.go
@@ -0,0 +1,139 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial
+
+import (
+ "fmt"
+ "io"
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type testUIC struct {
+ UncompressedImageCore
+ configFile []byte
+}
+
+func (t testUIC) RawConfigFile() ([]byte, error) {
+ return t.configFile, nil
+}
+
+type testCIC struct {
+ CompressedImageCore
+ configFile []byte
+}
+
+func (t testCIC) LayerByDigest(h v1.Hash) (CompressedLayer, error) {
+ return nil, fmt.Errorf("no layer by diff ID %v", h)
+}
+
+func (t testCIC) RawConfigFile() ([]byte, error) {
+ return t.configFile, nil
+}
+
+func TestConfigLayer(t *testing.T) {
+ cases := []v1.Image{
+ &compressedImageExtender{
+ CompressedImageCore: testCIC{
+ configFile: []byte("{}"),
+ },
+ },
+ &uncompressedImageExtender{
+ UncompressedImageCore: testUIC{
+ configFile: []byte("{}"),
+ },
+ },
+ }
+
+ for _, image := range cases {
+ hash, err := image.ConfigName()
+ if err != nil {
+ t.Fatalf("Error getting config name: %v", err)
+ }
+
+ if _, err := image.LayerByDigest(hash); err == nil {
+ t.Error("LayerByDigest(config hash) returned nil error, wanted error")
+ }
+
+ layer, err := ConfigLayer(image)
+ if err != nil {
+ t.Fatalf("ConfigLayer: %v", err)
+ }
+ lr, err := layer.Uncompressed()
+ if err != nil {
+ t.Fatalf("Error getting uncompressed layer: %v", err)
+ }
+ zr, err := layer.Compressed()
+ if err != nil {
+ t.Fatalf("Error getting compressed layer: %v", err)
+ }
+
+ cfgLayerBytes, err := io.ReadAll(lr)
+ if err != nil {
+ t.Fatalf("Error reading config layer bytes: %v", err)
+ }
+ zcfgLayerBytes, err := io.ReadAll(zr)
+ if err != nil {
+ t.Fatalf("Error reading config layer bytes: %v", err)
+ }
+
+ cfgFile, err := image.RawConfigFile()
+ if err != nil {
+ t.Fatalf("Error getting raw config file: %v", err)
+ }
+
+ if string(cfgFile) != string(cfgLayerBytes) {
+ t.Errorf("Config file layer doesn't match raw config file")
+ }
+ if string(cfgFile) != string(zcfgLayerBytes) {
+ t.Errorf("Config file layer doesn't match raw config file")
+ }
+
+ size, err := layer.Size()
+ if err != nil {
+ t.Fatalf("Error getting config layer size: %v", err)
+ }
+ if size != int64(len(cfgFile)) {
+ t.Errorf("Size() = %d, want %d", size, len(cfgFile))
+ }
+
+ digest, err := layer.Digest()
+ if err != nil {
+ t.Fatalf("Digest() = %v", err)
+ }
+ if digest != hash {
+ t.Errorf("ConfigLayer().Digest() != ConfigName(); %v, %v", digest, hash)
+ }
+
+ diffid, err := layer.DiffID()
+ if err != nil {
+ t.Fatalf("DiffId() = %v", err)
+ }
+ if diffid != hash {
+ t.Errorf("ConfigLayer().DiffID() != ConfigName(); %v, %v", diffid, hash)
+ }
+
+ mt, err := layer.MediaType()
+ if err != nil {
+ t.Fatalf("Error getting config layer media type: %v", err)
+ }
+
+ if mt != types.OCIConfigJSON {
+ t.Errorf("MediaType() = %v, want %v", mt, types.OCIConfigJSON)
+ }
+ }
+}
diff --git a/pkg/v1/partial/doc.go b/pkg/v1/partial/doc.go
new file mode 100644
index 0000000..153dfe4
--- /dev/null
+++ b/pkg/v1/partial/doc.go
@@ -0,0 +1,17 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package partial defines methods for building up a v1.Image from
+// minimal subsets that are sufficient for defining a v1.Image.
+package partial
diff --git a/pkg/v1/partial/image.go b/pkg/v1/partial/image.go
new file mode 100644
index 0000000..c65f45e
--- /dev/null
+++ b/pkg/v1/partial/image.go
@@ -0,0 +1,28 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial
+
+import (
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// ImageCore is the core set of properties without which we cannot build a v1.Image
+type ImageCore interface {
+ // RawConfigFile returns the serialized bytes of this image's config file.
+ RawConfigFile() ([]byte, error)
+
+ // MediaType of this image's manifest.
+ MediaType() (types.MediaType, error)
+}
diff --git a/pkg/v1/partial/index.go b/pkg/v1/partial/index.go
new file mode 100644
index 0000000..f17f274
--- /dev/null
+++ b/pkg/v1/partial/index.go
@@ -0,0 +1,85 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial
+
+import (
+ "fmt"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/match"
+)
+
+// FindManifests given a v1.ImageIndex, find the manifests that fit the matcher.
+func FindManifests(index v1.ImageIndex, matcher match.Matcher) ([]v1.Descriptor, error) {
+ // get the actual manifest list
+ indexManifest, err := index.IndexManifest()
+ if err != nil {
+ return nil, fmt.Errorf("unable to get raw index: %w", err)
+ }
+ manifests := []v1.Descriptor{}
+ // try to get the root of our image
+ for _, manifest := range indexManifest.Manifests {
+ if matcher(manifest) {
+ manifests = append(manifests, manifest)
+ }
+ }
+ return manifests, nil
+}
+
+// FindImages given a v1.ImageIndex, find the images that fit the matcher. If a Descriptor
+// matches the provider Matcher, but the referenced item is not an Image, ignores it.
+// Only returns those that match the Matcher and are images.
+func FindImages(index v1.ImageIndex, matcher match.Matcher) ([]v1.Image, error) {
+ matches := []v1.Image{}
+ manifests, err := FindManifests(index, matcher)
+ if err != nil {
+ return nil, err
+ }
+ for _, desc := range manifests {
+ // if it is not an image, ignore it
+ if !desc.MediaType.IsImage() {
+ continue
+ }
+ img, err := index.Image(desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+ matches = append(matches, img)
+ }
+ return matches, nil
+}
+
+// FindIndexes given a v1.ImageIndex, find the indexes that fit the matcher. If a Descriptor
+// matches the provider Matcher, but the referenced item is not an Index, ignores it.
+// Only returns those that match the Matcher and are indexes.
+func FindIndexes(index v1.ImageIndex, matcher match.Matcher) ([]v1.ImageIndex, error) {
+ matches := []v1.ImageIndex{}
+ manifests, err := FindManifests(index, matcher)
+ if err != nil {
+ return nil, err
+ }
+ for _, desc := range manifests {
+ if !desc.MediaType.IsIndex() {
+ continue
+ }
+ // if it is not an index, ignore it
+ idx, err := index.ImageIndex(desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+ matches = append(matches, idx)
+ }
+ return matches, nil
+}
diff --git a/pkg/v1/partial/index_test.go b/pkg/v1/partial/index_test.go
new file mode 100644
index 0000000..c289758
--- /dev/null
+++ b/pkg/v1/partial/index_test.go
@@ -0,0 +1,119 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial_test
+
+import (
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestFindManifests(t *testing.T) {
+ ii, err := random.Index(100, 5, 6) // random image of 6 manifests, each having 5 layers of size 100
+ if err != nil {
+ t.Fatal("could not create random index:", err)
+ }
+ m, _ := ii.IndexManifest()
+ digest := m.Manifests[0].Digest
+
+ matcher := func(desc v1.Descriptor) bool {
+ return desc.Digest != digest
+ }
+
+ descriptors, err := partial.FindManifests(ii, matcher)
+ expected := len(m.Manifests) - 1
+ switch {
+ case err != nil:
+ t.Error("unexpected error:", err)
+ case len(descriptors) != expected:
+ t.Errorf("failed on manifests, actual %d, expected %d", len(descriptors), expected)
+ }
+}
+
+func TestFindImages(t *testing.T) {
+ // create our imageindex with which to work
+ ii, err := random.Index(100, 5, 6) // random image of 6 manifests, each having 5 layers of size 100
+ if err != nil {
+ t.Fatal("could not create random index:", err)
+ }
+ m, _ := ii.IndexManifest()
+ digest := m.Manifests[0].Digest
+
+ matcher := func(desc v1.Descriptor) bool {
+ return desc.Digest != digest
+ }
+ images, err := partial.FindImages(ii, matcher)
+ expected := len(m.Manifests) - 1
+ switch {
+ case err != nil:
+ t.Error("unexpected error:", err)
+ case len(images) != expected:
+ t.Errorf("failed on images, actual %d, expected %d", len(images), expected)
+ }
+}
+
+func TestFindIndexes(t *testing.T) {
+ // there is no utility to generate an index of indexes, so we need to create one
+ // base index
+ var (
+ indexCount = 5
+ imageCount = 7
+ )
+ base := empty.Index
+ // we now have 5 indexes and 5 images, so wrap them into a single index
+ adds := []mutate.IndexAddendum{}
+ for i := 0; i < indexCount; i++ {
+ ii, err := random.Index(100, 1, 1)
+ if err != nil {
+ t.Fatalf("%d: unable to create random index: %v", i, err)
+ }
+ adds = append(adds, mutate.IndexAddendum{
+ Add: ii,
+ Descriptor: v1.Descriptor{
+ MediaType: types.OCIImageIndex,
+ },
+ })
+ }
+ for i := 0; i < imageCount; i++ {
+ img, err := random.Image(100, 1)
+ if err != nil {
+ t.Fatalf("%d: unable to create random image: %v", i, err)
+ }
+ adds = append(adds, mutate.IndexAddendum{
+ Add: img,
+ Descriptor: v1.Descriptor{
+ MediaType: types.OCIManifestSchema1,
+ },
+ })
+ }
+
+ // just see if it finds all of the indexes
+ matcher := func(desc v1.Descriptor) bool {
+ return true
+ }
+ index := mutate.AppendManifests(base, adds...)
+ idxes, err := partial.FindIndexes(index, matcher)
+ switch {
+ case err != nil:
+ t.Error("unexpected error:", err)
+ case len(idxes) != indexCount:
+ t.Errorf("failed on index, actual %d, expected %d", len(idxes), indexCount)
+ }
+}
diff --git a/pkg/v1/partial/uncompressed.go b/pkg/v1/partial/uncompressed.go
new file mode 100644
index 0000000..df20d3a
--- /dev/null
+++ b/pkg/v1/partial/uncompressed.go
@@ -0,0 +1,223 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial
+
+import (
+ "bytes"
+ "io"
+ "sync"
+
+ "github.com/google/go-containerregistry/internal/gzip"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// UncompressedLayer represents the bare minimum interface a natively
+// uncompressed layer must implement for us to produce a v1.Layer
+type UncompressedLayer interface {
+ // DiffID returns the Hash of the uncompressed layer.
+ DiffID() (v1.Hash, error)
+
+ // Uncompressed returns an io.ReadCloser for the uncompressed layer contents.
+ Uncompressed() (io.ReadCloser, error)
+
+ // Returns the mediaType for the compressed Layer
+ MediaType() (types.MediaType, error)
+}
+
+// uncompressedLayerExtender implements v1.Image using the uncompressed base properties.
+type uncompressedLayerExtender struct {
+ UncompressedLayer
+ // Memoize size/hash so that the methods aren't twice as
+ // expensive as doing this manually.
+ hash v1.Hash
+ size int64
+ hashSizeError error
+ once sync.Once
+}
+
+// Compressed implements v1.Layer
+func (ule *uncompressedLayerExtender) Compressed() (io.ReadCloser, error) {
+ u, err := ule.Uncompressed()
+ if err != nil {
+ return nil, err
+ }
+ return gzip.ReadCloser(u), nil
+}
+
+// Digest implements v1.Layer
+func (ule *uncompressedLayerExtender) Digest() (v1.Hash, error) {
+ ule.calcSizeHash()
+ return ule.hash, ule.hashSizeError
+}
+
+// Size implements v1.Layer
+func (ule *uncompressedLayerExtender) Size() (int64, error) {
+ ule.calcSizeHash()
+ return ule.size, ule.hashSizeError
+}
+
+func (ule *uncompressedLayerExtender) calcSizeHash() {
+ ule.once.Do(func() {
+ var r io.ReadCloser
+ r, ule.hashSizeError = ule.Compressed()
+ if ule.hashSizeError != nil {
+ return
+ }
+ defer r.Close()
+ ule.hash, ule.size, ule.hashSizeError = v1.SHA256(r)
+ })
+}
+
+// UncompressedToLayer fills in the missing methods from an UncompressedLayer so that it implements v1.Layer
+func UncompressedToLayer(ul UncompressedLayer) (v1.Layer, error) {
+ return &uncompressedLayerExtender{UncompressedLayer: ul}, nil
+}
+
+// UncompressedImageCore represents the bare minimum interface a natively
+// uncompressed image must implement for us to produce a v1.Image
+type UncompressedImageCore interface {
+ ImageCore
+
+ // LayerByDiffID is a variation on the v1.Image method, which returns
+ // an UncompressedLayer instead.
+ LayerByDiffID(v1.Hash) (UncompressedLayer, error)
+}
+
+// UncompressedToImage fills in the missing methods from an UncompressedImageCore so that it implements v1.Image.
+func UncompressedToImage(uic UncompressedImageCore) (v1.Image, error) {
+ return &uncompressedImageExtender{
+ UncompressedImageCore: uic,
+ }, nil
+}
+
+// uncompressedImageExtender implements v1.Image by extending UncompressedImageCore with the
+// appropriate methods computed from the minimal core.
+type uncompressedImageExtender struct {
+ UncompressedImageCore
+
+ lock sync.Mutex
+ manifest *v1.Manifest
+}
+
+// Assert that our extender type completes the v1.Image interface
+var _ v1.Image = (*uncompressedImageExtender)(nil)
+
+// Digest implements v1.Image
+func (i *uncompressedImageExtender) Digest() (v1.Hash, error) {
+ return Digest(i)
+}
+
+// Manifest implements v1.Image
+func (i *uncompressedImageExtender) Manifest() (*v1.Manifest, error) {
+ i.lock.Lock()
+ defer i.lock.Unlock()
+ if i.manifest != nil {
+ return i.manifest, nil
+ }
+
+ b, err := i.RawConfigFile()
+ if err != nil {
+ return nil, err
+ }
+
+ cfgHash, cfgSize, err := v1.SHA256(bytes.NewReader(b))
+ if err != nil {
+ return nil, err
+ }
+
+ m := &v1.Manifest{
+ SchemaVersion: 2,
+ MediaType: types.DockerManifestSchema2,
+ Config: v1.Descriptor{
+ MediaType: types.DockerConfigJSON,
+ Size: cfgSize,
+ Digest: cfgHash,
+ },
+ }
+
+ ls, err := i.Layers()
+ if err != nil {
+ return nil, err
+ }
+
+ m.Layers = make([]v1.Descriptor, len(ls))
+ for i, l := range ls {
+ desc, err := Descriptor(l)
+ if err != nil {
+ return nil, err
+ }
+
+ m.Layers[i] = *desc
+ }
+
+ i.manifest = m
+ return i.manifest, nil
+}
+
+// RawManifest implements v1.Image
+func (i *uncompressedImageExtender) RawManifest() ([]byte, error) {
+ return RawManifest(i)
+}
+
+// Size implements v1.Image
+func (i *uncompressedImageExtender) Size() (int64, error) {
+ return Size(i)
+}
+
+// ConfigName implements v1.Image
+func (i *uncompressedImageExtender) ConfigName() (v1.Hash, error) {
+ return ConfigName(i)
+}
+
+// ConfigFile implements v1.Image
+func (i *uncompressedImageExtender) ConfigFile() (*v1.ConfigFile, error) {
+ return ConfigFile(i)
+}
+
+// Layers implements v1.Image
+func (i *uncompressedImageExtender) Layers() ([]v1.Layer, error) {
+ diffIDs, err := DiffIDs(i)
+ if err != nil {
+ return nil, err
+ }
+ ls := make([]v1.Layer, 0, len(diffIDs))
+ for _, h := range diffIDs {
+ l, err := i.LayerByDiffID(h)
+ if err != nil {
+ return nil, err
+ }
+ ls = append(ls, l)
+ }
+ return ls, nil
+}
+
+// LayerByDiffID implements v1.Image
+func (i *uncompressedImageExtender) LayerByDiffID(diffID v1.Hash) (v1.Layer, error) {
+ ul, err := i.UncompressedImageCore.LayerByDiffID(diffID)
+ if err != nil {
+ return nil, err
+ }
+ return UncompressedToLayer(ul)
+}
+
+// LayerByDigest implements v1.Image
+func (i *uncompressedImageExtender) LayerByDigest(h v1.Hash) (v1.Layer, error) {
+ diffID, err := BlobToDiffID(i, h)
+ if err != nil {
+ return nil, err
+ }
+ return i.LayerByDiffID(diffID)
+}
diff --git a/pkg/v1/partial/uncompressed_test.go b/pkg/v1/partial/uncompressed_test.go
new file mode 100644
index 0000000..ce5a6fa
--- /dev/null
+++ b/pkg/v1/partial/uncompressed_test.go
@@ -0,0 +1,233 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial_test
+
+import (
+ "io"
+ "os"
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/compare"
+ legacy "github.com/google/go-containerregistry/pkg/legacy/tarball"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+// foreignLayer implements both partial.Describable and partial.UncompressedLayer.
+type foreignLayer struct {
+ wrapped v1.Layer
+}
+
+func (l *foreignLayer) Digest() (v1.Hash, error) {
+ return l.wrapped.Digest()
+}
+
+func (l *foreignLayer) Size() (int64, error) {
+ return l.wrapped.Size()
+}
+
+func (l *foreignLayer) MediaType() (types.MediaType, error) {
+ return types.DockerForeignLayer, nil
+}
+
+func (l *foreignLayer) Uncompressed() (io.ReadCloser, error) {
+ return l.wrapped.Uncompressed()
+}
+
+func (l *foreignLayer) DiffID() (v1.Hash, error) {
+ return l.wrapped.DiffID()
+}
+
+func (l *foreignLayer) Descriptor() (*v1.Descriptor, error) {
+ r, err := l.wrapped.Compressed()
+ if err != nil {
+ return nil, err
+ }
+ h, sz, err := v1.SHA256(r)
+ if err != nil {
+ return nil, err
+ }
+ return &v1.Descriptor{
+ Digest: h,
+ Size: sz,
+ MediaType: types.DockerForeignLayer,
+ URLs: []string{"http://example.com"},
+ }, nil
+}
+
+func (l *foreignLayer) UncompressedSize() (int64, error) {
+ return partial.UncompressedSize(l.wrapped)
+}
+
+func TestUncompressedLayer(t *testing.T) {
+ randLayer, err := random.Layer(1024, types.DockerForeignLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ l := &foreignLayer{randLayer}
+
+ desc, err := partial.Descriptor(l)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if want, got := desc.URLs[0], "http://example.com"; want != got {
+ t.Errorf("URLs[0] = %s != %s", got, want)
+ }
+
+ layer, err := partial.UncompressedToLayer(l)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Layer(layer); err != nil {
+ t.Errorf("validate.Layer: %v", err)
+ }
+ if _, err := partial.UncompressedSize(layer); err != nil {
+ t.Errorf("partial.UncompressedSize: %v", err)
+ }
+}
+
+// legacy/tarball.Write + tarball.Image leverages a lot of uncompressed partials.
+//
+// This is cribbed from pkg/legacy/tarball just to get intra-package coverage.
+func TestLegacyWrite(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file.")
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image + layer with Descriptor().
+ randImage, err := random.Image(256, 2)
+ if err != nil {
+ t.Fatalf("Error creating random image: %v", err)
+ }
+ randLayer, err := random.Layer(1024, types.DockerForeignLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ l, err := partial.UncompressedToLayer(&foreignLayer{randLayer})
+ if err != nil {
+ t.Fatal(err)
+ }
+ img, err := mutate.AppendLayers(randImage, l)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag: %v", err)
+ }
+ o, err := os.Create(fp.Name())
+ if err != nil {
+ t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
+ }
+ defer o.Close()
+ if err := legacy.Write(tag, img, o); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+
+ // Make sure the image is valid and can be loaded.
+ // Load it both by nil and by its name.
+ for _, it := range []*name.Tag{nil, &tag} {
+ tarImage, err := tarball.ImageFromPath(fp.Name(), it)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+ if err := validate.Image(tarImage); err != nil {
+ t.Errorf("validate.Image: %v", err)
+ }
+ if err := compare.Images(img, tarImage); err != nil {
+ t.Errorf("compare.Images: %v", err)
+ }
+ }
+
+ // Try loading a different tag, it should error.
+ fakeTag, err := name.NewTag("gcr.io/notthistag:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error generating tag: %v", err)
+ }
+ if _, err := tarball.ImageFromPath(fp.Name(), &fakeTag); err == nil {
+ t.Errorf("Expected error loading tag %v from image", fakeTag)
+ }
+}
+
+type uncompressedImage struct {
+ img v1.Image
+}
+
+func (i *uncompressedImage) RawConfigFile() ([]byte, error) {
+ return i.img.RawConfigFile()
+}
+
+func (i *uncompressedImage) MediaType() (types.MediaType, error) {
+ return i.img.MediaType()
+}
+
+func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) {
+ return i.img.LayerByDiffID(h)
+}
+
+func (i *uncompressedImage) Descriptor() (*v1.Descriptor, error) {
+ return partial.Descriptor(i.img)
+}
+
+func TestUncompressed(t *testing.T) {
+ rnd, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ core := &uncompressedImage{rnd}
+
+ img, err := partial.UncompressedToImage(core)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validate.Image: %v", err)
+ }
+ if _, err := partial.Descriptor(img); err != nil {
+ t.Fatalf("partial.Descriptor: %v", err)
+ }
+
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ layer, err := partial.UncompressedToLayer(&fastpathLayer{layers[0]})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ok, err := partial.Exists(layer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, want := ok, true; got != want {
+ t.Errorf("Exists() = %t != %t", got, want)
+ }
+}
diff --git a/pkg/v1/partial/with.go b/pkg/v1/partial/with.go
new file mode 100644
index 0000000..c8b22b3
--- /dev/null
+++ b/pkg/v1/partial/with.go
@@ -0,0 +1,436 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// WithRawConfigFile defines the subset of v1.Image used by these helper methods
+type WithRawConfigFile interface {
+ // RawConfigFile returns the serialized bytes of this image's config file.
+ RawConfigFile() ([]byte, error)
+}
+
+// ConfigFile is a helper for implementing v1.Image
+func ConfigFile(i WithRawConfigFile) (*v1.ConfigFile, error) {
+ b, err := i.RawConfigFile()
+ if err != nil {
+ return nil, err
+ }
+ return v1.ParseConfigFile(bytes.NewReader(b))
+}
+
+// ConfigName is a helper for implementing v1.Image
+func ConfigName(i WithRawConfigFile) (v1.Hash, error) {
+ b, err := i.RawConfigFile()
+ if err != nil {
+ return v1.Hash{}, err
+ }
+ h, _, err := v1.SHA256(bytes.NewReader(b))
+ return h, err
+}
+
+type configLayer struct {
+ hash v1.Hash
+ content []byte
+}
+
+// Digest implements v1.Layer
+func (cl *configLayer) Digest() (v1.Hash, error) {
+ return cl.hash, nil
+}
+
+// DiffID implements v1.Layer
+func (cl *configLayer) DiffID() (v1.Hash, error) {
+ return cl.hash, nil
+}
+
+// Uncompressed implements v1.Layer
+func (cl *configLayer) Uncompressed() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewBuffer(cl.content)), nil
+}
+
+// Compressed implements v1.Layer
+func (cl *configLayer) Compressed() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewBuffer(cl.content)), nil
+}
+
+// Size implements v1.Layer
+func (cl *configLayer) Size() (int64, error) {
+ return int64(len(cl.content)), nil
+}
+
+func (cl *configLayer) MediaType() (types.MediaType, error) {
+ // Defaulting this to OCIConfigJSON as it should remain
+ // backwards compatible with DockerConfigJSON
+ return types.OCIConfigJSON, nil
+}
+
+var _ v1.Layer = (*configLayer)(nil)
+
+// withConfigLayer allows partial image implementations to provide a layer
+// for their config file.
+type withConfigLayer interface {
+ ConfigLayer() (v1.Layer, error)
+}
+
+// ConfigLayer implements v1.Layer from the raw config bytes.
+// This is so that clients (e.g. remote) can access the config as a blob.
+//
+// Images that want to return a specific layer implementation can implement
+// withConfigLayer.
+func ConfigLayer(i WithRawConfigFile) (v1.Layer, error) {
+ if wcl, ok := unwrap(i).(withConfigLayer); ok {
+ return wcl.ConfigLayer()
+ }
+
+ h, err := ConfigName(i)
+ if err != nil {
+ return nil, err
+ }
+ rcfg, err := i.RawConfigFile()
+ if err != nil {
+ return nil, err
+ }
+ return &configLayer{
+ hash: h,
+ content: rcfg,
+ }, nil
+}
+
+// WithConfigFile defines the subset of v1.Image used by these helper methods
+type WithConfigFile interface {
+ // ConfigFile returns this image's config file.
+ ConfigFile() (*v1.ConfigFile, error)
+}
+
+// DiffIDs is a helper for implementing v1.Image
+func DiffIDs(i WithConfigFile) ([]v1.Hash, error) {
+ cfg, err := i.ConfigFile()
+ if err != nil {
+ return nil, err
+ }
+ return cfg.RootFS.DiffIDs, nil
+}
+
+// RawConfigFile is a helper for implementing v1.Image
+func RawConfigFile(i WithConfigFile) ([]byte, error) {
+ cfg, err := i.ConfigFile()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(cfg)
+}
+
+// WithRawManifest defines the subset of v1.Image used by these helper methods
+type WithRawManifest interface {
+ // RawManifest returns the serialized bytes of this image's config file.
+ RawManifest() ([]byte, error)
+}
+
+// Digest is a helper for implementing v1.Image
+func Digest(i WithRawManifest) (v1.Hash, error) {
+ mb, err := i.RawManifest()
+ if err != nil {
+ return v1.Hash{}, err
+ }
+ digest, _, err := v1.SHA256(bytes.NewReader(mb))
+ return digest, err
+}
+
+// Manifest is a helper for implementing v1.Image
+func Manifest(i WithRawManifest) (*v1.Manifest, error) {
+ b, err := i.RawManifest()
+ if err != nil {
+ return nil, err
+ }
+ return v1.ParseManifest(bytes.NewReader(b))
+}
+
+// WithManifest defines the subset of v1.Image used by these helper methods
+type WithManifest interface {
+ // Manifest returns this image's Manifest object.
+ Manifest() (*v1.Manifest, error)
+}
+
+// RawManifest is a helper for implementing v1.Image
+func RawManifest(i WithManifest) ([]byte, error) {
+ m, err := i.Manifest()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(m)
+}
+
+// Size is a helper for implementing v1.Image
+func Size(i WithRawManifest) (int64, error) {
+ b, err := i.RawManifest()
+ if err != nil {
+ return -1, err
+ }
+ return int64(len(b)), nil
+}
+
+// FSLayers is a helper for implementing v1.Image
+func FSLayers(i WithManifest) ([]v1.Hash, error) {
+ m, err := i.Manifest()
+ if err != nil {
+ return nil, err
+ }
+ fsl := make([]v1.Hash, len(m.Layers))
+ for i, l := range m.Layers {
+ fsl[i] = l.Digest
+ }
+ return fsl, nil
+}
+
+// BlobSize is a helper for implementing v1.Image
+func BlobSize(i WithManifest, h v1.Hash) (int64, error) {
+ d, err := BlobDescriptor(i, h)
+ if err != nil {
+ return -1, err
+ }
+ return d.Size, nil
+}
+
+// BlobDescriptor is a helper for implementing v1.Image
+func BlobDescriptor(i WithManifest, h v1.Hash) (*v1.Descriptor, error) {
+ m, err := i.Manifest()
+ if err != nil {
+ return nil, err
+ }
+
+ if m.Config.Digest == h {
+ return &m.Config, nil
+ }
+
+ for _, l := range m.Layers {
+ if l.Digest == h {
+ return &l, nil
+ }
+ }
+ return nil, fmt.Errorf("blob %v not found", h)
+}
+
+// WithManifestAndConfigFile defines the subset of v1.Image used by these helper methods
+type WithManifestAndConfigFile interface {
+ WithConfigFile
+
+ // Manifest returns this image's Manifest object.
+ Manifest() (*v1.Manifest, error)
+}
+
+// BlobToDiffID is a helper for mapping between compressed
+// and uncompressed blob hashes.
+func BlobToDiffID(i WithManifestAndConfigFile, h v1.Hash) (v1.Hash, error) {
+ blobs, err := FSLayers(i)
+ if err != nil {
+ return v1.Hash{}, err
+ }
+ diffIDs, err := DiffIDs(i)
+ if err != nil {
+ return v1.Hash{}, err
+ }
+ if len(blobs) != len(diffIDs) {
+ return v1.Hash{}, fmt.Errorf("mismatched fs layers (%d) and diff ids (%d)", len(blobs), len(diffIDs))
+ }
+ for i, blob := range blobs {
+ if blob == h {
+ return diffIDs[i], nil
+ }
+ }
+ return v1.Hash{}, fmt.Errorf("unknown blob %v", h)
+}
+
+// DiffIDToBlob is a helper for mapping between uncompressed
+// and compressed blob hashes.
+func DiffIDToBlob(wm WithManifestAndConfigFile, h v1.Hash) (v1.Hash, error) {
+ blobs, err := FSLayers(wm)
+ if err != nil {
+ return v1.Hash{}, err
+ }
+ diffIDs, err := DiffIDs(wm)
+ if err != nil {
+ return v1.Hash{}, err
+ }
+ if len(blobs) != len(diffIDs) {
+ return v1.Hash{}, fmt.Errorf("mismatched fs layers (%d) and diff ids (%d)", len(blobs), len(diffIDs))
+ }
+ for i, diffID := range diffIDs {
+ if diffID == h {
+ return blobs[i], nil
+ }
+ }
+ return v1.Hash{}, fmt.Errorf("unknown diffID %v", h)
+}
+
+// WithDiffID defines the subset of v1.Layer for exposing the DiffID method.
+type WithDiffID interface {
+ DiffID() (v1.Hash, error)
+}
+
+// withDescriptor allows partial layer implementations to provide a layer
+// descriptor to the partial image manifest builder. This allows partial
+// uncompressed layers to provide foreign layer metadata like URLs to the
+// uncompressed image manifest.
+type withDescriptor interface {
+ Descriptor() (*v1.Descriptor, error)
+}
+
+// Describable represents something for which we can produce a v1.Descriptor.
+type Describable interface {
+ Digest() (v1.Hash, error)
+ MediaType() (types.MediaType, error)
+ Size() (int64, error)
+}
+
+// Descriptor returns a v1.Descriptor given a Describable. It also encodes
+// some logic for unwrapping things that have been wrapped by
+// CompressedToLayer, UncompressedToLayer, CompressedToImage, or
+// UncompressedToImage.
+func Descriptor(d Describable) (*v1.Descriptor, error) {
+ // If Describable implements Descriptor itself, return that.
+ if wd, ok := unwrap(d).(withDescriptor); ok {
+ return wd.Descriptor()
+ }
+
+ // If all else fails, compute the descriptor from the individual methods.
+ var (
+ desc v1.Descriptor
+ err error
+ )
+
+ if desc.Size, err = d.Size(); err != nil {
+ return nil, err
+ }
+ if desc.Digest, err = d.Digest(); err != nil {
+ return nil, err
+ }
+ if desc.MediaType, err = d.MediaType(); err != nil {
+ return nil, err
+ }
+ if wat, ok := d.(withArtifactType); ok {
+ if desc.ArtifactType, err = wat.ArtifactType(); err != nil {
+ return nil, err
+ }
+ } else {
+ if wrm, ok := d.(WithRawManifest); ok && desc.MediaType.IsImage() {
+ mf, _ := Manifest(wrm)
+ // Failing to parse as a manifest should just be ignored.
+ // The manifest might not be valid, and that's okay.
+ if mf != nil && !mf.Config.MediaType.IsConfig() {
+ desc.ArtifactType = string(mf.Config.MediaType)
+ }
+ }
+ }
+
+ return &desc, nil
+}
+
+type withArtifactType interface {
+ ArtifactType() (string, error)
+}
+
+type withUncompressedSize interface {
+ UncompressedSize() (int64, error)
+}
+
+// UncompressedSize returns the size of the Uncompressed layer. If the
+// underlying implementation doesn't implement UncompressedSize directly,
+// this will compute the uncompressedSize by reading everything returned
+// by Compressed(). This is potentially expensive and may consume the contents
+// for streaming layers.
+func UncompressedSize(l v1.Layer) (int64, error) {
+ // If the layer implements UncompressedSize itself, return that.
+ if wus, ok := unwrap(l).(withUncompressedSize); ok {
+ return wus.UncompressedSize()
+ }
+
+ // The layer doesn't implement UncompressedSize, we need to compute it.
+ rc, err := l.Uncompressed()
+ if err != nil {
+ return -1, err
+ }
+ defer rc.Close()
+
+ return io.Copy(io.Discard, rc)
+}
+
+type withExists interface {
+ Exists() (bool, error)
+}
+
+// Exists checks to see if a layer exists. This is a hack to work around the
+// mistakes of the partial package. Don't use this.
+func Exists(l v1.Layer) (bool, error) {
+ // If the layer implements Exists itself, return that.
+ if we, ok := unwrap(l).(withExists); ok {
+ return we.Exists()
+ }
+
+ // The layer doesn't implement Exists, so we hope that calling Compressed()
+ // is enough to trigger an error if the layer does not exist.
+ rc, err := l.Compressed()
+ if err != nil {
+ return false, err
+ }
+ defer rc.Close()
+
+ // We may want to try actually reading a single byte, but if we need to do
+ // that, we should just fix this hack.
+ return true, nil
+}
+
+// Recursively unwrap our wrappers so that we can check for the original implementation.
+// We might want to expose this?
+func unwrap(i any) any {
+ if ule, ok := i.(*uncompressedLayerExtender); ok {
+ return unwrap(ule.UncompressedLayer)
+ }
+ if cle, ok := i.(*compressedLayerExtender); ok {
+ return unwrap(cle.CompressedLayer)
+ }
+ if uie, ok := i.(*uncompressedImageExtender); ok {
+ return unwrap(uie.UncompressedImageCore)
+ }
+ if cie, ok := i.(*compressedImageExtender); ok {
+ return unwrap(cie.CompressedImageCore)
+ }
+ return i
+}
+
+// ArtifactType returns the artifact type for the given manifest.
+//
+// If the manifest reports its own artifact type, that's returned, otherwise
+// the manifest is parsed and, if successful, its config.mediaType is returned.
+func ArtifactType(w WithManifest) (string, error) {
+ if wat, ok := w.(withArtifactType); ok {
+ return wat.ArtifactType()
+ }
+ mf, _ := w.Manifest()
+ // Failing to parse as a manifest should just be ignored.
+ // The manifest might not be valid, and that's okay.
+ if mf != nil && !mf.Config.MediaType.IsConfig() {
+ return string(mf.Config.MediaType), nil
+ }
+ return "", nil
+}
diff --git a/pkg/v1/partial/with_test.go b/pkg/v1/partial/with_test.go
new file mode 100644
index 0000000..7796bd9
--- /dev/null
+++ b/pkg/v1/partial/with_test.go
@@ -0,0 +1,246 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package partial_test
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestRawConfigFile(t *testing.T) {
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ part, err := partial.RawConfigFile(img)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ method, err := img.RawConfigFile()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(part) != string(method) {
+ t.Errorf("mismatched config file: %s vs %s", part, method)
+ }
+}
+
+func TestDigest(t *testing.T) {
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ part, err := partial.Digest(img)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ method, err := img.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if part != method {
+ t.Errorf("mismatched digest: %s vs %s", part, method)
+ }
+}
+
+func TestManifest(t *testing.T) {
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ part, err := partial.Manifest(img)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ method, err := img.Manifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := cmp.Diff(part, method); diff != "" {
+ t.Errorf("mismatched manifest: %v", diff)
+ }
+}
+
+func TestSize(t *testing.T) {
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ part, err := partial.Size(img)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ method, err := img.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := cmp.Diff(part, method); diff != "" {
+ t.Errorf("mismatched size: %v", diff)
+ }
+}
+
+func TestDiffIDToBlob(t *testing.T) {
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cf, err := img.ConfigFile()
+ if err != nil {
+ t.Fatal(err)
+ }
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ want, err := layers[0].Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := partial.DiffIDToBlob(img, cf.RootFS.DiffIDs[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("mismatched digest: %v", diff)
+ }
+
+ if _, err := partial.DiffIDToBlob(img, want); err == nil {
+ t.Errorf("expected err, got nil")
+ }
+}
+
+func TestBlobToDiffID(t *testing.T) {
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cf, err := img.ConfigFile()
+ if err != nil {
+ t.Fatal(err)
+ }
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ d, err := layers[0].Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := cf.RootFS.DiffIDs[0]
+ got, err := partial.BlobToDiffID(img, d)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("mismatched digest: %v", diff)
+ }
+
+ if _, err := partial.BlobToDiffID(img, want); err == nil {
+ t.Errorf("expected err, got nil")
+ }
+}
+
+func TestBlobSize(t *testing.T) {
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ m, err := img.Manifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := m.Layers[0].Size
+ got, err := partial.BlobSize(img, m.Layers[0].Digest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("mismatched blob size: %v", diff)
+ }
+
+ if _, err := partial.BlobSize(img, v1.Hash{}); err == nil {
+ t.Errorf("expected err, got nil")
+ }
+}
+
+type fastpathLayer struct {
+ v1.Layer
+}
+
+func (l *fastpathLayer) UncompressedSize() (int64, error) {
+ return 100, nil
+}
+
+func (l *fastpathLayer) Exists() (bool, error) {
+ return true, nil
+}
+
+func TestUncompressedSize(t *testing.T) {
+ randLayer, err := random.Layer(1024, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fpl := &fastpathLayer{randLayer}
+ us, err := partial.UncompressedSize(fpl)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, want := us, int64(100); got != want {
+ t.Errorf("UncompressedSize() = %d != %d", got, want)
+ }
+}
+
+func TestExists(t *testing.T) {
+ randLayer, err := random.Layer(1024, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fpl := &fastpathLayer{randLayer}
+ ok, err := partial.Exists(fpl)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, want := ok, true; got != want {
+ t.Errorf("Exists() = %t != %t", got, want)
+ }
+
+ ok, err = partial.Exists(randLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, want := ok, true; got != want {
+ t.Errorf("Exists() = %t != %t", got, want)
+ }
+}
diff --git a/pkg/v1/platform.go b/pkg/v1/platform.go
new file mode 100644
index 0000000..59ca402
--- /dev/null
+++ b/pkg/v1/platform.go
@@ -0,0 +1,149 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+// Platform represents the target os/arch for an image.
+type Platform struct {
+ Architecture string `json:"architecture"`
+ OS string `json:"os"`
+ OSVersion string `json:"os.version,omitempty"`
+ OSFeatures []string `json:"os.features,omitempty"`
+ Variant string `json:"variant,omitempty"`
+ Features []string `json:"features,omitempty"`
+}
+
+func (p Platform) String() string {
+ if p.OS == "" {
+ return ""
+ }
+ var b strings.Builder
+ b.WriteString(p.OS)
+ if p.Architecture != "" {
+ b.WriteString("/")
+ b.WriteString(p.Architecture)
+ }
+ if p.Variant != "" {
+ b.WriteString("/")
+ b.WriteString(p.Variant)
+ }
+ if p.OSVersion != "" {
+ b.WriteString(":")
+ b.WriteString(p.OSVersion)
+ }
+ return b.String()
+}
+
+// ParsePlatform parses a string representing a Platform, if possible.
+func ParsePlatform(s string) (*Platform, error) {
+ var p Platform
+ parts := strings.Split(strings.TrimSpace(s), ":")
+ if len(parts) == 2 {
+ p.OSVersion = parts[1]
+ }
+ parts = strings.Split(parts[0], "/")
+ if len(parts) > 0 {
+ p.OS = parts[0]
+ }
+ if len(parts) > 1 {
+ p.Architecture = parts[1]
+ }
+ if len(parts) > 2 {
+ p.Variant = parts[2]
+ }
+ if len(parts) > 3 {
+ return nil, fmt.Errorf("too many slashes in platform spec: %s", s)
+ }
+ return &p, nil
+}
+
+// Equals returns true if the given platform is semantically equivalent to this one.
+// The order of Features and OSFeatures is not important.
+func (p Platform) Equals(o Platform) bool {
+ return p.OS == o.OS &&
+ p.Architecture == o.Architecture &&
+ p.Variant == o.Variant &&
+ p.OSVersion == o.OSVersion &&
+ stringSliceEqualIgnoreOrder(p.OSFeatures, o.OSFeatures) &&
+ stringSliceEqualIgnoreOrder(p.Features, o.Features)
+}
+
+// Satisfies returns true if this Platform "satisfies" the given spec Platform.
+//
+// Note that this is different from Equals and that Satisfies is not reflexive.
+//
+// The given spec represents "requirements" such that any missing values in the
+// spec are not compared.
+//
+// For OSFeatures and Features, Satisfies will return true if this Platform's
+// fields contain a superset of the values in the spec's fields (order ignored).
+func (p Platform) Satisfies(spec Platform) bool {
+ return satisfies(spec.OS, p.OS) &&
+ satisfies(spec.Architecture, p.Architecture) &&
+ satisfies(spec.Variant, p.Variant) &&
+ satisfies(spec.OSVersion, p.OSVersion) &&
+ satisfiesList(spec.OSFeatures, p.OSFeatures) &&
+ satisfiesList(spec.Features, p.Features)
+}
+
+func satisfies(want, have string) bool {
+ return want == "" || want == have
+}
+
+func satisfiesList(want, have []string) bool {
+ if len(want) == 0 {
+ return true
+ }
+
+ set := map[string]struct{}{}
+ for _, h := range have {
+ set[h] = struct{}{}
+ }
+
+ for _, w := range want {
+ if _, ok := set[w]; !ok {
+ return false
+ }
+ }
+
+ return true
+}
+
+// stringSliceEqual compares 2 string slices and returns if their contents are identical.
+func stringSliceEqual(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i, elm := range a {
+ if elm != b[i] {
+ return false
+ }
+ }
+ return true
+}
+
+// stringSliceEqualIgnoreOrder compares 2 string slices and returns if their contents are identical, ignoring order
+func stringSliceEqualIgnoreOrder(a, b []string) bool {
+ if a != nil && b != nil {
+ sort.Strings(a)
+ sort.Strings(b)
+ }
+ return stringSliceEqual(a, b)
+}
diff --git a/pkg/v1/platform_test.go b/pkg/v1/platform_test.go
new file mode 100644
index 0000000..80c67ed
--- /dev/null
+++ b/pkg/v1/platform_test.go
@@ -0,0 +1,235 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1_test
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+func TestPlatformString(t *testing.T) {
+ for _, c := range []struct {
+ plat v1.Platform
+ want string
+ }{{
+ v1.Platform{},
+ "",
+ }, {
+ v1.Platform{OS: "linux"},
+ "linux",
+ }, {
+ v1.Platform{OS: "linux", Architecture: "amd64"},
+ "linux/amd64",
+ }, {
+ v1.Platform{OS: "linux", Architecture: "amd64", Variant: "v7"},
+ "linux/amd64/v7",
+ }, {
+ v1.Platform{OS: "linux", Architecture: "amd64", OSVersion: "1.2.3.4"},
+ "linux/amd64:1.2.3.4",
+ }, {
+ v1.Platform{OS: "linux", Architecture: "amd64", OSVersion: "1.2.3.4", OSFeatures: []string{"a", "b"}, Features: []string{"c", "d"}},
+ "linux/amd64:1.2.3.4",
+ }} {
+ if got := c.plat.String(); got != c.want {
+ t.Errorf("got %q, want %q", got, c.want)
+ }
+
+ if len(c.plat.OSFeatures) > 0 || len(c.plat.Features) > 0 {
+ // If these values are set, roundtripping back to the
+ // Platform will be lossy, and we expect that.
+ continue
+ }
+
+ back, err := v1.ParsePlatform(c.plat.String())
+ if err != nil {
+ t.Errorf("ParsePlatform(%q): %v", c.plat, err)
+ }
+ if d := cmp.Diff(&c.plat, back); d != "" {
+ t.Errorf("ParsePlatform(%q) diff:\n%s", c.plat.String(), d)
+ }
+ }
+
+ // Known bad examples.
+ for _, s := range []string{
+ "linux/amd64/v7/s9", // too many slashes
+ } {
+ got, err := v1.ParsePlatform(s)
+ if err == nil {
+ t.Errorf("ParsePlatform(%q) wanted error; got %v", s, got)
+ }
+ }
+}
+
+func TestPlatformEquals(t *testing.T) {
+ tests := []struct {
+ a, b v1.Platform
+ equal bool
+ }{{
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ v1.Platform{Architecture: "arm64", OS: "linux"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ v1.Platform{Architecture: "amd64", OS: "darwin"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"},
+ v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "3.6"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"},
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "ubuntu"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"},
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"ac", "bd"}},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"b", "a"}},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"ac", "bd"}},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"b", "a"}},
+ true,
+ }}
+ for i, tt := range tests {
+ if equal := tt.a.Equals(tt.b); equal != tt.equal {
+ t.Errorf("%d: mismatched was %v expected %v; original (-want +got) %s", i, equal, tt.equal, cmp.Diff(tt.a, tt.b))
+ }
+ }
+}
+
+func TestPlatformSatisfies(t *testing.T) {
+ tests := []struct {
+ have, spec v1.Platform
+ sat bool
+ }{{
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ v1.Platform{Architecture: "arm64", OS: "linux"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ v1.Platform{Architecture: "amd64", OS: "darwin"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"},
+ v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "3.6"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"},
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "ubuntu"},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"},
+ v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"ac", "bd"}},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"b", "a"}},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux"},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ true,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"ac", "bd"}},
+ false,
+ }, {
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}},
+ v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"b", "a"}},
+ true,
+ }}
+ for i, tt := range tests {
+ if sat := tt.have.Satisfies(tt.spec); sat != tt.sat {
+ t.Errorf("%d: mismatched was %v expected %v; original (-want +got) %s", i, sat, tt.sat, cmp.Diff(tt.have, tt.spec))
+ }
+ }
+}
diff --git a/pkg/v1/progress.go b/pkg/v1/progress.go
new file mode 100644
index 0000000..844f04d
--- /dev/null
+++ b/pkg/v1/progress.go
@@ -0,0 +1,25 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+// Update representation of an update of transfer progress. Some functions
+// in this module can take a channel to which updates will be sent while a
+// transfer is in progress.
+// +k8s:deepcopy-gen=false
+type Update struct {
+ Total int64
+ Complete int64
+ Error error
+}
diff --git a/pkg/v1/random/doc.go b/pkg/v1/random/doc.go
new file mode 100644
index 0000000..d371276
--- /dev/null
+++ b/pkg/v1/random/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package random provides a facility for synthesizing pseudo-random images.
+package random
diff --git a/pkg/v1/random/image.go b/pkg/v1/random/image.go
new file mode 100644
index 0000000..4b28913
--- /dev/null
+++ b/pkg/v1/random/image.go
@@ -0,0 +1,116 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package random
+
+import (
+ "archive/tar"
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "io"
+ mrand "math/rand"
+ "time"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// uncompressedLayer implements partial.UncompressedLayer from raw bytes.
+type uncompressedLayer struct {
+ diffID v1.Hash
+ mediaType types.MediaType
+ content []byte
+}
+
+// DiffID implements partial.UncompressedLayer
+func (ul *uncompressedLayer) DiffID() (v1.Hash, error) {
+ return ul.diffID, nil
+}
+
+// Uncompressed implements partial.UncompressedLayer
+func (ul *uncompressedLayer) Uncompressed() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewBuffer(ul.content)), nil
+}
+
+// MediaType returns the media type of the layer
+func (ul *uncompressedLayer) MediaType() (types.MediaType, error) {
+ return ul.mediaType, nil
+}
+
+var _ partial.UncompressedLayer = (*uncompressedLayer)(nil)
+
+// Image returns a pseudo-randomly generated Image.
+func Image(byteSize, layers int64) (v1.Image, error) {
+ adds := make([]mutate.Addendum, 0, 5)
+ for i := int64(0); i < layers; i++ {
+ layer, err := Layer(byteSize, types.DockerLayer)
+ if err != nil {
+ return nil, err
+ }
+ adds = append(adds, mutate.Addendum{
+ Layer: layer,
+ History: v1.History{
+ Author: "random.Image",
+ Comment: fmt.Sprintf("this is a random history %d of %d", i, layers),
+ CreatedBy: "random",
+ Created: v1.Time{Time: time.Now()},
+ },
+ })
+ }
+
+ return mutate.Append(empty.Image, adds...)
+}
+
+// Layer returns a layer with pseudo-randomly generated content.
+func Layer(byteSize int64, mt types.MediaType) (v1.Layer, error) {
+ fileName := fmt.Sprintf("random_file_%d.txt", mrand.Int()) //nolint: gosec
+
+ // Hash the contents as we write it out to the buffer.
+ var b bytes.Buffer
+ hasher := crypto.SHA256.New()
+ mw := io.MultiWriter(&b, hasher)
+
+ // Write a single file with a random name and random contents.
+ tw := tar.NewWriter(mw)
+ if err := tw.WriteHeader(&tar.Header{
+ Name: fileName,
+ Size: byteSize,
+ Typeflag: tar.TypeReg,
+ }); err != nil {
+ return nil, err
+ }
+ if _, err := io.CopyN(tw, rand.Reader, byteSize); err != nil {
+ return nil, err
+ }
+ if err := tw.Close(); err != nil {
+ return nil, err
+ }
+
+ h := v1.Hash{
+ Algorithm: "sha256",
+ Hex: hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))),
+ }
+
+ return partial.UncompressedToLayer(&uncompressedLayer{
+ diffID: h,
+ mediaType: mt,
+ content: b.Bytes(),
+ })
+}
diff --git a/pkg/v1/random/image_test.go b/pkg/v1/random/image_test.go
new file mode 100644
index 0000000..8f30bc7
--- /dev/null
+++ b/pkg/v1/random/image_test.go
@@ -0,0 +1,129 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package random
+
+import (
+ "archive/tar"
+ "errors"
+ "io"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestManifestAndConfig(t *testing.T) {
+ want := int64(12)
+ img, err := Image(1024, want)
+ if err != nil {
+ t.Fatalf("Error loading image: %v", err)
+ }
+ manifest, err := img.Manifest()
+ if err != nil {
+ t.Fatalf("Error loading manifest: %v", err)
+ }
+ if got := int64(len(manifest.Layers)); got != want {
+ t.Fatalf("num layers; got %v, want %v", got, want)
+ }
+
+ config, err := img.ConfigFile()
+ if err != nil {
+ t.Fatalf("Error loading config file: %v", err)
+ }
+ if got := int64(len(config.RootFS.DiffIDs)); got != want {
+ t.Fatalf("num diff ids; got %v, want %v", got, want)
+ }
+
+ if err := validate.Image(img); err != nil {
+ t.Errorf("failed to validate: %v", err)
+ }
+}
+
+func TestTarLayer(t *testing.T) {
+ img, err := Image(1024, 5)
+ if err != nil {
+ t.Fatalf("Image: %v", err)
+ }
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatalf("Layers: %v", err)
+ }
+ if len(layers) != 5 {
+ t.Errorf("Got %d layers, want 5", len(layers))
+ }
+ for i, l := range layers {
+ mediaType, err := l.MediaType()
+ if err != nil {
+ t.Fatalf("MediaType: %v", err)
+ }
+ if got, want := mediaType, types.DockerLayer; got != want {
+ t.Fatalf("MediaType(); got %q, want %q", got, want)
+ }
+
+ rc, err := l.Uncompressed()
+ if err != nil {
+ t.Errorf("Uncompressed(%d): %v", i, err)
+ }
+ defer rc.Close()
+ tr := tar.NewReader(rc)
+ if _, err := tr.Next(); err != nil {
+ t.Errorf("tar.Next: %v", err)
+ }
+
+ if n, err := io.Copy(io.Discard, tr); err != nil {
+ t.Errorf("Reading tar layer: %v", err)
+ } else if n != 1024 {
+ t.Errorf("Layer %d was %d bytes, want 1024", i, n)
+ }
+
+ if _, err := tr.Next(); !errors.Is(err, io.EOF) {
+ t.Errorf("Layer contained more files; got %v, want EOF", err)
+ }
+ }
+}
+
+func TestRandomLayer(t *testing.T) {
+ l, err := Layer(1024, types.DockerLayer)
+ if err != nil {
+ t.Fatalf("Layer: %v", err)
+ }
+ mediaType, err := l.MediaType()
+ if err != nil {
+ t.Fatalf("MediaType: %v", err)
+ }
+ if got, want := mediaType, types.DockerLayer; got != want {
+ t.Errorf("MediaType(); got %q, want %q", got, want)
+ }
+
+ rc, err := l.Uncompressed()
+ if err != nil {
+ t.Fatalf("Uncompressed(): %v", err)
+ }
+ defer rc.Close()
+ tr := tar.NewReader(rc)
+ if _, err := tr.Next(); err != nil {
+ t.Fatalf("tar.Next: %v", err)
+ }
+
+ if n, err := io.Copy(io.Discard, tr); err != nil {
+ t.Errorf("Reading tar layer: %v", err)
+ } else if n != 1024 {
+ t.Errorf("Layer was %d bytes, want 1024", n)
+ }
+
+ if _, err := tr.Next(); !errors.Is(err, io.EOF) {
+ t.Errorf("Layer contained more files; got %v, want EOF", err)
+ }
+}
diff --git a/pkg/v1/random/index.go b/pkg/v1/random/index.go
new file mode 100644
index 0000000..89a8843
--- /dev/null
+++ b/pkg/v1/random/index.go
@@ -0,0 +1,111 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package random
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type randomIndex struct {
+ images map[v1.Hash]v1.Image
+ manifest *v1.IndexManifest
+}
+
+// Index returns a pseudo-randomly generated ImageIndex with count images, each
+// having the given number of layers of size byteSize.
+func Index(byteSize, layers, count int64) (v1.ImageIndex, error) {
+ manifest := v1.IndexManifest{
+ SchemaVersion: 2,
+ MediaType: types.OCIImageIndex,
+ Manifests: []v1.Descriptor{},
+ }
+
+ images := make(map[v1.Hash]v1.Image)
+ for i := int64(0); i < count; i++ {
+ img, err := Image(byteSize, layers)
+ if err != nil {
+ return nil, err
+ }
+
+ rawManifest, err := img.RawManifest()
+ if err != nil {
+ return nil, err
+ }
+ digest, size, err := v1.SHA256(bytes.NewReader(rawManifest))
+ if err != nil {
+ return nil, err
+ }
+ mediaType, err := img.MediaType()
+ if err != nil {
+ return nil, err
+ }
+
+ manifest.Manifests = append(manifest.Manifests, v1.Descriptor{
+ Digest: digest,
+ Size: size,
+ MediaType: mediaType,
+ })
+
+ images[digest] = img
+ }
+
+ return &randomIndex{
+ images: images,
+ manifest: &manifest,
+ }, nil
+}
+
+func (i *randomIndex) MediaType() (types.MediaType, error) {
+ return i.manifest.MediaType, nil
+}
+
+func (i *randomIndex) Digest() (v1.Hash, error) {
+ return partial.Digest(i)
+}
+
+func (i *randomIndex) Size() (int64, error) {
+ return partial.Size(i)
+}
+
+func (i *randomIndex) IndexManifest() (*v1.IndexManifest, error) {
+ return i.manifest, nil
+}
+
+func (i *randomIndex) RawManifest() ([]byte, error) {
+ m, err := i.IndexManifest()
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(m)
+}
+
+func (i *randomIndex) Image(h v1.Hash) (v1.Image, error) {
+ if img, ok := i.images[h]; ok {
+ return img, nil
+ }
+
+ return nil, fmt.Errorf("image not found: %v", h)
+}
+
+func (i *randomIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
+ // This is a single level index (for now?).
+ return nil, fmt.Errorf("image not found: %v", h)
+}
diff --git a/pkg/v1/random/index_test.go b/pkg/v1/random/index_test.go
new file mode 100644
index 0000000..73e744b
--- /dev/null
+++ b/pkg/v1/random/index_test.go
@@ -0,0 +1,64 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package random
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestRandomIndex(t *testing.T) {
+ ii, err := Index(1024, 5, 3)
+ if err != nil {
+ t.Fatalf("Error loading index: %v", err)
+ }
+
+ if err := validate.Index(ii); err != nil {
+ t.Errorf("validate.Index() = %v", err)
+ }
+
+ digest, err := ii.Digest()
+ if err != nil {
+ t.Fatalf("Digest(): unexpected err: %v", err)
+ }
+
+ if _, err := ii.Image(digest); err == nil {
+ t.Errorf("Image(%s): expected err, got nil", digest)
+ }
+
+ if _, err := ii.ImageIndex(digest); err == nil {
+ t.Errorf("ImageIndex(%s): expected err, got nil", digest)
+ }
+
+ mt, err := ii.MediaType()
+ if err != nil {
+ t.Errorf("MediaType(): unexpected err: %v", err)
+ }
+
+ if got, want := mt, types.OCIImageIndex; got != want {
+ t.Errorf("MediaType(): got: %v, want: %v", got, want)
+ }
+
+ man, err := ii.IndexManifest()
+ if err != nil {
+ t.Errorf("IndexManifest(): unexpected err: %v", err)
+ }
+
+ if got, want := man.MediaType, types.OCIImageIndex; got != want {
+ t.Errorf("MediaType: got: %v, want: %v", got, want)
+ }
+}
diff --git a/pkg/v1/remote/README.md b/pkg/v1/remote/README.md
new file mode 100644
index 0000000..c1e81b3
--- /dev/null
+++ b/pkg/v1/remote/README.md
@@ -0,0 +1,117 @@
+# `remote`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote)
+
+The `remote` package implements a client for accessing a registry,
+per the [OCI distribution spec](https://github.com/opencontainers/distribution-spec/blob/master/spec.md).
+
+It leans heavily on the lower level [`transport`](/pkg/v1/remote/transport) package, which handles the
+authentication handshake and structured errors.
+
+## Usage
+
+```go
+package main
+
+import (
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+func main() {
+ ref, err := name.ParseReference("gcr.io/google-containers/pause")
+ if err != nil {
+ panic(err)
+ }
+
+ img, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
+ if err != nil {
+ panic(err)
+ }
+
+ // do stuff with img
+}
+```
+
+## Structure
+
+<p align="center">
+ <img src="/images/remote.dot.svg" />
+</p>
+
+
+## Background
+
+There are a lot of confusingly similar terms that come up when talking about images in registries.
+
+### Anatomy of an image
+
+In general...
+
+* A tag refers to an image manifest.
+* An image manifest references a config file and an orderered list of _compressed_ layers by sha256 digest.
+* A config file references an ordered list of _uncompressed_ layers by sha256 digest and contains runtime configuration.
+* The sha256 digest of the config file is the [image id](https://github.com/opencontainers/image-spec/blob/master/config.md#imageid) for the image.
+
+For example, an image with two layers would look something like this:
+
+![image anatomy](/images/image-anatomy.dot.svg)
+
+### Anatomy of an index
+
+In the normal case, an [index](https://github.com/opencontainers/image-spec/blob/master/image-index.md) is used to represent a multi-platform image.
+This was the original use case for a [manifest
+list](https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list).
+
+![image index anatomy](/images/index-anatomy.dot.svg)
+
+It is possible for an index to reference another index, per the OCI
+[image-spec](https://github.com/opencontainers/image-spec/blob/master/media-types.md#compatibility-matrix).
+In theory, both an image and image index can reference arbitrary things via
+[descriptors](https://github.com/opencontainers/image-spec/blob/master/descriptor.md),
+e.g. see the [image layout
+example](https://github.com/opencontainers/image-spec/blob/master/image-layout.md#index-example),
+which references an application/xml file from an image index.
+
+That could look something like this:
+
+![strange image index anatomy](/images/index-anatomy-strange.dot.svg)
+
+Using a recursive index like this might not be possible with all registries,
+but this flexibility allows for some interesting applications, e.g. the
+[OCI Artifacts](https://github.com/opencontainers/artifacts) effort.
+
+### Anatomy of an image upload
+
+The structure of an image requires a delicate ordering when uploading an image to a registry.
+Below is a (slightly simplified) figure that describes how an image is prepared for upload
+to a registry and how the data flows between various artifacts:
+
+![upload](/images/upload.dot.svg)
+
+Note that:
+
+* A config file references the uncompressed layer contents by sha256.
+* A manifest references the compressed layer contents by sha256 and the size of the layer.
+* A manifest references the config file contents by sha256 and the size of the file.
+
+It follows that during an upload, we need to upload layers before the config file,
+and we need to upload the config file before the manifest.
+
+Sometimes, we know all of this information ahead of time, (e.g. when copying from remote.Image),
+so the ordering is less important.
+
+In other cases, e.g. when using a [`stream.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream#Layer),
+we can't compute anything until we have already uploaded the layer, so we need to be careful about ordering.
+
+## Caveats
+
+### schema 1
+
+This package does not support schema 1 images, see [`#377`](https://github.com/google/go-containerregistry/issues/377),
+however, it's possible to do _something_ useful with them via [`remote.Get`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Get),
+which doesn't try to interpret what is returned by the registry.
+
+[`crane.Copy`](https://godoc.org/github.com/google/go-containerregistry/pkg/crane#Copy) takes advantage of this to implement support for copying schema 1 images,
+see [here](https://github.com/google/go-containerregistry/blob/main/pkg/internal/legacy/copy.go).
diff --git a/pkg/v1/remote/catalog.go b/pkg/v1/remote/catalog.go
new file mode 100644
index 0000000..eb4306f
--- /dev/null
+++ b/pkg/v1/remote/catalog.go
@@ -0,0 +1,154 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+)
+
+type catalog struct {
+ Repos []string `json:"repositories"`
+}
+
+// CatalogPage calls /_catalog, returning the list of repositories on the registry.
+func CatalogPage(target name.Registry, last string, n int, options ...Option) ([]string, error) {
+ o, err := makeOptions(target, options...)
+ if err != nil {
+ return nil, err
+ }
+
+ scopes := []string{target.Scope(transport.PullScope)}
+ tr, err := transport.NewWithContext(o.context, target, o.auth, o.transport, scopes)
+ if err != nil {
+ return nil, err
+ }
+
+ query := fmt.Sprintf("last=%s&n=%d", url.QueryEscape(last), n)
+
+ uri := url.URL{
+ Scheme: target.Scheme(),
+ Host: target.RegistryStr(),
+ Path: "/v2/_catalog",
+ RawQuery: query,
+ }
+
+ client := http.Client{Transport: tr}
+ req, err := http.NewRequest(http.MethodGet, uri.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := client.Do(req.WithContext(o.context))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ return nil, err
+ }
+
+ var parsed catalog
+ if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
+ return nil, err
+ }
+
+ return parsed.Repos, nil
+}
+
+// Catalog calls /_catalog, returning the list of repositories on the registry.
+func Catalog(ctx context.Context, target name.Registry, options ...Option) ([]string, error) {
+ o, err := makeOptions(target, options...)
+ if err != nil {
+ return nil, err
+ }
+
+ scopes := []string{target.Scope(transport.PullScope)}
+ tr, err := transport.NewWithContext(o.context, target, o.auth, o.transport, scopes)
+ if err != nil {
+ return nil, err
+ }
+
+ uri := &url.URL{
+ Scheme: target.Scheme(),
+ Host: target.RegistryStr(),
+ Path: "/v2/_catalog",
+ }
+
+ if o.pageSize > 0 {
+ uri.RawQuery = fmt.Sprintf("n=%d", o.pageSize)
+ }
+
+ client := http.Client{Transport: tr}
+
+ // WithContext overrides the ctx passed directly.
+ if o.context != context.Background() {
+ ctx = o.context
+ }
+
+ var (
+ parsed catalog
+ repoList []string
+ )
+
+ // get responses until there is no next page
+ for {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ req, err := http.NewRequest("GET", uri.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ return nil, err
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
+ return nil, err
+ }
+ if err := resp.Body.Close(); err != nil {
+ return nil, err
+ }
+
+ repoList = append(repoList, parsed.Repos...)
+
+ uri, err = getNextPageURL(resp)
+ if err != nil {
+ return nil, err
+ }
+ // no next page
+ if uri == nil {
+ break
+ }
+ }
+ return repoList, nil
+}
diff --git a/pkg/v1/remote/catalog_test.go b/pkg/v1/remote/catalog_test.go
new file mode 100644
index 0000000..0a90bf6
--- /dev/null
+++ b/pkg/v1/remote/catalog_test.go
@@ -0,0 +1,183 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+func TestCatalogPage(t *testing.T) {
+ cases := []struct {
+ name string
+ responseBody []byte
+ wantErr bool
+ wantRepos []string
+ }{{
+ name: "success",
+ responseBody: []byte(`{"repositories":["test/test","foo/bar"]}`),
+ wantErr: false,
+ wantRepos: []string{"test/test", "foo/bar"},
+ }, {
+ name: "not json",
+ responseBody: []byte("notjson"),
+ wantErr: true,
+ }}
+ // TODO: add test cases for pagination
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ catalogPath := "/v2/_catalog"
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case catalogPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+
+ w.Write(tc.responseBody)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ reg, err := name.NewRegistry(u.Host)
+ if err != nil {
+ t.Fatalf("name.NewRegistry(%v) = %v", u.Host, err)
+ }
+
+ repos, err := CatalogPage(reg, "", 100)
+ if (err != nil) != tc.wantErr {
+ t.Errorf("CatalogPage() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err)
+ }
+
+ if diff := cmp.Diff(tc.wantRepos, repos); diff != "" {
+ t.Errorf("CatalogPage() wrong repos (-want +got) = %s", diff)
+ }
+ })
+ }
+}
+
+func TestCatalog(t *testing.T) {
+ cases := []struct {
+ name string
+ pages [][]byte
+ wantErr bool
+ wantRepos []string
+ }{{
+ name: "success",
+ pages: [][]byte{
+ []byte(`{"repositories":["test/one","test/two"]}`),
+ []byte(`{"repositories":["test/three","test/four"]}`),
+ },
+ wantErr: false,
+ wantRepos: []string{"test/one", "test/two", "test/three", "test/four"},
+ }, {
+ name: "not json",
+ pages: [][]byte{[]byte("notjson")},
+ wantErr: true,
+ }}
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ catalogPath := "/v2/_catalog"
+ pageTwo := "/v2/_catalog_two"
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ page := 0
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case pageTwo:
+ page = 1
+ fallthrough
+ case catalogPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+
+ if page == 0 {
+ w.Header().Set("Link", fmt.Sprintf("<%s>", pageTwo))
+ }
+ w.Write(tc.pages[page])
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ reg, err := name.NewRegistry(u.Host)
+ if err != nil {
+ t.Fatalf("name.NewRegistry(%v) = %v", u.Host, err)
+ }
+
+ repos, err := Catalog(context.Background(), reg)
+ if (err != nil) != tc.wantErr {
+ t.Errorf("Catalog() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err)
+ }
+
+ if diff := cmp.Diff(tc.wantRepos, repos); diff != "" {
+ t.Errorf("Catalog() wrong repos (-want +got) = %s", diff)
+ }
+ })
+ }
+}
+
+func TestCancelledCatalog(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ reg, err := name.NewRegistry(u.Host)
+ if err != nil {
+ t.Fatalf("name.NewRegistry(%v) = %v", u.Host, err)
+ }
+
+ _, err = Catalog(ctx, reg)
+ if want, got := context.Canceled, err; !errors.Is(got, want) {
+ t.Errorf("wanted %v got %v", want, got)
+ }
+}
diff --git a/pkg/v1/remote/check.go b/pkg/v1/remote/check.go
new file mode 100644
index 0000000..b4395c2
--- /dev/null
+++ b/pkg/v1/remote/check.go
@@ -0,0 +1,72 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+)
+
+// CheckPushPermission returns an error if the given keychain cannot authorize
+// a push operation to the given ref.
+//
+// This can be useful to check whether the caller has permission to push an
+// image before doing work to construct the image.
+//
+// TODO(#412): Remove the need for this method.
+func CheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTripper) error {
+ auth, err := kc.Resolve(ref.Context().Registry)
+ if err != nil {
+ return fmt.Errorf("resolving authorization for %v failed: %w", ref.Context().Registry, err)
+ }
+
+ scopes := []string{ref.Scope(transport.PushScope)}
+ tr, err := transport.NewWithContext(context.TODO(), ref.Context().Registry, auth, t, scopes)
+ if err != nil {
+ return fmt.Errorf("creating push check transport for %v failed: %w", ref.Context().Registry, err)
+ }
+ // TODO(jasonhall): Against GCR, just doing the token handshake is
+ // enough, but this doesn't extend to Dockerhub
+ // (https://github.com/docker/hub-feedback/issues/1771), so we actually
+ // need to initiate an upload to tell whether the credentials can
+ // authorize a push. Figure out how to return early here when we can,
+ // to avoid a roundtrip for spec-compliant registries.
+ w := writer{
+ repo: ref.Context(),
+ client: &http.Client{Transport: tr},
+ }
+ loc, _, err := w.initiateUpload(context.Background(), "", "", "")
+ if loc != "" {
+ // Since we're only initiating the upload to check whether we
+ // can, we should attempt to cancel it, in case initiating
+ // reserves some resources on the server. We shouldn't wait for
+ // cancelling to complete, and we don't care if it fails.
+ go w.cancelUpload(loc)
+ }
+ return err
+}
+
+func (w *writer) cancelUpload(loc string) {
+ req, err := http.NewRequest(http.MethodDelete, loc, nil)
+ if err != nil {
+ return
+ }
+ _, _ = w.client.Do(req)
+}
diff --git a/pkg/v1/remote/check_e2e_test.go b/pkg/v1/remote/check_e2e_test.go
new file mode 100644
index 0000000..a302230
--- /dev/null
+++ b/pkg/v1/remote/check_e2e_test.go
@@ -0,0 +1,46 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build integration
+// +build integration
+
+package remote
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+func TestCheckPushPermission_Real(t *testing.T) {
+ // Tests should not run in an environment where these registries can
+ // be pushed to.
+ for _, r := range []name.Reference{
+ name.MustParseReference("ubuntu"),
+ name.MustParseReference("google/cloud-sdk"),
+ name.MustParseReference("microsoft/dotnet:sdk"),
+ name.MustParseReference("gcr.io/non-existent-project/made-up"),
+ name.MustParseReference("gcr.io/google-containers/foo"),
+ name.MustParseReference("quay.io/username/reponame"),
+ } {
+ t.Run(r.String(), func(t *testing.T) {
+ t.Parallel()
+ if err := CheckPushPermission(r, authn.DefaultKeychain, http.DefaultTransport); err == nil {
+ t.Errorf("CheckPushPermission(%s) returned nil", r)
+ }
+ })
+ }
+}
diff --git a/pkg/v1/remote/check_test.go b/pkg/v1/remote/check_test.go
new file mode 100644
index 0000000..2f76e12
--- /dev/null
+++ b/pkg/v1/remote/check_test.go
@@ -0,0 +1,76 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+)
+
+func TestCheckPushPermission(t *testing.T) {
+ for _, c := range []struct {
+ status int
+ wantErr bool
+ }{{
+ http.StatusCreated,
+ false,
+ }, {
+ http.StatusAccepted,
+ false,
+ }, {
+ http.StatusForbidden,
+ true,
+ }, {
+ http.StatusBadRequest,
+ true,
+ }} {
+ expectedRepo := "write/time"
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ somewhereElse := fmt.Sprintf("/v2/%s/blobs/uploads/somewhere/else", expectedRepo)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ w.Header().Set("Location", "somewhere/else")
+ http.Error(w, "", c.status)
+ case somewhereElse:
+ if r.Method != http.MethodDelete {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodDelete)
+ }
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ ref := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
+ if err := CheckPushPermission(ref, authn.DefaultKeychain, http.DefaultTransport); (err != nil) != c.wantErr {
+ t.Errorf("CheckPermission(%d): got error = %v, want err = %t", c.status, err, c.wantErr)
+ }
+ }
+}
diff --git a/pkg/v1/remote/delete.go b/pkg/v1/remote/delete.go
new file mode 100644
index 0000000..74a06fd
--- /dev/null
+++ b/pkg/v1/remote/delete.go
@@ -0,0 +1,61 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+)
+
+// Delete removes the specified image reference from the remote registry.
+func Delete(ref name.Reference, options ...Option) error {
+ o, err := makeOptions(ref.Context(), options...)
+ if err != nil {
+ return err
+ }
+ scopes := []string{ref.Scope(transport.DeleteScope)}
+ tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
+ if err != nil {
+ return err
+ }
+ c := &http.Client{Transport: tr}
+
+ u := url.URL{
+ Scheme: ref.Context().Registry.Scheme(),
+ Host: ref.Context().RegistryStr(),
+ Path: fmt.Sprintf("/v2/%s/manifests/%s", ref.Context().RepositoryStr(), ref.Identifier()),
+ }
+
+ req, err := http.NewRequest(http.MethodDelete, u.String(), nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := c.Do(req.WithContext(o.context))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return transport.CheckError(resp, http.StatusOK, http.StatusAccepted)
+
+ // TODO(jason): If the manifest had a `subject`, and if the registry
+ // doesn't support Referrers, update the index pointed to by the
+ // subject's fallback tag to remove the descriptor for this manifest.
+}
diff --git a/pkg/v1/remote/delete_test.go b/pkg/v1/remote/delete_test.go
new file mode 100644
index 0000000..4918e16
--- /dev/null
+++ b/pkg/v1/remote/delete_test.go
@@ -0,0 +1,89 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+func TestDelete(t *testing.T) {
+ expectedRepo := "write/time"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case manifestPath:
+ if r.Method != http.MethodDelete {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodDelete)
+ }
+ http.Error(w, "Deleted", http.StatusOK)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := Delete(tag); err != nil {
+ t.Errorf("Delete() = %v", err)
+ }
+}
+
+func TestDeleteBadStatus(t *testing.T) {
+ expectedRepo := "write/time"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case manifestPath:
+ if r.Method != http.MethodDelete {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodDelete)
+ }
+ http.Error(w, "Boom Goes Server", http.StatusInternalServerError)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := Delete(tag); err == nil {
+ t.Error("Delete() = nil; wanted error")
+ }
+}
diff --git a/pkg/v1/remote/descriptor.go b/pkg/v1/remote/descriptor.go
new file mode 100644
index 0000000..78919d7
--- /dev/null
+++ b/pkg/v1/remote/descriptor.go
@@ -0,0 +1,511 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/google/go-containerregistry/internal/redact"
+ "github.com/google/go-containerregistry/internal/verify"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// ErrSchema1 indicates that we received a schema1 manifest from the registry.
+// This library doesn't have plans to support this legacy image format:
+// https://github.com/google/go-containerregistry/issues/377
+type ErrSchema1 struct {
+ schema string
+}
+
+// newErrSchema1 returns an ErrSchema1 with the unexpected MediaType.
+func newErrSchema1(schema types.MediaType) error {
+ return &ErrSchema1{
+ schema: string(schema),
+ }
+}
+
+// Error implements error.
+func (e *ErrSchema1) Error() string {
+ return fmt.Sprintf("unsupported MediaType: %q, see https://github.com/google/go-containerregistry/issues/377", e.schema)
+}
+
+// Descriptor provides access to metadata about remote artifact and accessors
+// for efficiently converting it into a v1.Image or v1.ImageIndex.
+type Descriptor struct {
+ fetcher
+ v1.Descriptor
+ Manifest []byte
+
+ // So we can share this implementation with Image.
+ platform v1.Platform
+}
+
+// RawManifest exists to satisfy the Taggable interface.
+func (d *Descriptor) RawManifest() ([]byte, error) {
+ return d.Manifest, nil
+}
+
+// Get returns a remote.Descriptor for the given reference. The response from
+// the registry is left un-interpreted, for the most part. This is useful for
+// querying what kind of artifact a reference represents.
+//
+// See Head if you don't need the response body.
+func Get(ref name.Reference, options ...Option) (*Descriptor, error) {
+ acceptable := []types.MediaType{
+ // Just to look at them.
+ types.DockerManifestSchema1,
+ types.DockerManifestSchema1Signed,
+ }
+ acceptable = append(acceptable, acceptableImageMediaTypes...)
+ acceptable = append(acceptable, acceptableIndexMediaTypes...)
+ return get(ref, acceptable, options...)
+}
+
+// Head returns a v1.Descriptor for the given reference by issuing a HEAD
+// request.
+//
+// Note that the server response will not have a body, so any errors encountered
+// should be retried with Get to get more details.
+func Head(ref name.Reference, options ...Option) (*v1.Descriptor, error) {
+ acceptable := []types.MediaType{
+ // Just to look at them.
+ types.DockerManifestSchema1,
+ types.DockerManifestSchema1Signed,
+ }
+ acceptable = append(acceptable, acceptableImageMediaTypes...)
+ acceptable = append(acceptable, acceptableIndexMediaTypes...)
+
+ o, err := makeOptions(ref.Context(), options...)
+ if err != nil {
+ return nil, err
+ }
+
+ f, err := makeFetcher(ref, o)
+ if err != nil {
+ return nil, err
+ }
+
+ return f.headManifest(ref, acceptable)
+}
+
+// Handle options and fetch the manifest with the acceptable MediaTypes in the
+// Accept header.
+func get(ref name.Reference, acceptable []types.MediaType, options ...Option) (*Descriptor, error) {
+ o, err := makeOptions(ref.Context(), options...)
+ if err != nil {
+ return nil, err
+ }
+ f, err := makeFetcher(ref, o)
+ if err != nil {
+ return nil, err
+ }
+ b, desc, err := f.fetchManifest(ref, acceptable)
+ if err != nil {
+ return nil, err
+ }
+ return &Descriptor{
+ fetcher: *f,
+ Manifest: b,
+ Descriptor: *desc,
+ platform: o.platform,
+ }, nil
+}
+
+// Image converts the Descriptor into a v1.Image.
+//
+// If the fetched artifact is already an image, it will just return it.
+//
+// If the fetched artifact is an index, it will attempt to resolve the index to
+// a child image with the appropriate platform.
+//
+// See WithPlatform to set the desired platform.
+func (d *Descriptor) Image() (v1.Image, error) {
+ switch d.MediaType {
+ case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
+ // We don't care to support schema 1 images:
+ // https://github.com/google/go-containerregistry/issues/377
+ return nil, newErrSchema1(d.MediaType)
+ case types.OCIImageIndex, types.DockerManifestList:
+ // We want an image but the registry has an index, resolve it to an image.
+ return d.remoteIndex().imageByPlatform(d.platform)
+ case types.OCIManifestSchema1, types.DockerManifestSchema2:
+ // These are expected. Enumerated here to allow a default case.
+ default:
+ // We could just return an error here, but some registries (e.g. static
+ // registries) don't set the Content-Type headers correctly, so instead...
+ logs.Warn.Printf("Unexpected media type for Image(): %s", d.MediaType)
+ }
+
+ // Wrap the v1.Layers returned by this v1.Image in a hint for downstream
+ // remote.Write calls to facilitate cross-repo "mounting".
+ imgCore, err := partial.CompressedToImage(d.remoteImage())
+ if err != nil {
+ return nil, err
+ }
+ return &mountableImage{
+ Image: imgCore,
+ Reference: d.Ref,
+ }, nil
+}
+
+// ImageIndex converts the Descriptor into a v1.ImageIndex.
+func (d *Descriptor) ImageIndex() (v1.ImageIndex, error) {
+ switch d.MediaType {
+ case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
+ // We don't care to support schema 1 images:
+ // https://github.com/google/go-containerregistry/issues/377
+ return nil, newErrSchema1(d.MediaType)
+ case types.OCIManifestSchema1, types.DockerManifestSchema2:
+ // We want an index but the registry has an image, nothing we can do.
+ return nil, fmt.Errorf("unexpected media type for ImageIndex(): %s; call Image() instead", d.MediaType)
+ case types.OCIImageIndex, types.DockerManifestList:
+ // These are expected.
+ default:
+ // We could just return an error here, but some registries (e.g. static
+ // registries) don't set the Content-Type headers correctly, so instead...
+ logs.Warn.Printf("Unexpected media type for ImageIndex(): %s", d.MediaType)
+ }
+ return d.remoteIndex(), nil
+}
+
+func (d *Descriptor) remoteImage() *remoteImage {
+ return &remoteImage{
+ fetcher: d.fetcher,
+ manifest: d.Manifest,
+ mediaType: d.MediaType,
+ descriptor: &d.Descriptor,
+ }
+}
+
+func (d *Descriptor) remoteIndex() *remoteIndex {
+ return &remoteIndex{
+ fetcher: d.fetcher,
+ manifest: d.Manifest,
+ mediaType: d.MediaType,
+ descriptor: &d.Descriptor,
+ }
+}
+
+// fetcher implements methods for reading from a registry.
+type fetcher struct {
+ Ref name.Reference
+ Client *http.Client
+ context context.Context
+}
+
+func makeFetcher(ref name.Reference, o *options) (*fetcher, error) {
+ tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, []string{ref.Scope(transport.PullScope)})
+ if err != nil {
+ return nil, err
+ }
+ return &fetcher{
+ Ref: ref,
+ Client: &http.Client{Transport: tr},
+ context: o.context,
+ }, nil
+}
+
+// url returns a url.Url for the specified path in the context of this remote image reference.
+func (f *fetcher) url(resource, identifier string) url.URL {
+ return url.URL{
+ Scheme: f.Ref.Context().Registry.Scheme(),
+ Host: f.Ref.Context().RegistryStr(),
+ Path: fmt.Sprintf("/v2/%s/%s/%s", f.Ref.Context().RepositoryStr(), resource, identifier),
+ }
+}
+
+// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema
+func fallbackTag(d name.Digest) name.Tag {
+ return d.Context().Tag(strings.Replace(d.DigestStr(), ":", "-", 1))
+}
+
+func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string, d name.Digest) (*v1.IndexManifest, error) {
+ // Check the Referrers API endpoint first.
+ u := f.url("referrers", d.DigestStr())
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Accept", string(types.OCIImageIndex))
+
+ resp, err := f.Client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest); err != nil {
+ return nil, err
+ }
+ if resp.StatusCode == http.StatusOK {
+ var im v1.IndexManifest
+ if err := json.NewDecoder(resp.Body).Decode(&im); err != nil {
+ return nil, err
+ }
+ return filterReferrersResponse(filter, &im), nil
+ }
+
+ // The registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
+ b, _, err := f.fetchManifest(fallbackTag(d), []types.MediaType{types.OCIImageIndex})
+ if err != nil {
+ return nil, err
+ }
+ var terr *transport.Error
+ if ok := errors.As(err, &terr); ok && terr.StatusCode == http.StatusNotFound {
+ // Not found just means there are no attachments yet. Start with an empty manifest.
+ return &v1.IndexManifest{MediaType: types.OCIImageIndex}, nil
+ }
+
+ var im v1.IndexManifest
+ if err := json.Unmarshal(b, &im); err != nil {
+ return nil, err
+ }
+
+ return filterReferrersResponse(filter, &im), nil
+}
+
+func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType) ([]byte, *v1.Descriptor, error) {
+ u := f.url("manifests", ref.Identifier())
+ req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ accept := []string{}
+ for _, mt := range acceptable {
+ accept = append(accept, string(mt))
+ }
+ req.Header.Set("Accept", strings.Join(accept, ","))
+
+ resp, err := f.Client.Do(req.WithContext(f.context))
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ return nil, nil, err
+ }
+
+ manifest, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ digest, size, err := v1.SHA256(bytes.NewReader(manifest))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ mediaType := types.MediaType(resp.Header.Get("Content-Type"))
+ contentDigest, err := v1.NewHash(resp.Header.Get("Docker-Content-Digest"))
+ if err == nil && mediaType == types.DockerManifestSchema1Signed {
+ // If we can parse the digest from the header, and it's a signed schema 1
+ // manifest, let's use that for the digest to appease older registries.
+ digest = contentDigest
+ }
+
+ // Validate the digest matches what we asked for, if pulling by digest.
+ if dgst, ok := ref.(name.Digest); ok {
+ if digest.String() != dgst.DigestStr() {
+ return nil, nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), f.Ref)
+ }
+ }
+
+ var artifactType string
+ mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
+ // Failing to parse as a manifest should just be ignored.
+ // The manifest might not be valid, and that's okay.
+ if mf != nil && !mf.Config.MediaType.IsConfig() {
+ artifactType = string(mf.Config.MediaType)
+ }
+
+ // Do nothing for tags; I give up.
+ //
+ // We'd like to validate that the "Docker-Content-Digest" header matches what is returned by the registry,
+ // but so many registries implement this incorrectly that it's not worth checking.
+ //
+ // For reference:
+ // https://github.com/GoogleContainerTools/kaniko/issues/298
+
+ // Return all this info since we have to calculate it anyway.
+ desc := v1.Descriptor{
+ Digest: digest,
+ Size: size,
+ MediaType: mediaType,
+ ArtifactType: artifactType,
+ }
+
+ return manifest, &desc, nil
+}
+
+func (f *fetcher) headManifest(ref name.Reference, acceptable []types.MediaType) (*v1.Descriptor, error) {
+ u := f.url("manifests", ref.Identifier())
+ req, err := http.NewRequest(http.MethodHead, u.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ accept := []string{}
+ for _, mt := range acceptable {
+ accept = append(accept, string(mt))
+ }
+ req.Header.Set("Accept", strings.Join(accept, ","))
+
+ resp, err := f.Client.Do(req.WithContext(f.context))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ return nil, err
+ }
+
+ mth := resp.Header.Get("Content-Type")
+ if mth == "" {
+ return nil, fmt.Errorf("HEAD %s: response did not include Content-Type header", u.String())
+ }
+ mediaType := types.MediaType(mth)
+
+ size := resp.ContentLength
+ if size == -1 {
+ return nil, fmt.Errorf("GET %s: response did not include Content-Length header", u.String())
+ }
+
+ dh := resp.Header.Get("Docker-Content-Digest")
+ if dh == "" {
+ return nil, fmt.Errorf("HEAD %s: response did not include Docker-Content-Digest header", u.String())
+ }
+ digest, err := v1.NewHash(dh)
+ if err != nil {
+ return nil, err
+ }
+
+ // Validate the digest matches what we asked for, if pulling by digest.
+ if dgst, ok := ref.(name.Digest); ok {
+ if digest.String() != dgst.DigestStr() {
+ return nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), f.Ref)
+ }
+ }
+
+ // Return all this info since we have to calculate it anyway.
+ return &v1.Descriptor{
+ Digest: digest,
+ Size: size,
+ MediaType: mediaType,
+ }, nil
+}
+
+func (f *fetcher) fetchBlob(ctx context.Context, size int64, h v1.Hash) (io.ReadCloser, error) {
+ u := f.url("blobs", h.String())
+ req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := f.Client.Do(req.WithContext(ctx))
+ if err != nil {
+ return nil, redact.Error(err)
+ }
+
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ resp.Body.Close()
+ return nil, err
+ }
+
+ // Do whatever we can.
+ // If we have an expected size and Content-Length doesn't match, return an error.
+ // If we don't have an expected size and we do have a Content-Length, use Content-Length.
+ if hsize := resp.ContentLength; hsize != -1 {
+ if size == verify.SizeUnknown {
+ size = hsize
+ } else if hsize != size {
+ return nil, fmt.Errorf("GET %s: Content-Length header %d does not match expected size %d", u.String(), hsize, size)
+ }
+ }
+
+ return verify.ReadCloser(resp.Body, size, h)
+}
+
+func (f *fetcher) headBlob(h v1.Hash) (*http.Response, error) {
+ u := f.url("blobs", h.String())
+ req, err := http.NewRequest(http.MethodHead, u.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := f.Client.Do(req.WithContext(f.context))
+ if err != nil {
+ return nil, redact.Error(err)
+ }
+
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ resp.Body.Close()
+ return nil, err
+ }
+
+ return resp, nil
+}
+
+func (f *fetcher) blobExists(h v1.Hash) (bool, error) {
+ u := f.url("blobs", h.String())
+ req, err := http.NewRequest(http.MethodHead, u.String(), nil)
+ if err != nil {
+ return false, err
+ }
+
+ resp, err := f.Client.Do(req.WithContext(f.context))
+ if err != nil {
+ return false, redact.Error(err)
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil {
+ return false, err
+ }
+
+ return resp.StatusCode == http.StatusOK, nil
+}
+
+// If filter applied, filter out by artifactType.
+// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
+func filterReferrersResponse(filter map[string]string, origIndex *v1.IndexManifest) *v1.IndexManifest {
+ newIndex := origIndex
+ if filter == nil {
+ return newIndex
+ }
+ if v, ok := filter["artifactType"]; ok {
+ tmp := []v1.Descriptor{}
+ for _, desc := range newIndex.Manifests {
+ if desc.ArtifactType == v {
+ tmp = append(tmp, desc)
+ }
+ }
+ newIndex.Manifests = tmp
+ }
+ return newIndex
+}
diff --git a/pkg/v1/remote/descriptor_test.go b/pkg/v1/remote/descriptor_test.go
new file mode 100644
index 0000000..1b77f80
--- /dev/null
+++ b/pkg/v1/remote/descriptor_test.go
@@ -0,0 +1,259 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestGetSchema1(t *testing.T) {
+ expectedRepo := "foo/bar"
+ fakeDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Header().Set("Content-Type", string(types.DockerManifestSchema1Signed))
+ w.Header().Set("Docker-Content-Digest", fakeDigest)
+ w.Write([]byte("doesn't matter"))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
+
+ // Get should succeed even for invalid json. We don't parse the response.
+ desc, err := Get(tag)
+ if err != nil {
+ t.Fatalf("Get(%s) = %v", tag, err)
+ }
+
+ if desc.Digest.String() != fakeDigest {
+ t.Errorf("Descriptor.Digest = %q, expected %q", desc.Digest, fakeDigest)
+ }
+
+ want := `unsupported MediaType: "application/vnd.docker.distribution.manifest.v1+prettyjws", see https://github.com/google/go-containerregistry/issues/377`
+ // Should fail based on media type.
+ if _, err := desc.Image(); err != nil {
+ if errors.Is(err, &ErrSchema1{}) {
+ t.Errorf("Image() = %v, expected remote.ErrSchema1", err)
+ }
+ if diff := cmp.Diff(want, err.Error()); diff != "" {
+ t.Errorf("Image() error message (-want +got) = %v", diff)
+ }
+ } else {
+ t.Errorf("Image() = %v, expected err", err)
+ }
+
+ // Should fail based on media type.
+ if _, err := desc.ImageIndex(); err != nil {
+ var s1err ErrSchema1
+ if errors.Is(err, &s1err) {
+ t.Errorf("ImageImage() = %v, expected remote.ErrSchema1", err)
+ }
+ } else {
+ t.Errorf("ImageIndex() = %v, expected err", err)
+ }
+}
+
+func TestGetImageAsIndex(t *testing.T) {
+ expectedRepo := "foo/bar"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Header().Set("Content-Type", string(types.DockerManifestSchema2))
+ w.Write([]byte("doesn't matter"))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
+
+ // Get should succeed even for invalid json. We don't parse the response.
+ desc, err := Get(tag)
+ if err != nil {
+ t.Fatalf("Get(%s) = %v", tag, err)
+ }
+
+ // Should fail based on media type.
+ if _, err := desc.ImageIndex(); err == nil {
+ t.Errorf("ImageIndex() = %v, expected err", err)
+ }
+}
+
+func TestHeadSchema1(t *testing.T) {
+ expectedRepo := "foo/bar"
+ mediaType := types.DockerManifestSchema1Signed
+ fakeDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+ response := []byte("doesn't matter")
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case manifestPath:
+ if r.Method != http.MethodHead {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
+ }
+ w.Header().Set("Content-Type", string(mediaType))
+ w.Header().Set("Content-Length", strconv.Itoa(len(response)))
+ w.Header().Set("Docker-Content-Digest", fakeDigest)
+ w.Write(response)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
+
+ // Head should succeed even for invalid json. We don't parse the response.
+ desc, err := Head(tag)
+ if err != nil {
+ t.Fatalf("Head(%s) = %v", tag, err)
+ }
+
+ if desc.MediaType != mediaType {
+ t.Errorf("Descriptor.MediaType = %q, expected %q", desc.MediaType, mediaType)
+ }
+
+ if desc.Digest.String() != fakeDigest {
+ t.Errorf("Descriptor.Digest = %q, expected %q", desc.Digest, fakeDigest)
+ }
+
+ if desc.Size != int64(len(response)) {
+ t.Errorf("Descriptor.Size = %q, expected %q", desc.Size, len(response))
+ }
+}
+
+// TestHead_MissingHeaders tests that HEAD responses missing necessary headers
+// result in errors.
+func TestHead_MissingHeaders(t *testing.T) {
+ missingType := "missing-type"
+ missingLength := "missing-length"
+ missingDigest := "missing-digest"
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/v2/" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ if r.Method != http.MethodHead {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
+ }
+ if !strings.Contains(r.URL.Path, missingType) {
+ w.Header().Set("Content-Type", "My-Media-Type")
+ }
+ if !strings.Contains(r.URL.Path, missingLength) {
+ w.Header().Set("Content-Length", "10")
+ }
+ if !strings.Contains(r.URL.Path, missingDigest) {
+ w.Header().Set("Docker-Content-Digest", "sha256:0000000000000000000000000000000000000000000000000000000000000000")
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ for _, repo := range []string{missingType, missingLength, missingDigest} {
+ tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, repo))
+ if _, err := Head(tag); err == nil {
+ t.Errorf("Head(%q): expected error, got nil", tag)
+ }
+ }
+}
+
+// TestRedactFetchBlob tests that a request to fetchBlob that gets redirected
+// to a URL that contains sensitive information has that information redacted
+// if the subsequent request fails.
+func TestRedactFetchBlob(t *testing.T) {
+ ctx := context.Background()
+ f := fetcher{
+ Ref: mustNewTag(t, "original.com/repo:latest"),
+ Client: &http.Client{
+ Transport: errTransport{},
+ },
+ context: ctx,
+ }
+ h, err := v1.NewHash("sha256:0000000000000000000000000000000000000000000000000000000000000000")
+ if err != nil {
+ t.Fatal("NewHash:", err)
+ }
+ if _, err := f.fetchBlob(ctx, 0, h); err == nil {
+ t.Fatalf("fetchBlob: expected error, got nil")
+ } else if !strings.Contains(err.Error(), "access_token=REDACTED") {
+ t.Fatalf("fetchBlob: expected error to contain redacted access token, got %v", err)
+ }
+}
+
+type errTransport struct{}
+
+func (errTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ // This simulates a registry that returns a redirect upon the first
+ // request, and then returns an error upon subsequent requests. This helps
+ // test whether error redaction takes into account URLs in error messasges
+ // that are not the original request URL.
+ if req.URL.Host == "original.com" {
+ return &http.Response{
+ StatusCode: http.StatusSeeOther,
+ Header: http.Header{"Location": []string{"https://redirected.com?access_token=SECRET"}},
+ }, nil
+ }
+ return nil, fmt.Errorf("error reaching %s", req.URL.String())
+}
diff --git a/pkg/v1/remote/doc.go b/pkg/v1/remote/doc.go
new file mode 100644
index 0000000..846ba07
--- /dev/null
+++ b/pkg/v1/remote/doc.go
@@ -0,0 +1,17 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package remote provides facilities for reading/writing v1.Images from/to
+// a remote image registry.
+package remote
diff --git a/pkg/v1/remote/error_roundtrip_test.go b/pkg/v1/remote/error_roundtrip_test.go
new file mode 100644
index 0000000..5b81ee5
--- /dev/null
+++ b/pkg/v1/remote/error_roundtrip_test.go
@@ -0,0 +1,127 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote_test
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+)
+
+func TestStatusCodeReturned(t *testing.T) {
+ tcs := []struct {
+ Description string
+ Handler http.Handler
+ }{{
+ Description: "Only returns teapot status",
+ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusTeapot)
+ }),
+ }, {
+ Description: "Handle v2, returns teapot status else",
+ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ log.Print(r.URL.Path)
+ if r.URL.Path == "/v2/" {
+ return
+ }
+ w.WriteHeader(http.StatusTeapot)
+ }),
+ }}
+
+ for _, tc := range tcs {
+ t.Run(tc.Description, func(t *testing.T) {
+ o := httptest.NewServer(tc.Handler)
+ defer o.Close()
+
+ ref, err := name.NewDigest(strings.TrimPrefix(o.URL+"/foo:@sha256:53b27244ffa2f585799adbfaf79fba5a5af104597751b289c8b235e7b8f7ebf5", "http://"))
+
+ if err != nil {
+ t.Fatalf("Unable to parse digest: %v", err)
+ }
+
+ _, err = remote.Image(ref)
+ var terr *transport.Error
+ if !errors.As(err, &terr) {
+ t.Fatalf("Unable to cast error to transport error: %v", err)
+ }
+ if terr.StatusCode != http.StatusTeapot {
+ t.Errorf("Incorrect status code received, got %v, wanted %v", terr.StatusCode, http.StatusTeapot)
+ }
+ })
+ }
+}
+
+func TestBlobStatusCodeReturned(t *testing.T) {
+ reg := registry.New()
+ rh := httptest.NewServer(reg)
+ defer rh.Close()
+ i, _ := random.Image(1024, 16)
+ tag := strings.TrimPrefix(fmt.Sprintf("%s/foo:bar", rh.URL), "http://")
+ d, _ := name.NewTag(tag)
+ if err := remote.Write(d, i); err != nil {
+ t.Fatalf("Unable to write empty image: %v", err)
+ }
+
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ log.Print(r.URL.Path)
+ if strings.Contains(r.URL.Path, "blob") {
+ w.WriteHeader(http.StatusTeapot)
+ return
+ }
+ reg.ServeHTTP(w, r)
+ })
+
+ o := httptest.NewServer(handler)
+ defer o.Close()
+
+ ref, err := name.NewTag(strings.TrimPrefix(fmt.Sprintf("%s/foo:bar", o.URL), "http://"))
+ if err != nil {
+ t.Fatalf("Unable to parse digest: %v", err)
+ }
+
+ ri, err := remote.Image(ref)
+ if err != nil {
+ t.Fatalf("Unable to fetch manifest: %v", err)
+ }
+ l, err := ri.Layers()
+ if err != nil {
+ t.Fatalf("Unable to fetch layers: %v", err)
+ }
+ _, err = l[0].Compressed()
+ var terr *transport.Error
+ if !errors.As(err, &terr) {
+ t.Fatalf("Unable to cast error to transport error: %v", err)
+ }
+ if terr.StatusCode != http.StatusTeapot {
+ t.Errorf("Incorrect status code received, got %v, wanted %v", terr.StatusCode, http.StatusTeapot)
+ }
+ _, err = l[0].Uncompressed()
+ if !errors.As(err, &terr) {
+ t.Fatalf("Unable to cast error to transport error: %v", err)
+ }
+ if terr.StatusCode != http.StatusTeapot {
+ t.Errorf("Incorrect status code received, got %v, wanted %v", terr.StatusCode, http.StatusTeapot)
+ }
+}
diff --git a/pkg/v1/remote/image.go b/pkg/v1/remote/image.go
new file mode 100644
index 0000000..fde6142
--- /dev/null
+++ b/pkg/v1/remote/image.go
@@ -0,0 +1,256 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "bytes"
+ "io"
+ "net/http"
+ "net/url"
+ "sync"
+
+ "github.com/google/go-containerregistry/internal/redact"
+ "github.com/google/go-containerregistry/internal/verify"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+var acceptableImageMediaTypes = []types.MediaType{
+ types.DockerManifestSchema2,
+ types.OCIManifestSchema1,
+}
+
+// remoteImage accesses an image from a remote registry
+type remoteImage struct {
+ fetcher
+ manifestLock sync.Mutex // Protects manifest
+ manifest []byte
+ configLock sync.Mutex // Protects config
+ config []byte
+ mediaType types.MediaType
+ descriptor *v1.Descriptor
+}
+
+func (r *remoteImage) ArtifactType() (string, error) {
+ // kind of a hack, but RawManifest does appropriate locking/memoization
+ // and makes sure r.descriptor is populated.
+ if _, err := r.RawManifest(); err != nil {
+ return "", err
+ }
+ return r.descriptor.ArtifactType, nil
+}
+
+var _ partial.CompressedImageCore = (*remoteImage)(nil)
+
+// Image provides access to a remote image reference.
+func Image(ref name.Reference, options ...Option) (v1.Image, error) {
+ desc, err := Get(ref, options...)
+ if err != nil {
+ return nil, err
+ }
+
+ return desc.Image()
+}
+
+func (r *remoteImage) MediaType() (types.MediaType, error) {
+ if string(r.mediaType) != "" {
+ return r.mediaType, nil
+ }
+ return types.DockerManifestSchema2, nil
+}
+
+func (r *remoteImage) RawManifest() ([]byte, error) {
+ r.manifestLock.Lock()
+ defer r.manifestLock.Unlock()
+ if r.manifest != nil {
+ return r.manifest, nil
+ }
+
+ // NOTE(jonjohnsonjr): We should never get here because the public entrypoints
+ // do type-checking via remote.Descriptor. I've left this here for tests that
+ // directly instantiate a remoteImage.
+ manifest, desc, err := r.fetchManifest(r.Ref, acceptableImageMediaTypes)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.descriptor == nil {
+ r.descriptor = desc
+ }
+ r.mediaType = desc.MediaType
+ r.manifest = manifest
+ return r.manifest, nil
+}
+
+func (r *remoteImage) RawConfigFile() ([]byte, error) {
+ r.configLock.Lock()
+ defer r.configLock.Unlock()
+ if r.config != nil {
+ return r.config, nil
+ }
+
+ m, err := partial.Manifest(r)
+ if err != nil {
+ return nil, err
+ }
+
+ if m.Config.Data != nil {
+ if err := verify.Descriptor(m.Config); err != nil {
+ return nil, err
+ }
+ r.config = m.Config.Data
+ return r.config, nil
+ }
+
+ body, err := r.fetchBlob(r.context, m.Config.Size, m.Config.Digest)
+ if err != nil {
+ return nil, err
+ }
+ defer body.Close()
+
+ r.config, err = io.ReadAll(body)
+ if err != nil {
+ return nil, err
+ }
+ return r.config, nil
+}
+
+// Descriptor retains the original descriptor from an index manifest.
+// See partial.Descriptor.
+func (r *remoteImage) Descriptor() (*v1.Descriptor, error) {
+ // kind of a hack, but RawManifest does appropriate locking/memoization
+ // and makes sure r.descriptor is populated.
+ _, err := r.RawManifest()
+ return r.descriptor, err
+}
+
+// remoteImageLayer implements partial.CompressedLayer
+type remoteImageLayer struct {
+ ri *remoteImage
+ digest v1.Hash
+}
+
+// Digest implements partial.CompressedLayer
+func (rl *remoteImageLayer) Digest() (v1.Hash, error) {
+ return rl.digest, nil
+}
+
+// Compressed implements partial.CompressedLayer
+func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
+ urls := []url.URL{rl.ri.url("blobs", rl.digest.String())}
+
+ // Add alternative layer sources from URLs (usually none).
+ d, err := partial.BlobDescriptor(rl, rl.digest)
+ if err != nil {
+ return nil, err
+ }
+
+ if d.Data != nil {
+ return verify.ReadCloser(io.NopCloser(bytes.NewReader(d.Data)), d.Size, d.Digest)
+ }
+
+ // We don't want to log binary layers -- this can break terminals.
+ ctx := redact.NewContext(rl.ri.context, "omitting binary blobs from logs")
+
+ for _, s := range d.URLs {
+ u, err := url.Parse(s)
+ if err != nil {
+ return nil, err
+ }
+ urls = append(urls, *u)
+ }
+
+ // The lastErr for most pulls will be the same (the first error), but for
+ // foreign layers we'll want to surface the last one, since we try to pull
+ // from the registry first, which would often fail.
+ // TODO: Maybe we don't want to try pulling from the registry first?
+ var lastErr error
+ for _, u := range urls {
+ req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := rl.ri.Client.Do(req.WithContext(ctx))
+ if err != nil {
+ lastErr = err
+ continue
+ }
+
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ resp.Body.Close()
+ lastErr = err
+ continue
+ }
+
+ return verify.ReadCloser(resp.Body, d.Size, rl.digest)
+ }
+
+ return nil, lastErr
+}
+
+// Manifest implements partial.WithManifest so that we can use partial.BlobSize below.
+func (rl *remoteImageLayer) Manifest() (*v1.Manifest, error) {
+ return partial.Manifest(rl.ri)
+}
+
+// MediaType implements v1.Layer
+func (rl *remoteImageLayer) MediaType() (types.MediaType, error) {
+ bd, err := partial.BlobDescriptor(rl, rl.digest)
+ if err != nil {
+ return "", err
+ }
+
+ return bd.MediaType, nil
+}
+
+// Size implements partial.CompressedLayer
+func (rl *remoteImageLayer) Size() (int64, error) {
+ // Look up the size of this digest in the manifest to avoid a request.
+ return partial.BlobSize(rl, rl.digest)
+}
+
+// ConfigFile implements partial.WithManifestAndConfigFile so that we can use partial.BlobToDiffID below.
+func (rl *remoteImageLayer) ConfigFile() (*v1.ConfigFile, error) {
+ return partial.ConfigFile(rl.ri)
+}
+
+// DiffID implements partial.WithDiffID so that we don't recompute a DiffID that we already have
+// available in our ConfigFile.
+func (rl *remoteImageLayer) DiffID() (v1.Hash, error) {
+ return partial.BlobToDiffID(rl, rl.digest)
+}
+
+// Descriptor retains the original descriptor from an image manifest.
+// See partial.Descriptor.
+func (rl *remoteImageLayer) Descriptor() (*v1.Descriptor, error) {
+ return partial.BlobDescriptor(rl, rl.digest)
+}
+
+// See partial.Exists.
+func (rl *remoteImageLayer) Exists() (bool, error) {
+ return rl.ri.blobExists(rl.digest)
+}
+
+// LayerByDigest implements partial.CompressedLayer
+func (r *remoteImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
+ return &remoteImageLayer{
+ ri: r,
+ digest: h,
+ }, nil
+}
diff --git a/pkg/v1/remote/image_test.go b/pkg/v1/remote/image_test.go
new file mode 100644
index 0000000..4a6c29d
--- /dev/null
+++ b/pkg/v1/remote/image_test.go
@@ -0,0 +1,743 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "path"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+const bogusDigest = "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
+
+type withDigest interface {
+ Digest() (v1.Hash, error)
+}
+
+func mustDigest(t *testing.T, img withDigest) v1.Hash {
+ h, err := img.Digest()
+ if err != nil {
+ t.Fatalf("Digest() = %v", err)
+ }
+ return h
+}
+
+func mustManifest(t *testing.T, img v1.Image) *v1.Manifest {
+ m, err := img.Manifest()
+ if err != nil {
+ t.Fatalf("Manifest() = %v", err)
+ }
+ return m
+}
+
+func mustRawManifest(t *testing.T, img Taggable) []byte {
+ m, err := img.RawManifest()
+ if err != nil {
+ t.Fatalf("RawManifest() = %v", err)
+ }
+ return m
+}
+
+func mustRawConfigFile(t *testing.T, img v1.Image) []byte {
+ c, err := img.RawConfigFile()
+ if err != nil {
+ t.Fatalf("RawConfigFile() = %v", err)
+ }
+ return c
+}
+
+func randomImage(t *testing.T) v1.Image {
+ rnd, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatalf("random.Image() = %v", err)
+ }
+ return rnd
+}
+
+func newReference(host, repo, ref string) (name.Reference, error) {
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", host, repo, ref), name.WeakValidation)
+ if err == nil {
+ return tag, nil
+ }
+ return name.NewDigest(fmt.Sprintf("%s/%s@%s", host, repo, ref), name.WeakValidation)
+}
+
+// TODO(jonjohnsonjr): Make this real.
+func TestMediaType(t *testing.T) {
+ img := remoteImage{}
+ got, err := img.MediaType()
+ if err != nil {
+ t.Fatalf("MediaType() = %v", err)
+ }
+ want := types.DockerManifestSchema2
+ if got != want {
+ t.Errorf("MediaType() = %v, want %v", got, want)
+ }
+}
+
+func TestRawManifestDigests(t *testing.T) {
+ img := randomImage(t)
+ expectedRepo := "foo/bar"
+
+ cases := []struct {
+ name string
+ ref string
+ responseBody []byte
+ contentDigest string
+ wantErr bool
+ }{{
+ name: "normal pull, by tag",
+ ref: "latest",
+ responseBody: mustRawManifest(t, img),
+ contentDigest: mustDigest(t, img).String(),
+ wantErr: false,
+ }, {
+ name: "normal pull, by digest",
+ ref: mustDigest(t, img).String(),
+ responseBody: mustRawManifest(t, img),
+ contentDigest: mustDigest(t, img).String(),
+ wantErr: false,
+ }, {
+ name: "right content-digest, wrong body, by digest",
+ ref: mustDigest(t, img).String(),
+ responseBody: []byte("not even json"),
+ contentDigest: mustDigest(t, img).String(),
+ wantErr: true,
+ }, {
+ name: "right body, wrong content-digest, by tag",
+ ref: "latest",
+ responseBody: mustRawManifest(t, img),
+ contentDigest: bogusDigest,
+ wantErr: false,
+ }, {
+ // NB: This succeeds! We don't care what the registry thinks.
+ name: "right body, wrong content-digest, by digest",
+ ref: mustDigest(t, img).String(),
+ responseBody: mustRawManifest(t, img),
+ contentDigest: bogusDigest,
+ wantErr: false,
+ }}
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, tc.ref)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+
+ w.Header().Set("Docker-Content-Digest", tc.contentDigest)
+ w.Write(tc.responseBody)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ ref, err := newReference(u.Host, expectedRepo, tc.ref)
+ if err != nil {
+ t.Fatalf("url.Parse(%v, %v, %v) = %v", u.Host, expectedRepo, tc.ref, err)
+ }
+
+ rmt := remoteImage{
+ fetcher: fetcher{
+ Ref: ref,
+ Client: http.DefaultClient,
+ context: context.Background(),
+ },
+ }
+
+ if _, err := rmt.RawManifest(); (err != nil) != tc.wantErr {
+ t.Errorf("RawManifest() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err)
+ }
+ })
+ }
+}
+
+func TestRawManifestNotFound(t *testing.T) {
+ expectedRepo := "foo/bar"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.WriteHeader(http.StatusNotFound)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ img := remoteImage{
+ fetcher: fetcher{
+ Ref: mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)),
+ Client: http.DefaultClient,
+ context: context.Background(),
+ },
+ }
+
+ if _, err := img.RawManifest(); err == nil {
+ t.Error("RawManifest() = nil; wanted error")
+ }
+}
+
+func TestRawConfigFileNotFound(t *testing.T) {
+ img := randomImage(t)
+ expectedRepo := "foo/bar"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, img))
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case configPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.WriteHeader(http.StatusNotFound)
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawManifest(t, img))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ rmt := remoteImage{
+ fetcher: fetcher{
+ Ref: mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)),
+ Client: http.DefaultClient,
+ context: context.Background(),
+ },
+ }
+
+ if _, err := rmt.RawConfigFile(); err == nil {
+ t.Error("RawConfigFile() = nil; wanted error")
+ }
+}
+
+func TestAcceptHeaders(t *testing.T) {
+ img := randomImage(t)
+ expectedRepo := "foo/bar"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ wantAccept := strings.Join([]string{
+ string(types.DockerManifestSchema2),
+ string(types.OCIManifestSchema1),
+ }, ",")
+ if got, want := r.Header.Get("Accept"), wantAccept; got != want {
+ t.Errorf("Accept header; got %v, want %v", got, want)
+ }
+ w.Write(mustRawManifest(t, img))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ rmt := &remoteImage{
+ fetcher: fetcher{
+ Ref: mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)),
+ Client: http.DefaultClient,
+ context: context.Background(),
+ },
+ }
+ manifest, err := rmt.RawManifest()
+ if err != nil {
+ t.Errorf("RawManifest() = %v", err)
+ }
+ if got, want := manifest, mustRawManifest(t, img); !bytes.Equal(got, want) {
+ t.Errorf("RawManifest() = %v, want %v", got, want)
+ }
+}
+
+func TestImage(t *testing.T) {
+ img := randomImage(t)
+ expectedRepo := "foo/bar"
+ layerDigest := mustManifest(t, img).Layers[0].Digest
+ layerSize := mustManifest(t, img).Layers[0].Size
+ configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, img))
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ layerPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, layerDigest)
+ manifestReqCount := 0
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case configPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawConfigFile(t, img))
+ case manifestPath:
+ manifestReqCount++
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawManifest(t, img))
+ case layerPath:
+ t.Fatalf("BlobSize should not make any request: %v", r.URL.Path)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
+ rmt, err := Image(tag, WithTransport(http.DefaultTransport), WithAuthFromKeychain(authn.DefaultKeychain))
+ if err != nil {
+ t.Errorf("Image() = %v", err)
+ }
+
+ if got, want := mustRawManifest(t, rmt), mustRawManifest(t, img); !bytes.Equal(got, want) {
+ t.Errorf("RawManifest() = %v, want %v", got, want)
+ }
+ if got, want := mustRawConfigFile(t, rmt), mustRawConfigFile(t, img); !bytes.Equal(got, want) {
+ t.Errorf("RawConfigFile() = %v, want %v", got, want)
+ }
+ // Make sure caching the manifest works.
+ if manifestReqCount != 1 {
+ t.Errorf("RawManifest made %v requests, expected 1", manifestReqCount)
+ }
+
+ l, err := rmt.LayerByDigest(layerDigest)
+ if err != nil {
+ t.Errorf("LayerByDigest() = %v", err)
+ }
+ // BlobSize should not HEAD.
+ size, err := l.Size()
+ if err != nil {
+ t.Errorf("BlobSize() = %v", err)
+ }
+ if got, want := size, layerSize; want != got {
+ t.Errorf("BlobSize() = %v want %v", got, want)
+ }
+}
+
+func TestPullingManifestList(t *testing.T) {
+ idx := randomIndex(t)
+ expectedRepo := "foo/bar"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ childDigest := mustIndexManifest(t, idx).Manifests[1].Digest
+ child := mustChild(t, idx, childDigest)
+ childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest)
+ fakePlatformChildDigest := mustIndexManifest(t, idx).Manifests[0].Digest
+ fakePlatformChild := mustChild(t, idx, fakePlatformChildDigest)
+ fakePlatformChildPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, fakePlatformChildDigest)
+ configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, child))
+
+ fakePlatform := v1.Platform{
+ Architecture: "not-real-arch",
+ OS: "not-real-os",
+ }
+
+ // Rewrite the index to make sure the desired platform matches the second child.
+ manifest, err := idx.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Make sure the first manifest doesn't match.
+ manifest.Manifests[0].Platform = &fakePlatform
+ // Make sure the second manifest does.
+ manifest.Manifests[1].Platform = &defaultPlatform
+ // Do short-circuiting via Data.
+ manifest.Manifests[1].Data = mustRawManifest(t, child)
+ rawManifest, err := json.Marshal(manifest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Header().Set("Content-Type", string(mustMediaType(t, idx)))
+ w.Write(rawManifest)
+ case childPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawManifest(t, child))
+ case configPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawConfigFile(t, child))
+ case fakePlatformChildPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawManifest(t, fakePlatformChild))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
+ rmtChild, err := Image(tag)
+ if err != nil {
+ t.Errorf("Image() = %v", err)
+ }
+
+ // Test that child works as expected.
+ if got, want := mustRawManifest(t, rmtChild), mustRawManifest(t, child); !bytes.Equal(got, want) {
+ t.Errorf("RawManifest() = %v, want %v", string(got), string(want))
+ }
+ if got, want := mustRawConfigFile(t, rmtChild), mustRawConfigFile(t, child); !bytes.Equal(got, want) {
+ t.Errorf("RawConfigFile() = %v, want %v", got, want)
+ }
+
+ // Make sure we can roundtrip platform info via Descriptor.
+ img, err := Image(tag, WithPlatform(fakePlatform))
+ if err != nil {
+ t.Fatal(err)
+ }
+ desc, err := partial.Descriptor(img)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := cmp.Diff(*desc.Platform, fakePlatform); diff != "" {
+ t.Errorf("Desciptor() (-want +got) = %v", diff)
+ }
+}
+
+func TestPullingManifestListNoMatch(t *testing.T) {
+ idx := randomIndex(t)
+ expectedRepo := "foo/bar"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ childDigest := mustIndexManifest(t, idx).Manifests[1].Digest
+ child := mustChild(t, idx, childDigest)
+ childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest)
+ configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, child))
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Header().Set("Content-Type", string(mustMediaType(t, idx)))
+ w.Write(mustRawManifest(t, idx))
+ case childPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawManifest(t, child))
+ case configPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawConfigFile(t, child))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ platform := v1.Platform{
+ Architecture: "not-real-arch",
+ OS: "not-real-os",
+ }
+ tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
+ if _, err := Image(tag, WithPlatform(platform)); err == nil {
+ t.Errorf("Image succeeded, wanted err")
+ }
+}
+
+func TestValidate(t *testing.T) {
+ img, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tag, err := name.NewTag(u.Host + "/foo/bar")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Write(tag, img); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err = Image(tag)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Image(img); err != nil {
+ t.Errorf("failed to validate remote.Image: %v", err)
+ }
+}
+
+func TestPullingForeignLayer(t *testing.T) {
+ // For that sweet, sweet coverage in options.
+ var b bytes.Buffer
+ logs.Debug.SetOutput(&b)
+
+ img := randomImage(t)
+ expectedRepo := "foo/bar"
+ foreignPath := "/foreign/path"
+
+ foreignLayer, err := random.Layer(1024, types.DockerForeignLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ foreignServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case foreignPath:
+ compressed, err := foreignLayer.Compressed()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := io.Copy(w, compressed); err != nil {
+ t.Fatal(err)
+ }
+ w.WriteHeader(http.StatusOK)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer foreignServer.Close()
+ fu, err := url.Parse(foreignServer.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", foreignServer.URL, err)
+ }
+
+ img, err = mutate.Append(img, mutate.Addendum{
+ Layer: foreignLayer,
+ URLs: []string{
+ "http://" + path.Join(fu.Host, foreignPath),
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Set up a fake registry that will respond 404 to the foreign layer,
+ // but serve everything else correctly.
+ configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, img))
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ foreignLayerDigest := mustManifest(t, img).Layers[1].Digest
+ foreignLayerPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, foreignLayerDigest)
+ layerDigest := mustManifest(t, img).Layers[0].Digest
+ layerPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, layerDigest)
+
+ layer, err := img.LayerByDigest(layerDigest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case configPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawConfigFile(t, img))
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawManifest(t, img))
+ case layerPath:
+ compressed, err := layer.Compressed()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := io.Copy(w, compressed); err != nil {
+ t.Fatal(err)
+ }
+ w.WriteHeader(http.StatusOK)
+ case foreignLayerPath:
+ // Not here!
+ w.WriteHeader(http.StatusNotFound)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ // Pull from the registry and ensure that everything Validates; i.e. that
+ // we pull the layer from the foreignServer.
+ tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
+ rmt, err := Image(tag, WithTransport(http.DefaultTransport))
+ if err != nil {
+ t.Errorf("Image() = %v", err)
+ }
+
+ if err := validate.Image(rmt); err != nil {
+ t.Errorf("failed to validate foreign image: %v", err)
+ }
+
+ // Set up a fake registry and write what we pulled to it.
+ // This ensures we get coverage for the remoteLayer.MediaType path.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err = url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/foreign/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Write(ref, rmt); err != nil {
+ t.Errorf("failed to Write: %v", err)
+ }
+}
+
+func TestData(t *testing.T) {
+ img := randomImage(t)
+ manifest, err := img.Manifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ cb, err := img.RawConfigFile()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ manifest.Config.Data = cb
+ rc, err := layers[0].Compressed()
+ if err != nil {
+ t.Fatal(err)
+ }
+ lb, err := io.ReadAll(rc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ manifest.Layers[0].Data = lb
+ rawManifest, err := json.Marshal(manifest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case "/v2/test/manifests/latest":
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(rawManifest)
+ default:
+ // explode if we try to read blob or config
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ ref, err := newReference(u.Host, "test", "latest")
+ if err != nil {
+ t.Fatal(err)
+ }
+ rmt, err := Image(ref)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := validate.Image(rmt); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/pkg/v1/remote/index.go b/pkg/v1/remote/index.go
new file mode 100644
index 0000000..0939947
--- /dev/null
+++ b/pkg/v1/remote/index.go
@@ -0,0 +1,319 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "bytes"
+ "fmt"
+ "sync"
+
+ "github.com/google/go-containerregistry/internal/verify"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+var acceptableIndexMediaTypes = []types.MediaType{
+ types.DockerManifestList,
+ types.OCIImageIndex,
+}
+
+// remoteIndex accesses an index from a remote registry
+type remoteIndex struct {
+ fetcher
+ manifestLock sync.Mutex // Protects manifest
+ manifest []byte
+ mediaType types.MediaType
+ descriptor *v1.Descriptor
+}
+
+// Index provides access to a remote index reference.
+func Index(ref name.Reference, options ...Option) (v1.ImageIndex, error) {
+ desc, err := get(ref, acceptableIndexMediaTypes, options...)
+ if err != nil {
+ return nil, err
+ }
+
+ return desc.ImageIndex()
+}
+
+func (r *remoteIndex) MediaType() (types.MediaType, error) {
+ if string(r.mediaType) != "" {
+ return r.mediaType, nil
+ }
+ return types.DockerManifestList, nil
+}
+
+func (r *remoteIndex) Digest() (v1.Hash, error) {
+ return partial.Digest(r)
+}
+
+func (r *remoteIndex) Size() (int64, error) {
+ return partial.Size(r)
+}
+
+func (r *remoteIndex) RawManifest() ([]byte, error) {
+ r.manifestLock.Lock()
+ defer r.manifestLock.Unlock()
+ if r.manifest != nil {
+ return r.manifest, nil
+ }
+
+ // NOTE(jonjohnsonjr): We should never get here because the public entrypoints
+ // do type-checking via remote.Descriptor. I've left this here for tests that
+ // directly instantiate a remoteIndex.
+ manifest, desc, err := r.fetchManifest(r.Ref, acceptableIndexMediaTypes)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.descriptor == nil {
+ r.descriptor = desc
+ }
+ r.mediaType = desc.MediaType
+ r.manifest = manifest
+ return r.manifest, nil
+}
+
+func (r *remoteIndex) IndexManifest() (*v1.IndexManifest, error) {
+ b, err := r.RawManifest()
+ if err != nil {
+ return nil, err
+ }
+ return v1.ParseIndexManifest(bytes.NewReader(b))
+}
+
+func (r *remoteIndex) Image(h v1.Hash) (v1.Image, error) {
+ desc, err := r.childByHash(h)
+ if err != nil {
+ return nil, err
+ }
+
+ // Descriptor.Image will handle coercing nested indexes into an Image.
+ return desc.Image()
+}
+
+// Descriptor retains the original descriptor from an index manifest.
+// See partial.Descriptor.
+func (r *remoteIndex) Descriptor() (*v1.Descriptor, error) {
+ // kind of a hack, but RawManifest does appropriate locking/memoization
+ // and makes sure r.descriptor is populated.
+ _, err := r.RawManifest()
+ return r.descriptor, err
+}
+
+func (r *remoteIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
+ desc, err := r.childByHash(h)
+ if err != nil {
+ return nil, err
+ }
+ return desc.ImageIndex()
+}
+
+// Workaround for #819.
+func (r *remoteIndex) Layer(h v1.Hash) (v1.Layer, error) {
+ index, err := r.IndexManifest()
+ if err != nil {
+ return nil, err
+ }
+ for _, childDesc := range index.Manifests {
+ if h == childDesc.Digest {
+ l, err := partial.CompressedToLayer(&remoteLayer{
+ fetcher: r.fetcher,
+ digest: h,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &MountableLayer{
+ Layer: l,
+ Reference: r.Ref.Context().Digest(h.String()),
+ }, nil
+ }
+ }
+ return nil, fmt.Errorf("layer not found: %s", h)
+}
+
+// Experiment with a better API for v1.ImageIndex. We might want to move this
+// to partial?
+func (r *remoteIndex) Manifests() ([]partial.Describable, error) {
+ m, err := r.IndexManifest()
+ if err != nil {
+ return nil, err
+ }
+ manifests := []partial.Describable{}
+ for _, desc := range m.Manifests {
+ switch {
+ case desc.MediaType.IsImage():
+ img, err := r.Image(desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+ manifests = append(manifests, img)
+ case desc.MediaType.IsIndex():
+ idx, err := r.ImageIndex(desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+ manifests = append(manifests, idx)
+ default:
+ layer, err := r.Layer(desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+ manifests = append(manifests, layer)
+ }
+ }
+
+ return manifests, nil
+}
+
+func (r *remoteIndex) imageByPlatform(platform v1.Platform) (v1.Image, error) {
+ desc, err := r.childByPlatform(platform)
+ if err != nil {
+ return nil, err
+ }
+
+ // Descriptor.Image will handle coercing nested indexes into an Image.
+ return desc.Image()
+}
+
+// This naively matches the first manifest with matching platform attributes.
+//
+// We should probably use this instead:
+//
+// github.com/containerd/containerd/platforms
+//
+// But first we'd need to migrate to:
+//
+// github.com/opencontainers/image-spec/specs-go/v1
+func (r *remoteIndex) childByPlatform(platform v1.Platform) (*Descriptor, error) {
+ index, err := r.IndexManifest()
+ if err != nil {
+ return nil, err
+ }
+ for _, childDesc := range index.Manifests {
+ // If platform is missing from child descriptor, assume it's amd64/linux.
+ p := defaultPlatform
+ if childDesc.Platform != nil {
+ p = *childDesc.Platform
+ }
+
+ if matchesPlatform(p, platform) {
+ return r.childDescriptor(childDesc, platform)
+ }
+ }
+ return nil, fmt.Errorf("no child with platform %+v in index %s", platform, r.Ref)
+}
+
+func (r *remoteIndex) childByHash(h v1.Hash) (*Descriptor, error) {
+ index, err := r.IndexManifest()
+ if err != nil {
+ return nil, err
+ }
+ for _, childDesc := range index.Manifests {
+ if h == childDesc.Digest {
+ return r.childDescriptor(childDesc, defaultPlatform)
+ }
+ }
+ return nil, fmt.Errorf("no child with digest %s in index %s", h, r.Ref)
+}
+
+// Convert one of this index's child's v1.Descriptor into a remote.Descriptor, with the given platform option.
+func (r *remoteIndex) childDescriptor(child v1.Descriptor, platform v1.Platform) (*Descriptor, error) {
+ ref := r.Ref.Context().Digest(child.Digest.String())
+ var (
+ manifest []byte
+ err error
+ )
+ if child.Data != nil {
+ if err := verify.Descriptor(child); err != nil {
+ return nil, err
+ }
+ manifest = child.Data
+ } else {
+ manifest, _, err = r.fetchManifest(ref, []types.MediaType{child.MediaType})
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if child.MediaType.IsImage() {
+ mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
+ // Failing to parse as a manifest should just be ignored.
+ // The manifest might not be valid, and that's okay.
+ if mf != nil && !mf.Config.MediaType.IsConfig() {
+ child.ArtifactType = string(mf.Config.MediaType)
+ }
+ }
+
+ return &Descriptor{
+ fetcher: fetcher{
+ Ref: ref,
+ Client: r.Client,
+ context: r.context,
+ },
+ Manifest: manifest,
+ Descriptor: child,
+ platform: platform,
+ }, nil
+}
+
+// matchesPlatform checks if the given platform matches the required platforms.
+// The given platform matches the required platform if
+// - architecture and OS are identical.
+// - OS version and variant are identical if provided.
+// - features and OS features of the required platform are subsets of those of the given platform.
+func matchesPlatform(given, required v1.Platform) bool {
+ // Required fields that must be identical.
+ if given.Architecture != required.Architecture || given.OS != required.OS {
+ return false
+ }
+
+ // Optional fields that may be empty, but must be identical if provided.
+ if required.OSVersion != "" && given.OSVersion != required.OSVersion {
+ return false
+ }
+ if required.Variant != "" && given.Variant != required.Variant {
+ return false
+ }
+
+ // Verify required platform's features are a subset of given platform's features.
+ if !isSubset(given.OSFeatures, required.OSFeatures) {
+ return false
+ }
+ if !isSubset(given.Features, required.Features) {
+ return false
+ }
+
+ return true
+}
+
+// isSubset checks if the required array of strings is a subset of the given lst.
+func isSubset(lst, required []string) bool {
+ set := make(map[string]bool)
+ for _, value := range lst {
+ set[value] = true
+ }
+
+ for _, value := range required {
+ if _, ok := set[value]; !ok {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/pkg/v1/remote/index_test.go b/pkg/v1/remote/index_test.go
new file mode 100644
index 0000000..4399b16
--- /dev/null
+++ b/pkg/v1/remote/index_test.go
@@ -0,0 +1,504 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func randomIndex(t *testing.T) v1.ImageIndex {
+ rnd, err := random.Index(1024, 1, 3)
+ if err != nil {
+ t.Fatalf("random.Index() = %v", err)
+ }
+ return rnd
+}
+
+func mustIndexManifest(t *testing.T, idx v1.ImageIndex) *v1.IndexManifest {
+ m, err := idx.IndexManifest()
+ if err != nil {
+ t.Fatalf("IndexManifest() = %v", err)
+ }
+ return m
+}
+
+func mustChild(t *testing.T, idx v1.ImageIndex, h v1.Hash) v1.Image {
+ img, err := idx.Image(h)
+ if err != nil {
+ t.Fatalf("Image(%s) = %v", h, err)
+ }
+ return img
+}
+
+func mustMediaType(t *testing.T, tag withMediaType) types.MediaType {
+ mt, err := tag.MediaType()
+ if err != nil {
+ t.Fatalf("MediaType() = %v", err)
+ }
+ return mt
+}
+
+func mustHash(t *testing.T, s string) v1.Hash {
+ h, err := v1.NewHash(s)
+ if err != nil {
+ t.Fatalf("NewHash() = %v", err)
+ }
+ return h
+}
+
+func TestIndexRawManifestDigests(t *testing.T) {
+ idx := randomIndex(t)
+ expectedRepo := "foo/bar"
+
+ cases := []struct {
+ name string
+ ref string
+ responseBody []byte
+ contentDigest string
+ wantErr bool
+ }{{
+ name: "normal pull, by tag",
+ ref: "latest",
+ responseBody: mustRawManifest(t, idx),
+ contentDigest: mustDigest(t, idx).String(),
+ wantErr: false,
+ }, {
+ name: "normal pull, by digest",
+ ref: mustDigest(t, idx).String(),
+ responseBody: mustRawManifest(t, idx),
+ contentDigest: mustDigest(t, idx).String(),
+ wantErr: false,
+ }, {
+ name: "right content-digest, wrong body, by digest",
+ ref: mustDigest(t, idx).String(),
+ responseBody: []byte("not even json"),
+ contentDigest: mustDigest(t, idx).String(),
+ wantErr: true,
+ }, {
+ name: "right body, wrong content-digest, by tag",
+ ref: "latest",
+ responseBody: mustRawManifest(t, idx),
+ contentDigest: bogusDigest,
+ wantErr: false,
+ }, {
+ // NB: This succeeds! We don't care what the registry thinks.
+ name: "right body, wrong content-digest, by digest",
+ ref: mustDigest(t, idx).String(),
+ responseBody: mustRawManifest(t, idx),
+ contentDigest: bogusDigest,
+ wantErr: false,
+ }}
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, tc.ref)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case manifestPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+
+ w.Header().Set("Docker-Content-Digest", tc.contentDigest)
+ w.Write(tc.responseBody)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ ref, err := newReference(u.Host, expectedRepo, tc.ref)
+ if err != nil {
+ t.Fatalf("url.Parse(%v, %v, %v) = %v", u.Host, expectedRepo, tc.ref, err)
+ }
+
+ rmt := remoteIndex{
+ fetcher: fetcher{
+ Ref: ref,
+ Client: http.DefaultClient,
+ context: context.Background(),
+ },
+ }
+
+ if _, err := rmt.RawManifest(); (err != nil) != tc.wantErr {
+ t.Errorf("RawManifest() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err)
+ }
+ })
+ }
+}
+
+func TestIndex(t *testing.T) {
+ idx := randomIndex(t)
+ expectedRepo := "foo/bar"
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ childDigest := mustIndexManifest(t, idx).Manifests[0].Digest
+ child := mustChild(t, idx, childDigest)
+ childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest)
+ configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, child))
+ manifestReqCount := 0
+ childReqCount := 0
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case manifestPath:
+ manifestReqCount++
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Header().Set("Content-Type", string(mustMediaType(t, idx)))
+ w.Write(mustRawManifest(t, idx))
+ case childPath:
+ childReqCount++
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawManifest(t, child))
+ case configPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+ w.Write(mustRawConfigFile(t, child))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
+ rmt, err := Index(tag, WithTransport(http.DefaultTransport))
+ if err != nil {
+ t.Errorf("Index() = %v", err)
+ }
+ rmtChild, err := rmt.Image(childDigest)
+ if err != nil {
+ t.Errorf("remoteIndex.Image(%s) = %v", childDigest, err)
+ }
+
+ // Test that index works as expected.
+ if got, want := mustRawManifest(t, rmt), mustRawManifest(t, idx); !bytes.Equal(got, want) {
+ t.Errorf("RawManifest() = %v, want %v", got, want)
+ }
+ if diff := cmp.Diff(mustIndexManifest(t, idx), mustIndexManifest(t, rmt)); diff != "" {
+ t.Errorf("IndexManifest() (-want +got) = %v", diff)
+ }
+ if got, want := mustMediaType(t, rmt), mustMediaType(t, idx); got != want {
+ t.Errorf("MediaType() = %v, want %v", got, want)
+ }
+ if got, want := mustDigest(t, rmt), mustDigest(t, idx); got != want {
+ t.Errorf("Digest() = %v, want %v", got, want)
+ }
+ // Make sure caching the manifest works for index.
+ if manifestReqCount != 1 {
+ t.Errorf("RawManifest made %v requests, expected 1", manifestReqCount)
+ }
+
+ // Test that child works as expected.
+ if got, want := mustRawManifest(t, rmtChild), mustRawManifest(t, child); !bytes.Equal(got, want) {
+ t.Errorf("RawManifest() = %v, want %v", got, want)
+ }
+ if got, want := mustRawConfigFile(t, rmtChild), mustRawConfigFile(t, child); !bytes.Equal(got, want) {
+ t.Errorf("RawConfigFile() = %v, want %v", got, want)
+ }
+ // Make sure caching the manifest works for child.
+ if childReqCount != 1 {
+ t.Errorf("RawManifest made %v requests, expected 1", childReqCount)
+ }
+
+ // Try to fetch bogus children.
+ bogusHash := mustHash(t, bogusDigest)
+
+ if _, err := rmt.Image(bogusHash); err == nil {
+ t.Errorf("remoteIndex.Image(bogusDigest) err = %v, wanted err", err)
+ }
+ if _, err := rmt.ImageIndex(bogusHash); err == nil {
+ t.Errorf("remoteIndex.ImageIndex(bogusDigest) err = %v, wanted err", err)
+ }
+}
+
+// TestMatchesPlatform runs test cases on the matchesPlatform function which verifies
+// whether the given platform can run on the required platform by checking the
+// compatibility of architecture, OS, OS version, OS features, variant and features.
+func TestMatchesPlatform(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ // want is the expected return value from matchesPlatform
+ // when the given platform is 'given' and the required platform is 'required'.
+ given v1.Platform
+ required v1.Platform
+ want bool
+ }{{ // The given & required platforms are identical. matchesPlatform expected to return true.
+ given: v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win32k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ required: v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win32k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ want: true,
+ },
+ { // OS and Architecture must exactly match. matchesPlatform expected to return false.
+ given: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ required: v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win32k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ want: false,
+ },
+ { // OS version must exactly match
+ given: v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ required: v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+ OSVersion: "10.0.10587",
+ OSFeatures: []string{"win64k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ want: false,
+ },
+ { // OS Features must exactly match. matchesPlatform expected to return false.
+ given: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ required: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win32k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ want: false,
+ },
+ { // Variant must exactly match. matchesPlatform expected to return false.
+ given: v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ required: v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "armv7l",
+ Features: []string{"sse4"},
+ },
+ want: false,
+ },
+ { // OS must exactly match, and is case sensative. matchesPlatform expected to return false.
+ given: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ required: v1.Platform{
+ Architecture: "arm",
+ OS: "LinuX",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ want: false,
+ },
+ { // OSVersion and Variant are specified in given but not in required.
+ // matchesPlatform expected to return true.
+ given: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ required: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "",
+ OSFeatures: []string{"win64k"},
+ Variant: "",
+ Features: []string{"sse4"},
+ },
+ want: true,
+ },
+ { // Ensure the optional field OSVersion & Variant match exactly if specified as required.
+ given: v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+ OSVersion: "",
+ OSFeatures: []string{},
+ Variant: "",
+ Features: []string{},
+ },
+ required: v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win32k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ want: false,
+ },
+ { // Checking subset validity when required less features than given features.
+ // matchesPlatform expected to return true.
+ given: v1.Platform{
+ Architecture: "",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win32k"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ required: v1.Platform{
+ Architecture: "",
+ OS: "linux",
+ OSVersion: "",
+ OSFeatures: []string{},
+ Variant: "",
+ Features: []string{},
+ },
+ want: true,
+ },
+ { // Checking subset validity when required features are subset of given features.
+ // matchesPlatform expected to return true.
+ given: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k", "f1", "f2"},
+ Variant: "",
+ Features: []string{"sse4", "f1"},
+ },
+ required: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "",
+ Features: []string{"sse4"},
+ },
+ want: true,
+ },
+ { // Checking subset validity when some required features is not subset of given features.
+ // matchesPlatform expected to return false.
+ given: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k", "f1", "f2"},
+ Variant: "",
+ Features: []string{"sse4", "f1"},
+ },
+ required: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k"},
+ Variant: "",
+ Features: []string{"sse4", "f2"},
+ },
+ want: false,
+ },
+ { // Checking subset validity when OS features not required,
+ // and required features is indeed a subset of given features.
+ // matchesPlatform expected to return true.
+ given: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{"win64k", "f1", "f2"},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ required: v1.Platform{
+ Architecture: "arm",
+ OS: "linux",
+ OSVersion: "10.0.10586",
+ OSFeatures: []string{},
+ Variant: "armv6l",
+ Features: []string{"sse4"},
+ },
+ want: true,
+ },
+ }
+
+ for _, test := range tests {
+ got := matchesPlatform(test.given, test.required)
+ if got != test.want {
+ t.Errorf("matchesPlatform(%v, %v); got %v, want %v", test.given, test.required, got, test.want)
+ }
+ }
+}
diff --git a/pkg/v1/remote/layer.go b/pkg/v1/remote/layer.go
new file mode 100644
index 0000000..b2126f5
--- /dev/null
+++ b/pkg/v1/remote/layer.go
@@ -0,0 +1,94 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "io"
+
+ "github.com/google/go-containerregistry/internal/redact"
+ "github.com/google/go-containerregistry/internal/verify"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// remoteImagelayer implements partial.CompressedLayer
+type remoteLayer struct {
+ fetcher
+ digest v1.Hash
+}
+
+// Compressed implements partial.CompressedLayer
+func (rl *remoteLayer) Compressed() (io.ReadCloser, error) {
+ // We don't want to log binary layers -- this can break terminals.
+ ctx := redact.NewContext(rl.context, "omitting binary blobs from logs")
+ return rl.fetchBlob(ctx, verify.SizeUnknown, rl.digest)
+}
+
+// Compressed implements partial.CompressedLayer
+func (rl *remoteLayer) Size() (int64, error) {
+ resp, err := rl.headBlob(rl.digest)
+ if err != nil {
+ return -1, err
+ }
+ defer resp.Body.Close()
+ return resp.ContentLength, nil
+}
+
+// Digest implements partial.CompressedLayer
+func (rl *remoteLayer) Digest() (v1.Hash, error) {
+ return rl.digest, nil
+}
+
+// MediaType implements v1.Layer
+func (rl *remoteLayer) MediaType() (types.MediaType, error) {
+ return types.DockerLayer, nil
+}
+
+// See partial.Exists.
+func (rl *remoteLayer) Exists() (bool, error) {
+ return rl.blobExists(rl.digest)
+}
+
+// Layer reads the given blob reference from a registry as a Layer. A blob
+// reference here is just a punned name.Digest where the digest portion is the
+// digest of the blob to be read and the repository portion is the repo where
+// that blob lives.
+func Layer(ref name.Digest, options ...Option) (v1.Layer, error) {
+ o, err := makeOptions(ref.Context(), options...)
+ if err != nil {
+ return nil, err
+ }
+ f, err := makeFetcher(ref, o)
+ if err != nil {
+ return nil, err
+ }
+ h, err := v1.NewHash(ref.Identifier())
+ if err != nil {
+ return nil, err
+ }
+ l, err := partial.CompressedToLayer(&remoteLayer{
+ fetcher: *f,
+ digest: h,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &MountableLayer{
+ Layer: l,
+ Reference: ref,
+ }, nil
+}
diff --git a/pkg/v1/remote/layer_test.go b/pkg/v1/remote/layer_test.go
new file mode 100644
index 0000000..a2f56bd
--- /dev/null
+++ b/pkg/v1/remote/layer_test.go
@@ -0,0 +1,148 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "fmt"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/compare"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestRemoteLayer(t *testing.T) {
+ layer, err := random.Layer(1024, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest, err := layer.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Set up a fake registry and write what we pulled to it.
+ // This ensures we get coverage for the remoteLayer.MediaType path.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ t.Log(s.URL)
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(u)
+ dst := fmt.Sprintf("%s/some/path@%s", u.Host, digest)
+ t.Log(dst)
+ ref, err := name.NewDigest(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Log(ref)
+ if err := WriteLayer(ref.Context(), layer); err != nil {
+ t.Fatalf("failed to WriteLayer: %v", err)
+ }
+
+ got, err := Layer(ref)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := got.MediaType(); err != nil {
+ t.Errorf("reading MediaType: %v", err)
+ }
+
+ if err := compare.Layers(got, layer); err != nil {
+ t.Errorf("compare.Layers: %v", err)
+ }
+ if err := validate.Layer(got); err != nil {
+ t.Errorf("validate.Layer: %v", err)
+ }
+
+ if ok, err := partial.Exists(got); err != nil {
+ t.Fatal(err)
+ } else if got, want := ok, true; got != want {
+ t.Errorf("Exists() = %t != %t", got, want)
+ }
+}
+
+func TestRemoteLayerDescriptor(t *testing.T) {
+ layer, err := random.Layer(1024, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ image, err := mutate.Append(empty.Image, mutate.Addendum{
+ Layer: layer,
+ URLs: []string{"example.com"},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Set up a fake registry and write what we pulled to it.
+ // This ensures we get coverage for the remoteLayer.MediaType path.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dst := fmt.Sprintf("%s/some/path:tag", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Write(ref, image); err != nil {
+ t.Fatalf("failed to WriteLayer: %v", err)
+ }
+
+ pulled, err := Image(ref)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ layers, err := pulled.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ desc, err := partial.Descriptor(layers[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(desc.URLs) != 1 {
+ t.Fatalf("expected url for layer[0]")
+ }
+
+ if got, want := desc.URLs[0], "example.com"; got != want {
+ t.Errorf("layer[0].urls[0] = %s != %s", got, want)
+ }
+ if ok, err := partial.Exists(layers[0]); err != nil {
+ t.Fatal(err)
+ } else if got, want := ok, true; got != want {
+ t.Errorf("Exists() = %t != %t", got, want)
+ }
+}
diff --git a/pkg/v1/remote/list.go b/pkg/v1/remote/list.go
new file mode 100644
index 0000000..e643c49
--- /dev/null
+++ b/pkg/v1/remote/list.go
@@ -0,0 +1,141 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+)
+
+type tags struct {
+ Name string `json:"name"`
+ Tags []string `json:"tags"`
+}
+
+// ListWithContext calls List with the given context.
+//
+// Deprecated: Use List and WithContext. This will be removed in a future release.
+func ListWithContext(ctx context.Context, repo name.Repository, options ...Option) ([]string, error) {
+ return List(repo, append(options, WithContext(ctx))...)
+}
+
+// List calls /tags/list for the given repository, returning the list of tags
+// in the "tags" property.
+func List(repo name.Repository, options ...Option) ([]string, error) {
+ o, err := makeOptions(repo, options...)
+ if err != nil {
+ return nil, err
+ }
+ scopes := []string{repo.Scope(transport.PullScope)}
+ tr, err := transport.NewWithContext(o.context, repo.Registry, o.auth, o.transport, scopes)
+ if err != nil {
+ return nil, err
+ }
+
+ uri := &url.URL{
+ Scheme: repo.Registry.Scheme(),
+ Host: repo.Registry.RegistryStr(),
+ Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()),
+ }
+
+ if o.pageSize > 0 {
+ uri.RawQuery = fmt.Sprintf("n=%d", o.pageSize)
+ }
+
+ client := http.Client{Transport: tr}
+ tagList := []string{}
+ parsed := tags{}
+
+ // get responses until there is no next page
+ for {
+ select {
+ case <-o.context.Done():
+ return nil, o.context.Err()
+ default:
+ }
+
+ req, err := http.NewRequestWithContext(o.context, "GET", uri.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ return nil, err
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
+ return nil, err
+ }
+
+ if err := resp.Body.Close(); err != nil {
+ return nil, err
+ }
+
+ tagList = append(tagList, parsed.Tags...)
+
+ uri, err = getNextPageURL(resp)
+ if err != nil {
+ return nil, err
+ }
+ // no next page
+ if uri == nil {
+ break
+ }
+ }
+
+ return tagList, nil
+}
+
+// getNextPageURL checks if there is a Link header in a http.Response which
+// contains a link to the next page. If yes it returns the url.URL of the next
+// page otherwise it returns nil.
+func getNextPageURL(resp *http.Response) (*url.URL, error) {
+ link := resp.Header.Get("Link")
+ if link == "" {
+ return nil, nil
+ }
+
+ if link[0] != '<' {
+ return nil, fmt.Errorf("failed to parse link header: missing '<' in: %s", link)
+ }
+
+ end := strings.Index(link, ">")
+ if end == -1 {
+ return nil, fmt.Errorf("failed to parse link header: missing '>' in: %s", link)
+ }
+ link = link[1:end]
+
+ linkURL, err := url.Parse(link)
+ if err != nil {
+ return nil, err
+ }
+ if resp.Request == nil || resp.Request.URL == nil {
+ return nil, nil
+ }
+ linkURL = resp.Request.URL.ResolveReference(linkURL)
+ return linkURL, nil
+}
diff --git a/pkg/v1/remote/list_test.go b/pkg/v1/remote/list_test.go
new file mode 100644
index 0000000..89700b8
--- /dev/null
+++ b/pkg/v1/remote/list_test.go
@@ -0,0 +1,159 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+func TestList(t *testing.T) {
+ cases := []struct {
+ name string
+ responseBody []byte
+ wantErr bool
+ wantTags []string
+ }{{
+ name: "success",
+ responseBody: []byte(`{"tags":["foo","bar"]}`),
+ wantErr: false,
+ wantTags: []string{"foo", "bar"},
+ }, {
+ name: "not json",
+ responseBody: []byte("notjson"),
+ wantErr: true,
+ }}
+
+ repoName := "ubuntu"
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ tagsPath := fmt.Sprintf("/v2/%s/tags/list", repoName)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case tagsPath:
+ if r.Method != http.MethodGet {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
+ }
+
+ w.Write(tc.responseBody)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewRepository(%v) = %v", repoName, err)
+ }
+
+ tags, err := List(repo)
+ if (err != nil) != tc.wantErr {
+ t.Errorf("List() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err)
+ }
+
+ if diff := cmp.Diff(tc.wantTags, tags); diff != "" {
+ t.Errorf("List() wrong tags (-want +got) = %s", diff)
+ }
+ })
+ }
+}
+
+func TestCancelledList(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+
+ repoName := "doesnotmatter"
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+
+ repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewRepository(%v) = %v", repoName, err)
+ }
+
+ _, err = ListWithContext(ctx, repo)
+ if err == nil || !strings.Contains(err.Error(), "context canceled") {
+ t.Errorf(`unexpected error; want "context canceled", got %v`, err)
+ }
+}
+
+func makeResp(hdr string) *http.Response {
+ return &http.Response{
+ Header: http.Header{
+ "Link": []string{hdr},
+ },
+ }
+}
+
+func TestGetNextPageURL(t *testing.T) {
+ for _, hdr := range []string{
+ "",
+ "<",
+ "><",
+ "<>",
+ fmt.Sprintf("<%c>", 0x7f), // makes url.Parse fail
+ } {
+ u, err := getNextPageURL(makeResp(hdr))
+ if err == nil && u != nil {
+ t.Errorf("Expected err, got %+v", u)
+ }
+ }
+
+ good := &http.Response{
+ Header: http.Header{
+ "Link": []string{"<example.com>"},
+ },
+ Request: &http.Request{
+ URL: &url.URL{
+ Scheme: "https",
+ },
+ },
+ }
+ u, err := getNextPageURL(good)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if u.Scheme != "https" {
+ t.Errorf("expected scheme to match request, got %s", u.Scheme)
+ }
+}
diff --git a/pkg/v1/remote/mount.go b/pkg/v1/remote/mount.go
new file mode 100644
index 0000000..36d0885
--- /dev/null
+++ b/pkg/v1/remote/mount.go
@@ -0,0 +1,108 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+)
+
+// MountableLayer wraps a v1.Layer in a shim that enables the layer to be
+// "mounted" when published to another registry.
+type MountableLayer struct {
+ v1.Layer
+
+ Reference name.Reference
+}
+
+// Descriptor retains the original descriptor from an image manifest.
+// See partial.Descriptor.
+func (ml *MountableLayer) Descriptor() (*v1.Descriptor, error) {
+ return partial.Descriptor(ml.Layer)
+}
+
+// Exists is a hack. See partial.Exists.
+func (ml *MountableLayer) Exists() (bool, error) {
+ return partial.Exists(ml.Layer)
+}
+
+// mountableImage wraps the v1.Layer references returned by the embedded v1.Image
+// in MountableLayer's so that remote.Write might attempt to mount them from their
+// source repository.
+type mountableImage struct {
+ v1.Image
+
+ Reference name.Reference
+}
+
+// Layers implements v1.Image
+func (mi *mountableImage) Layers() ([]v1.Layer, error) {
+ ls, err := mi.Image.Layers()
+ if err != nil {
+ return nil, err
+ }
+ mls := make([]v1.Layer, 0, len(ls))
+ for _, l := range ls {
+ mls = append(mls, &MountableLayer{
+ Layer: l,
+ Reference: mi.Reference,
+ })
+ }
+ return mls, nil
+}
+
+// LayerByDigest implements v1.Image
+func (mi *mountableImage) LayerByDigest(d v1.Hash) (v1.Layer, error) {
+ l, err := mi.Image.LayerByDigest(d)
+ if err != nil {
+ return nil, err
+ }
+ return &MountableLayer{
+ Layer: l,
+ Reference: mi.Reference,
+ }, nil
+}
+
+// LayerByDiffID implements v1.Image
+func (mi *mountableImage) LayerByDiffID(d v1.Hash) (v1.Layer, error) {
+ l, err := mi.Image.LayerByDiffID(d)
+ if err != nil {
+ return nil, err
+ }
+ return &MountableLayer{
+ Layer: l,
+ Reference: mi.Reference,
+ }, nil
+}
+
+// Descriptor retains the original descriptor from an index manifest.
+// See partial.Descriptor.
+func (mi *mountableImage) Descriptor() (*v1.Descriptor, error) {
+ return partial.Descriptor(mi.Image)
+}
+
+// ConfigLayer retains the original reference so that it can be mounted.
+// See partial.ConfigLayer.
+func (mi *mountableImage) ConfigLayer() (v1.Layer, error) {
+ l, err := partial.ConfigLayer(mi.Image)
+ if err != nil {
+ return nil, err
+ }
+ return &MountableLayer{
+ Layer: l,
+ Reference: mi.Reference,
+ }, nil
+}
diff --git a/pkg/v1/remote/mount_test.go b/pkg/v1/remote/mount_test.go
new file mode 100644
index 0000000..ad9b8f6
--- /dev/null
+++ b/pkg/v1/remote/mount_test.go
@@ -0,0 +1,55 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestMountableImage(t *testing.T) {
+ img, err := random.Image(1024, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ref, err := name.ParseReference("ubuntu")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ img = &mountableImage{
+ Image: img,
+ Reference: ref,
+ }
+
+ if err := validate.Image(img); err != nil {
+ t.Errorf("Validate() = %v", err)
+ }
+
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i, l := range layers {
+ if _, ok := l.(*MountableLayer); !ok {
+ t.Errorf("layers[%d] should be MountableLayer but isn't", i)
+ }
+ }
+}
diff --git a/pkg/v1/remote/multi_write.go b/pkg/v1/remote/multi_write.go
new file mode 100644
index 0000000..7f32413
--- /dev/null
+++ b/pkg/v1/remote/multi_write.go
@@ -0,0 +1,302 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "golang.org/x/sync/errgroup"
+)
+
+// MultiWrite writes the given Images or ImageIndexes to the given refs, as
+// efficiently as possible, by deduping shared layer blobs and uploading layers
+// in parallel, then uploading all manifests in parallel.
+//
+// Current limitations:
+// - All refs must share the same repository.
+// - Images cannot consist of stream.Layers.
+func MultiWrite(m map[name.Reference]Taggable, options ...Option) (rerr error) {
+ // Determine the repository being pushed to; if asked to push to
+ // multiple repositories, give up.
+ var repo, zero name.Repository
+ for ref := range m {
+ if repo == zero {
+ repo = ref.Context()
+ } else if ref.Context() != repo {
+ return fmt.Errorf("MultiWrite can only push to the same repository (saw %q and %q)", repo, ref.Context())
+ }
+ }
+
+ o, err := makeOptions(repo, options...)
+ if err != nil {
+ return err
+ }
+
+ // Collect unique blobs (layers and config blobs).
+ blobs := map[v1.Hash]v1.Layer{}
+ newManifests := []map[name.Reference]Taggable{}
+ // Separate originally requested images and indexes, so we can push images first.
+ images, indexes := map[name.Reference]Taggable{}, map[name.Reference]Taggable{}
+ for ref, i := range m {
+ if img, ok := i.(v1.Image); ok {
+ images[ref] = i
+ if err := addImageBlobs(img, blobs, o.allowNondistributableArtifacts); err != nil {
+ return err
+ }
+ continue
+ }
+ if idx, ok := i.(v1.ImageIndex); ok {
+ indexes[ref] = i
+ newManifests, err = addIndexBlobs(idx, blobs, repo, newManifests, 0, o.allowNondistributableArtifacts)
+ if err != nil {
+ return err
+ }
+ continue
+ }
+ return fmt.Errorf("pushable resource was not Image or ImageIndex: %T", i)
+ }
+
+ // Determine if any of the layers are Mountable, because if so we need
+ // to request Pull scope too.
+ ls := []v1.Layer{}
+ for _, l := range blobs {
+ ls = append(ls, l)
+ }
+ scopes := scopesForUploadingImage(repo, ls)
+ tr, err := transport.NewWithContext(o.context, repo.Registry, o.auth, o.transport, scopes)
+ if err != nil {
+ return err
+ }
+ w := writer{
+ repo: repo,
+ client: &http.Client{Transport: tr},
+ backoff: o.retryBackoff,
+ predicate: o.retryPredicate,
+ }
+
+ // Collect the total size of blobs and manifests we're about to write.
+ if o.updates != nil {
+ w.progress = &progress{updates: o.updates}
+ w.progress.lastUpdate = &v1.Update{}
+ defer close(o.updates)
+ defer func() { _ = w.progress.err(rerr) }()
+ for _, b := range blobs {
+ size, err := b.Size()
+ if err != nil {
+ return err
+ }
+ w.progress.total(size)
+ }
+ countManifest := func(t Taggable) error {
+ b, err := t.RawManifest()
+ if err != nil {
+ return err
+ }
+ w.progress.total(int64(len(b)))
+ return nil
+ }
+ for _, i := range images {
+ if err := countManifest(i); err != nil {
+ return err
+ }
+ }
+ for _, nm := range newManifests {
+ for _, i := range nm {
+ if err := countManifest(i); err != nil {
+ return err
+ }
+ }
+ }
+ for _, i := range indexes {
+ if err := countManifest(i); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Upload individual blobs and collect any errors.
+ blobChan := make(chan v1.Layer, 2*o.jobs)
+ ctx := o.context
+ g, gctx := errgroup.WithContext(o.context)
+ for i := 0; i < o.jobs; i++ {
+ // Start N workers consuming blobs to upload.
+ g.Go(func() error {
+ for b := range blobChan {
+ if err := w.uploadOne(gctx, b); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ }
+ g.Go(func() error {
+ defer close(blobChan)
+ for _, b := range blobs {
+ select {
+ case blobChan <- b:
+ case <-gctx.Done():
+ return gctx.Err()
+ }
+ }
+ return nil
+ })
+ if err := g.Wait(); err != nil {
+ return err
+ }
+
+ commitMany := func(ctx context.Context, m map[name.Reference]Taggable) error {
+ g, ctx := errgroup.WithContext(ctx)
+ // With all of the constituent elements uploaded, upload the manifests
+ // to commit the images and indexes, and collect any errors.
+ type task struct {
+ i Taggable
+ ref name.Reference
+ }
+ taskChan := make(chan task, 2*o.jobs)
+ for i := 0; i < o.jobs; i++ {
+ // Start N workers consuming tasks to upload manifests.
+ g.Go(func() error {
+ for t := range taskChan {
+ if err := w.commitManifest(ctx, t.i, t.ref); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ }
+ go func() {
+ for ref, i := range m {
+ taskChan <- task{i, ref}
+ }
+ close(taskChan)
+ }()
+ return g.Wait()
+ }
+ // Push originally requested image manifests. These have no
+ // dependencies.
+ if err := commitMany(ctx, images); err != nil {
+ return err
+ }
+ // Push new manifests from lowest levels up.
+ for i := len(newManifests) - 1; i >= 0; i-- {
+ if err := commitMany(ctx, newManifests[i]); err != nil {
+ return err
+ }
+ }
+ // Push originally requested index manifests, which might depend on
+ // newly discovered manifests.
+
+ return commitMany(ctx, indexes)
+}
+
+// addIndexBlobs adds blobs to the set of blobs we intend to upload, and
+// returns the latest copy of the ordered collection of manifests to upload.
+func addIndexBlobs(idx v1.ImageIndex, blobs map[v1.Hash]v1.Layer, repo name.Repository, newManifests []map[name.Reference]Taggable, lvl int, allowNondistributableArtifacts bool) ([]map[name.Reference]Taggable, error) {
+ if lvl > len(newManifests)-1 {
+ newManifests = append(newManifests, map[name.Reference]Taggable{})
+ }
+
+ im, err := idx.IndexManifest()
+ if err != nil {
+ return nil, err
+ }
+ for _, desc := range im.Manifests {
+ switch desc.MediaType {
+ case types.OCIImageIndex, types.DockerManifestList:
+ idx, err := idx.ImageIndex(desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+ newManifests, err = addIndexBlobs(idx, blobs, repo, newManifests, lvl+1, allowNondistributableArtifacts)
+ if err != nil {
+ return nil, err
+ }
+
+ // Also track the sub-index manifest to upload later by digest.
+ newManifests[lvl][repo.Digest(desc.Digest.String())] = idx
+ case types.OCIManifestSchema1, types.DockerManifestSchema2:
+ img, err := idx.Image(desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+ if err := addImageBlobs(img, blobs, allowNondistributableArtifacts); err != nil {
+ return nil, err
+ }
+
+ // Also track the sub-image manifest to upload later by digest.
+ newManifests[lvl][repo.Digest(desc.Digest.String())] = img
+ default:
+ // Workaround for #819.
+ if wl, ok := idx.(withLayer); ok {
+ layer, err := wl.Layer(desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+ if err := addLayerBlob(layer, blobs, allowNondistributableArtifacts); err != nil {
+ return nil, err
+ }
+ } else {
+ return nil, fmt.Errorf("unknown media type: %v", desc.MediaType)
+ }
+ }
+ }
+ return newManifests, nil
+}
+
+func addLayerBlob(l v1.Layer, blobs map[v1.Hash]v1.Layer, allowNondistributableArtifacts bool) error {
+ // Ignore foreign layers.
+ mt, err := l.MediaType()
+ if err != nil {
+ return err
+ }
+
+ if mt.IsDistributable() || allowNondistributableArtifacts {
+ d, err := l.Digest()
+ if err != nil {
+ return err
+ }
+
+ blobs[d] = l
+ }
+
+ return nil
+}
+
+func addImageBlobs(img v1.Image, blobs map[v1.Hash]v1.Layer, allowNondistributableArtifacts bool) error {
+ ls, err := img.Layers()
+ if err != nil {
+ return err
+ }
+ // Collect all layers.
+ for _, l := range ls {
+ if err := addLayerBlob(l, blobs, allowNondistributableArtifacts); err != nil {
+ return err
+ }
+ }
+
+ // Collect config blob.
+ cl, err := partial.ConfigLayer(img)
+ if err != nil {
+ return err
+ }
+ return addLayerBlob(cl, blobs, allowNondistributableArtifacts)
+}
diff --git a/pkg/v1/remote/multi_write_test.go b/pkg/v1/remote/multi_write_test.go
new file mode 100644
index 0000000..c2dd2f0
--- /dev/null
+++ b/pkg/v1/remote/multi_write_test.go
@@ -0,0 +1,351 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "context"
+ "io"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestMultiWrite(t *testing.T) {
+ // Create a random image.
+ img1, err := random.Image(1024, 2)
+ if err != nil {
+ t.Fatal("random.Image:", err)
+ }
+
+ // Create another image that's based on the first.
+ rl, err := random.Layer(1024, types.OCIUncompressedLayer)
+ if err != nil {
+ t.Fatal("random.Layer:", err)
+ }
+ img2, err := mutate.AppendLayers(img1, rl)
+ if err != nil {
+ t.Fatal("mutate.AppendLayers:", err)
+ }
+
+ // Also create a random index of images.
+ subidx, err := random.Index(1024, 2, 3)
+ if err != nil {
+ t.Fatal("random.Index:", err)
+ }
+
+ // Add a sub-sub-index of random images.
+ subsubidx, err := random.Index(1024, 3, 4)
+ if err != nil {
+ t.Fatal("random.Index:", err)
+ }
+ subidx = mutate.AppendManifests(subidx, mutate.IndexAddendum{Add: subsubidx})
+
+ // Create an index containing both images and the index above.
+ idx := mutate.AppendManifests(empty.Index,
+ mutate.IndexAddendum{Add: img1},
+ mutate.IndexAddendum{Add: img2},
+ mutate.IndexAddendum{Add: subidx},
+ mutate.IndexAddendum{Add: rl},
+ )
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Write both images and the manifest list.
+ tag1, tag2, tag3 := mustNewTag(t, u.Host+"/repo:tag1"), mustNewTag(t, u.Host+"/repo:tag2"), mustNewTag(t, u.Host+"/repo:tag3")
+ if err := MultiWrite(map[name.Reference]Taggable{
+ tag1: img1,
+ tag2: img2,
+ tag3: idx,
+ }); err != nil {
+ t.Error("Write:", err)
+ }
+
+ // Check that tagged images are present.
+ for _, tag := range []name.Tag{tag1, tag2} {
+ got, err := Image(tag)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+ if err := validate.Image(got); err != nil {
+ t.Error("Validate() =", err)
+ }
+ }
+
+ // Check that tagged manfest list is present and valid.
+ got, err := Index(tag3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := validate.Index(got); err != nil {
+ t.Error("Validate() =", err)
+ }
+}
+
+func TestMultiWriteWithNondistributableLayer(t *testing.T) {
+ // Create a random image.
+ img1, err := random.Image(1024, 2)
+ if err != nil {
+ t.Fatal("random.Image:", err)
+ }
+
+ // Create another image that's based on the first.
+ rl, err := random.Layer(1024, types.OCIRestrictedLayer)
+ if err != nil {
+ t.Fatal("random.Layer:", err)
+ }
+ img, err := mutate.AppendLayers(img1, rl)
+ if err != nil {
+ t.Fatal("mutate.AppendLayers:", err)
+ }
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Write the image.
+ tag1 := mustNewTag(t, u.Host+"/repo:tag1")
+ if err := MultiWrite(map[name.Reference]Taggable{tag1: img}, WithNondistributable); err != nil {
+ t.Error("Write:", err)
+ }
+
+ // Check that tagged image is present.
+ got, err := Image(tag1)
+ if err != nil {
+ t.Error(err)
+ }
+ if err := validate.Image(got); err != nil {
+ t.Error("Validate() =", err)
+ }
+}
+
+func TestMultiWrite_Retry(t *testing.T) {
+ // Create a random image.
+ img1, err := random.Image(1024, 2)
+ if err != nil {
+ t.Fatal("random.Image:", err)
+ }
+
+ t.Run("retry http error 500", func(t *testing.T) {
+ // Set up a fake registry.
+ handler := registry.New()
+
+ numOfInternalServerErrors := 0
+ registryThatFailsOnFirstUpload := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
+ if strings.Contains(request.URL.Path, "/manifests/") && numOfInternalServerErrors < 1 {
+ numOfInternalServerErrors++
+ responseWriter.WriteHeader(500)
+ return
+ }
+ handler.ServeHTTP(responseWriter, request)
+ })
+
+ s := httptest.NewServer(registryThatFailsOnFirstUpload)
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tag1 := mustNewTag(t, u.Host+"/repo:tag1")
+ if err := MultiWrite(map[name.Reference]Taggable{
+ tag1: img1,
+ }, WithRetryBackoff(fastBackoff)); err != nil {
+ t.Error("Write:", err)
+ }
+ })
+
+ t.Run("do not retry http error 401", func(t *testing.T) {
+ // Set up a fake registry.
+ handler := registry.New()
+
+ numOf401HttpErrors := 0
+ registryThatFailsOnFirstUpload := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
+ if strings.Contains(request.URL.Path, "/manifests/") {
+ numOf401HttpErrors++
+ responseWriter.WriteHeader(401)
+ return
+ }
+ handler.ServeHTTP(responseWriter, request)
+ })
+
+ s := httptest.NewServer(registryThatFailsOnFirstUpload)
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tag1 := mustNewTag(t, u.Host+"/repo:tag1")
+ if err := MultiWrite(map[name.Reference]Taggable{
+ tag1: img1,
+ }); err == nil {
+ t.Fatal("Expected error:")
+ }
+
+ if numOf401HttpErrors > 1 {
+ t.Fatal("Should not retry on 401 errors:")
+ }
+ })
+
+ t.Run("do not retry transport errors if transport.Wrapper is used", func(t *testing.T) {
+ // reference a http server that is not listening (used to pick a port that isn't listening)
+ onlyHandlesPing := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
+ if strings.HasSuffix(request.URL.Path, "/v2/") {
+ responseWriter.WriteHeader(200)
+ return
+ }
+ })
+ s := httptest.NewServer(onlyHandlesPing)
+ defer s.Close()
+
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tag1 := mustNewTag(t, u.Host+"/repo:tag1")
+
+ // using a transport.Wrapper, meaning retry logic should not be wrapped
+ doesNotRetryTransport := &countTransport{inner: http.DefaultTransport}
+ transportWrapper, err := transport.NewWithContext(context.Background(), tag1.Repository.Registry, authn.Anonymous, doesNotRetryTransport, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ noRetry := func(error) bool { return false }
+
+ if err := MultiWrite(map[name.Reference]Taggable{
+ tag1: img1,
+ }, WithTransport(transportWrapper), WithJobs(1), WithRetryPredicate(noRetry)); err == nil {
+ t.Errorf("Expected an error, got nil")
+ }
+
+ // expect count == 1 since jobs is set to 1 and we should not retry on transport eof error
+ if doesNotRetryTransport.count != 1 {
+ t.Errorf("Incorrect count, got %d, want %d", doesNotRetryTransport.count, 1)
+ }
+ })
+
+ t.Run("do not add UserAgent if transport.Wrapper is used", func(t *testing.T) {
+ expectedNotUsedUserAgent := "TEST_USER_AGENT"
+
+ handler := registry.New()
+
+ registryThatAssertsUserAgentIsCorrect := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
+ if strings.Contains(request.Header.Get("User-Agent"), expectedNotUsedUserAgent) {
+ t.Fatalf("Should not contain User-Agent: %s, Got: %s", expectedNotUsedUserAgent, request.Header.Get("User-Agent"))
+ }
+
+ handler.ServeHTTP(responseWriter, request)
+ })
+
+ s := httptest.NewServer(registryThatAssertsUserAgentIsCorrect)
+
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tag1 := mustNewTag(t, u.Host+"/repo:tag1")
+ // using a transport.Wrapper, meaning retry logic should not be wrapped
+ transportWrapper, err := transport.NewWithContext(context.Background(), tag1.Repository.Registry, authn.Anonymous, http.DefaultTransport, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := MultiWrite(map[name.Reference]Taggable{
+ tag1: img1,
+ }, WithTransport(transportWrapper), WithUserAgent(expectedNotUsedUserAgent)); err != nil {
+ t.Fatal(err)
+ }
+ })
+}
+
+// TestMultiWrite_Deep tests that a deeply nested tree of manifest lists gets
+// pushed in the correct order (i.e., each level in sequence).
+func TestMultiWrite_Deep(t *testing.T) {
+ idx, err := random.Index(1024, 2, 2)
+ if err != nil {
+ t.Fatal("random.Image:", err)
+ }
+ for i := 0; i < 4; i++ {
+ idx = mutate.AppendManifests(idx, mutate.IndexAddendum{Add: idx})
+ }
+
+ // Set up a fake registry (with NOP logger to avoid spamming test logs).
+ nopLog := log.New(io.Discard, "", 0)
+ s := httptest.NewServer(registry.New(registry.Logger(nopLog)))
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Write both images and the manifest list.
+ tag := mustNewTag(t, u.Host+"/repo:tag")
+ if err := MultiWrite(map[name.Reference]Taggable{
+ tag: idx,
+ }); err != nil {
+ t.Error("Write:", err)
+ }
+
+ // Check that tagged manfest list is present and valid.
+ got, err := Index(tag)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := validate.Index(got); err != nil {
+ t.Error("Validate() =", err)
+ }
+}
+
+type countTransport struct {
+ count int
+ inner http.RoundTripper
+}
+
+func (t *countTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ if strings.HasSuffix(req.URL.Path, "/v2/") {
+ return t.inner.RoundTrip(req)
+ }
+
+ t.count++
+ return nil, io.ErrUnexpectedEOF
+}
diff --git a/pkg/v1/remote/options.go b/pkg/v1/remote/options.go
new file mode 100644
index 0000000..54a0af2
--- /dev/null
+++ b/pkg/v1/remote/options.go
@@ -0,0 +1,317 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "context"
+ "errors"
+ "io"
+ "net"
+ "net/http"
+ "syscall"
+ "time"
+
+ "github.com/google/go-containerregistry/internal/retry"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/logs"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+)
+
+// Option is a functional option for remote operations.
+type Option func(*options) error
+
+type options struct {
+ auth authn.Authenticator
+ keychain authn.Keychain
+ transport http.RoundTripper
+ platform v1.Platform
+ context context.Context
+ jobs int
+ userAgent string
+ allowNondistributableArtifacts bool
+ updates chan<- v1.Update
+ pageSize int
+ retryBackoff Backoff
+ retryPredicate retry.Predicate
+ filter map[string]string
+}
+
+var defaultPlatform = v1.Platform{
+ Architecture: "amd64",
+ OS: "linux",
+}
+
+// Backoff is an alias of retry.Backoff to expose this configuration option to consumers of this lib
+type Backoff = retry.Backoff
+
+var defaultRetryPredicate retry.Predicate = func(err error) bool {
+ // Various failure modes here, as we're often reading from and writing to
+ // the network.
+ if retry.IsTemporary(err) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) || errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
+ logs.Warn.Printf("retrying %v", err)
+ return true
+ }
+ return false
+}
+
+// Try this three times, waiting 1s after first failure, 3s after second.
+var defaultRetryBackoff = Backoff{
+ Duration: 1.0 * time.Second,
+ Factor: 3.0,
+ Jitter: 0.1,
+ Steps: 3,
+}
+
+// Useful for tests
+var fastBackoff = Backoff{
+ Duration: 1.0 * time.Millisecond,
+ Factor: 3.0,
+ Jitter: 0.1,
+ Steps: 3,
+}
+
+var retryableStatusCodes = []int{
+ http.StatusRequestTimeout,
+ http.StatusInternalServerError,
+ http.StatusBadGateway,
+ http.StatusServiceUnavailable,
+ http.StatusGatewayTimeout,
+}
+
+const (
+ defaultJobs = 4
+
+ // ECR returns an error if n > 1000:
+ // https://github.com/google/go-containerregistry/issues/1091
+ defaultPageSize = 1000
+)
+
+// DefaultTransport is based on http.DefaultTransport with modifications
+// documented inline below.
+var DefaultTransport http.RoundTripper = &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: (&net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ }).DialContext,
+ ForceAttemptHTTP2: true,
+ MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
+}
+
+func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
+ o := &options{
+ transport: DefaultTransport,
+ platform: defaultPlatform,
+ context: context.Background(),
+ jobs: defaultJobs,
+ pageSize: defaultPageSize,
+ retryPredicate: defaultRetryPredicate,
+ retryBackoff: defaultRetryBackoff,
+ }
+
+ for _, option := range opts {
+ if err := option(o); err != nil {
+ return nil, err
+ }
+ }
+
+ switch {
+ case o.auth != nil && o.keychain != nil:
+ // It is a better experience to explicitly tell a caller their auth is misconfigured
+ // than potentially fail silently when the correct auth is overridden by option misuse.
+ return nil, errors.New("provide an option for either authn.Authenticator or authn.Keychain, not both")
+ case o.keychain != nil:
+ auth, err := o.keychain.Resolve(target)
+ if err != nil {
+ return nil, err
+ }
+ o.auth = auth
+ case o.auth == nil:
+ o.auth = authn.Anonymous
+ }
+
+ // transport.Wrapper is a signal that consumers are opt-ing into providing their own transport without any additional wrapping.
+ // This is to allow consumers full control over the transports logic, such as providing retry logic.
+ if _, ok := o.transport.(*transport.Wrapper); !ok {
+ // Wrap the transport in something that logs requests and responses.
+ // It's expensive to generate the dumps, so skip it if we're writing
+ // to nothing.
+ if logs.Enabled(logs.Debug) {
+ o.transport = transport.NewLogger(o.transport)
+ }
+
+ // Wrap the transport in something that can retry network flakes.
+ o.transport = transport.NewRetry(o.transport, transport.WithRetryPredicate(defaultRetryPredicate), transport.WithRetryStatusCodes(retryableStatusCodes...))
+
+ // Wrap this last to prevent transport.New from double-wrapping.
+ if o.userAgent != "" {
+ o.transport = transport.NewUserAgent(o.transport, o.userAgent)
+ }
+ }
+
+ return o, nil
+}
+
+// WithTransport is a functional option for overriding the default transport
+// for remote operations.
+// If transport.Wrapper is provided, this signals that the consumer does *not* want any further wrapping to occur.
+// i.e. logging, retry and useragent
+//
+// The default transport is DefaultTransport.
+func WithTransport(t http.RoundTripper) Option {
+ return func(o *options) error {
+ o.transport = t
+ return nil
+ }
+}
+
+// WithAuth is a functional option for overriding the default authenticator
+// for remote operations.
+// It is an error to use both WithAuth and WithAuthFromKeychain in the same Option set.
+//
+// The default authenticator is authn.Anonymous.
+func WithAuth(auth authn.Authenticator) Option {
+ return func(o *options) error {
+ o.auth = auth
+ return nil
+ }
+}
+
+// WithAuthFromKeychain is a functional option for overriding the default
+// authenticator for remote operations, using an authn.Keychain to find
+// credentials.
+// It is an error to use both WithAuth and WithAuthFromKeychain in the same Option set.
+//
+// The default authenticator is authn.Anonymous.
+func WithAuthFromKeychain(keys authn.Keychain) Option {
+ return func(o *options) error {
+ o.keychain = keys
+ return nil
+ }
+}
+
+// WithPlatform is a functional option for overriding the default platform
+// that Image and Descriptor.Image use for resolving an index to an image.
+//
+// The default platform is amd64/linux.
+func WithPlatform(p v1.Platform) Option {
+ return func(o *options) error {
+ o.platform = p
+ return nil
+ }
+}
+
+// WithContext is a functional option for setting the context in http requests
+// performed by a given function. Note that this context is used for _all_
+// http requests, not just the initial volley. E.g., for remote.Image, the
+// context will be set on http requests generated by subsequent calls to
+// RawConfigFile() and even methods on layers returned by Layers().
+//
+// The default context is context.Background().
+func WithContext(ctx context.Context) Option {
+ return func(o *options) error {
+ o.context = ctx
+ return nil
+ }
+}
+
+// WithJobs is a functional option for setting the parallelism of remote
+// operations performed by a given function. Note that not all remote
+// operations support parallelism.
+//
+// The default value is 4.
+func WithJobs(jobs int) Option {
+ return func(o *options) error {
+ if jobs <= 0 {
+ return errors.New("jobs must be greater than zero")
+ }
+ o.jobs = jobs
+ return nil
+ }
+}
+
+// WithUserAgent adds the given string to the User-Agent header for any HTTP
+// requests. This header will also include "go-containerregistry/${version}".
+//
+// If you want to completely overwrite the User-Agent header, use WithTransport.
+func WithUserAgent(ua string) Option {
+ return func(o *options) error {
+ o.userAgent = ua
+ return nil
+ }
+}
+
+// WithNondistributable includes non-distributable (foreign) layers
+// when writing images, see:
+// https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers
+//
+// The default behaviour is to skip these layers
+func WithNondistributable(o *options) error {
+ o.allowNondistributableArtifacts = true
+ return nil
+}
+
+// WithProgress takes a channel that will receive progress updates as bytes are written.
+//
+// Sending updates to an unbuffered channel will block writes, so callers
+// should provide a buffered channel to avoid potential deadlocks.
+func WithProgress(updates chan<- v1.Update) Option {
+ return func(o *options) error {
+ o.updates = updates
+ return nil
+ }
+}
+
+// WithPageSize sets the given size as the value of parameter 'n' in the request.
+//
+// To omit the `n` parameter entirely, use WithPageSize(0).
+// The default value is 1000.
+func WithPageSize(size int) Option {
+ return func(o *options) error {
+ o.pageSize = size
+ return nil
+ }
+}
+
+// WithRetryBackoff sets the httpBackoff for retry HTTP operations.
+func WithRetryBackoff(backoff Backoff) Option {
+ return func(o *options) error {
+ o.retryBackoff = backoff
+ return nil
+ }
+}
+
+// WithRetryPredicate sets the predicate for retry HTTP operations.
+func WithRetryPredicate(predicate retry.Predicate) Option {
+ return func(o *options) error {
+ o.retryPredicate = predicate
+ return nil
+ }
+}
+
+// WithFilter sets the filter querystring for HTTP operations.
+func WithFilter(key string, value string) Option {
+ return func(o *options) error {
+ if o.filter == nil {
+ o.filter = map[string]string{}
+ }
+ o.filter[key] = value
+ return nil
+ }
+}
diff --git a/pkg/v1/remote/progress.go b/pkg/v1/remote/progress.go
new file mode 100644
index 0000000..1f43963
--- /dev/null
+++ b/pkg/v1/remote/progress.go
@@ -0,0 +1,69 @@
+// Copyright 2022 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "io"
+ "sync"
+ "sync/atomic"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+type progress struct {
+ sync.Mutex
+ updates chan<- v1.Update
+ lastUpdate *v1.Update
+}
+
+func (p *progress) total(delta int64) {
+ atomic.AddInt64(&p.lastUpdate.Total, delta)
+}
+
+func (p *progress) complete(delta int64) {
+ p.Lock()
+ defer p.Unlock()
+ p.updates <- v1.Update{
+ Total: p.lastUpdate.Total,
+ Complete: atomic.AddInt64(&p.lastUpdate.Complete, delta),
+ }
+}
+
+func (p *progress) err(err error) error {
+ if err != nil && p.updates != nil {
+ p.updates <- v1.Update{Error: err}
+ }
+ return err
+}
+
+type progressReader struct {
+ rc io.ReadCloser
+
+ count *int64 // number of bytes this reader has read, to support resetting on retry.
+ progress *progress
+}
+
+func (r *progressReader) Read(b []byte) (int, error) {
+ n, err := r.rc.Read(b)
+ if err != nil {
+ return n, err
+ }
+ atomic.AddInt64(r.count, int64(n))
+ // TODO: warn/debug log if sending takes too long, or if sending is blocked while context is canceled.
+ r.progress.complete(int64(n))
+ return n, nil
+}
+
+func (r *progressReader) Close() error { return r.rc.Close() }
diff --git a/pkg/v1/remote/progress_test.go b/pkg/v1/remote/progress_test.go
new file mode 100644
index 0000000..759c8ca
--- /dev/null
+++ b/pkg/v1/remote/progress_test.go
@@ -0,0 +1,463 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "sync"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestWriteLayer_Progress(t *testing.T) {
+ l, err := random.Layer(1000, types.OCIUncompressedLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := make(chan v1.Update, 200)
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/progress/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := WriteLayer(ref.Context(), l, WithProgress(c)); err != nil {
+ t.Fatalf("WriteLayer: %v", err)
+ }
+ if err := checkUpdates(c); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// TestWriteLayer_Progress_Exists tests progress reporting behavior when the
+// layer already exists in the registry, so writes are skipped, but progress
+// should still be reported in one update.
+func TestWriteLayer_Progress_Exists(t *testing.T) {
+ l, err := random.Layer(1000, types.OCILayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := make(chan v1.Update, 200)
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/progress/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Write the layer, so we can get updates when we write it again.
+ if err := WriteLayer(ref.Context(), l); err != nil {
+ t.Fatalf("WriteLayer: %v", err)
+ }
+ if err := WriteLayer(ref.Context(), l, WithProgress(c)); err != nil {
+ t.Fatalf("WriteLayer: %v", err)
+ }
+ if err := checkUpdates(c); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestWrite_Progress(t *testing.T) {
+ img, err := random.Image(1000, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := make(chan v1.Update, 200)
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/progress/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Write(ref, img, WithProgress(c)); err != nil {
+ t.Fatalf("Write: %v", err)
+ }
+
+ if err := checkUpdates(c); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// An image with multiple identical layers is handled correctly.
+func TestWrite_Progress_DedupeLayers(t *testing.T) {
+ img := empty.Image
+ for i := 0; i < 10; i++ {
+ l, err := random.Layer(1000, types.OCILayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ img, err = mutate.AppendLayers(img, l)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ c := make(chan v1.Update, 200)
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/progress/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Write(ref, img, WithProgress(c)); err != nil {
+ t.Fatalf("Write: %v", err)
+ }
+
+ if err := checkUpdates(c); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestWriteIndex_Progress(t *testing.T) {
+ idx, err := random.Index(1000, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := make(chan v1.Update, 200)
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/progress/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := WriteIndex(ref, idx, WithProgress(c)); err != nil {
+ t.Fatalf("WriteIndex: %v", err)
+ }
+
+ if err := checkUpdates(c); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMultiWrite_Progress(t *testing.T) {
+ idx, err := random.Index(1000, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := make(chan v1.Update, 1000)
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ref, err := name.ParseReference(fmt.Sprintf("%s/test/progress/upload", u.Host))
+ if err != nil {
+ t.Fatal(err)
+ }
+ ref2, err := name.ParseReference(fmt.Sprintf("%s/test/progress/upload:again", u.Host))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := MultiWrite(map[name.Reference]Taggable{
+ ref: idx,
+ ref2: idx,
+ }, WithProgress(c)); err != nil {
+ t.Fatalf("MultiWrite: %v", err)
+ }
+
+ if err := checkUpdates(c); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMultiWrite_Progress_Retry(t *testing.T) {
+ idx, err := random.Index(1000, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := make(chan v1.Update, 1000)
+
+ // Set up a fake registry.
+ handler := registry.New()
+ numOfInternalServerErrors := 0
+ var mu sync.Mutex
+ registryThatFailsOnFirstUpload := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
+ mu.Lock()
+ defer mu.Unlock()
+ if strings.Contains(request.URL.Path, "/manifests/") && numOfInternalServerErrors < 1 {
+ numOfInternalServerErrors++
+ responseWriter.WriteHeader(500)
+ return
+ }
+ handler.ServeHTTP(responseWriter, request)
+ })
+
+ s := httptest.NewServer(registryThatFailsOnFirstUpload)
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ref, err := name.ParseReference(fmt.Sprintf("%s/test/progress/upload", u.Host))
+ if err != nil {
+ t.Fatal(err)
+ }
+ ref2, err := name.ParseReference(fmt.Sprintf("%s/test/progress/upload:again", u.Host))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := MultiWrite(map[name.Reference]Taggable{
+ ref: idx,
+ ref2: idx,
+ }, WithProgress(c), WithRetryBackoff(fastBackoff)); err != nil {
+ t.Fatalf("MultiWrite: %v", err)
+ }
+
+ if err := checkUpdates(c); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestWriteLayer_Progress_Retry(t *testing.T) {
+ l, err := random.Layer(100000, types.OCIUncompressedLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := make(chan v1.Update, 200)
+
+ // Set up a fake registry.
+ handler := registry.New()
+
+ numOfInternalServerErrors := 0
+ registryThatFailsOnFirstUpload := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
+ if request.Method == http.MethodPatch && strings.Contains(request.URL.Path, "upload/blobs/uploads") && numOfInternalServerErrors < 1 {
+ numOfInternalServerErrors++
+ responseWriter.WriteHeader(500)
+ return
+ }
+ handler.ServeHTTP(responseWriter, request)
+ })
+
+ s := httptest.NewServer(registryThatFailsOnFirstUpload)
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/progress/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := WriteLayer(ref.Context(), l, WithProgress(c), WithRetryBackoff(fastBackoff)); err != nil {
+ t.Fatalf("WriteLayer: %v", err)
+ }
+
+ everyUpdate := []v1.Update{}
+ for update := range c {
+ everyUpdate = append(everyUpdate, update)
+ }
+
+ if diff := cmp.Diff(everyUpdate, []v1.Update{
+ {Total: 101921, Complete: 32768},
+ {Total: 101921, Complete: 65536},
+ {Total: 101921, Complete: 98304},
+ {Total: 101921, Complete: 101921},
+ // retry results in the same messages sent to the updates channel
+ {Total: 101921, Complete: 0},
+ {Total: 101921, Complete: 32768},
+ {Total: 101921, Complete: 65536},
+ {Total: 101921, Complete: 98304},
+ {Total: 101921, Complete: 101921},
+ }); diff != "" {
+ t.Errorf("received updates (-want +got) = %s", diff)
+ }
+}
+
+func TestWriteLayer_Progress_Error(t *testing.T) {
+ l, err := random.Layer(100000, types.OCIUncompressedLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := make(chan v1.Update, 200)
+
+ // Set up a fake registry.
+ handler := registry.New()
+ registryThatAlwaysFails := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
+ if request.Method == http.MethodPatch && strings.Contains(request.URL.Path, "blobs/uploads") {
+ responseWriter.WriteHeader(403)
+ }
+ handler.ServeHTTP(responseWriter, request)
+ })
+
+ s := httptest.NewServer(registryThatAlwaysFails)
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/progress/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := WriteLayer(ref.Context(), l, WithProgress(c)); err == nil {
+ t.Errorf("WriteLayer: wanted error, got nil")
+ }
+
+ everyUpdate := []v1.Update{}
+ for update := range c {
+ everyUpdate = append(everyUpdate, update)
+ }
+
+ if diff := cmp.Diff(everyUpdate[:len(everyUpdate)-1], []v1.Update{
+ {Total: 101921, Complete: 32768},
+ {Total: 101921, Complete: 65536},
+ {Total: 101921, Complete: 98304},
+ {Total: 101921, Complete: 101921},
+ // retry results in the same messages sent to the updates channel
+ {Total: 101921, Complete: 0},
+ }); diff != "" {
+ t.Errorf("received updates (-want +got) = %s", diff)
+ }
+ if everyUpdate[len(everyUpdate)-1].Error == nil {
+ t.Errorf("Last update had nil error")
+ }
+}
+
+func TestWrite_Progress_WithNonDistributableLayer_AndIncludeNonDistributableLayersOption(t *testing.T) {
+ ociLayer, err := random.Layer(1000, types.OCILayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nonDistributableLayer, err := random.Layer(1000, types.OCIRestrictedLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ img, err := mutate.AppendLayers(empty.Image, ociLayer, nonDistributableLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ c := make(chan v1.Update, 200)
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/progress/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Write(ref, img, WithProgress(c), WithNondistributable); err != nil {
+ t.Fatalf("Write: %v", err)
+ }
+
+ if err := checkUpdates(c); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// checkUpdates checks that updates show steady progress toward a total, and
+// don't describe errors.
+func checkUpdates(updates <-chan v1.Update) error {
+ var high, total int64
+ for u := range updates {
+ if u.Error != nil {
+ return u.Error
+ }
+
+ if u.Total == 0 {
+ return errors.New("saw zero total")
+ }
+
+ if total == 0 {
+ total = u.Total
+ } else if u.Total != total {
+ return fmt.Errorf("total changed: was %d, saw %d", total, u.Total)
+ }
+
+ if u.Complete < high {
+ return fmt.Errorf("saw progress revert: was high of %d, saw %d", high, u.Complete)
+ }
+ high = u.Complete
+ }
+
+ if high > total {
+ return fmt.Errorf("final progress (%d) exceeded total (%d) by %d", high, total, high-total)
+ } else if high < total {
+ return fmt.Errorf("final progress (%d) did not reach total (%d) by %d", high, total, total-high)
+ }
+
+ return nil
+}
diff --git a/pkg/v1/remote/referrers.go b/pkg/v1/remote/referrers.go
new file mode 100644
index 0000000..b3db863
--- /dev/null
+++ b/pkg/v1/remote/referrers.go
@@ -0,0 +1,35 @@
+// Copyright 2023 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// Referrers returns a list of descriptors that refer to the given manifest digest.
+//
+// The subject manifest doesn't have to exist in the registry for there to be descriptors that refer to it.
+func Referrers(d name.Digest, options ...Option) (*v1.IndexManifest, error) {
+ o, err := makeOptions(d.Context(), options...)
+ if err != nil {
+ return nil, err
+ }
+ f, err := makeFetcher(d, o)
+ if err != nil {
+ return nil, err
+ }
+ return f.fetchReferrers(o.context, o.filter, d)
+}
diff --git a/pkg/v1/remote/referrers_test.go b/pkg/v1/remote/referrers_test.go
new file mode 100644
index 0000000..91f9edc
--- /dev/null
+++ b/pkg/v1/remote/referrers_test.go
@@ -0,0 +1,183 @@
+// Copyright 2023 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote_test
+
+import (
+ "fmt"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestReferrers(t *testing.T) {
+ // Run all tests against:
+ //
+ // (1) A OCI 1.0 registry (without referrers API)
+ // (2) An OCI 1.1+ registry (with referrers API)
+ //
+ for _, leg := range []struct {
+ server *httptest.Server
+ tryFallback bool
+ }{
+ {
+ server: httptest.NewServer(registry.New(registry.WithReferrersSupport(false))),
+ tryFallback: true,
+ },
+ {
+ server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
+ tryFallback: false,
+ },
+ } {
+ s := leg.server
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ descriptor := func(img v1.Image) v1.Descriptor {
+ d, err := img.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ sz, err := img.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+ mt, err := img.MediaType()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return v1.Descriptor{
+ Digest: d,
+ Size: sz,
+ MediaType: mt,
+ ArtifactType: "application/testing123",
+ }
+ }
+
+ // Push an image we'll attach things to.
+ // We'll copy from src to dst.
+ rootRef, err := name.ParseReference(fmt.Sprintf("%s/repo:root", u.Host))
+ if err != nil {
+ t.Fatal(err)
+ }
+ rootImg, err := random.Image(10, 10)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rootImg = mutate.ConfigMediaType(rootImg, types.MediaType("application/testing123"))
+ if err := remote.Write(rootRef, rootImg); err != nil {
+ t.Fatal(err)
+ }
+ rootDesc := descriptor(rootImg)
+ t.Logf("root image is %s", rootDesc.Digest)
+
+ // Push an image that refers to the root image as its subject.
+ leafRef, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf", u.Host))
+ if err != nil {
+ t.Fatal(err)
+ }
+ leafImg, err := random.Image(20, 20)
+ if err != nil {
+ t.Fatal(err)
+ }
+ leafImg = mutate.ConfigMediaType(leafImg, types.MediaType("application/testing123"))
+ leafImg = mutate.Subject(leafImg, rootDesc).(v1.Image)
+ if err := remote.Write(leafRef, leafImg); err != nil {
+ t.Fatal(err)
+ }
+ leafDesc := descriptor(leafImg)
+ t.Logf("leaf image is %s", leafDesc.Digest)
+
+ // Get the referrers of the root image, by digest.
+ rootRefDigest := rootRef.Context().Digest(rootDesc.Digest.String())
+ index, err := remote.Referrers(rootRefDigest)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if d := cmp.Diff([]v1.Descriptor{leafDesc}, index.Manifests); d != "" {
+ t.Fatalf("referrers diff (-want,+got): %s", d)
+ }
+
+ if leg.tryFallback {
+ // Get the referrers by querying the root image's fallback tag directly.
+ tag, err := name.ParseReference(fmt.Sprintf("%s/repo:sha256-%s", u.Host, rootDesc.Digest.Hex))
+ if err != nil {
+ t.Fatal(err)
+ }
+ idx, err := remote.Index(tag)
+ if err != nil {
+ t.Fatal(err)
+ }
+ mf, err := idx.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if d := cmp.Diff(index.Manifests, mf.Manifests); d != "" {
+ t.Fatalf("fallback tag diff (-want,+got): %s", d)
+ }
+ }
+
+ // Push the leaf image again, this time with a different tag.
+ // This shouldn't add another item to the root image's referrers,
+ // because it's the same digest.
+ // Push an image that refers to the root image as its subject.
+ leaf2Ref, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf2", u.Host))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := remote.Write(leaf2Ref, leafImg); err != nil {
+ t.Fatal(err)
+ }
+ // Get the referrers of the root image again, which should only have one entry.
+ rootRefDigest = rootRef.Context().Digest(rootDesc.Digest.String())
+ index, err = remote.Referrers(rootRefDigest)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if d := cmp.Diff([]v1.Descriptor{leafDesc}, index.Manifests); d != "" {
+ t.Fatalf("referrers diff after second push (-want,+got): %s", d)
+ }
+
+ // Try applying filters and verify number of manifests and and annotations
+ index, err = remote.Referrers(rootRefDigest,
+ remote.WithFilter("artifactType", "application/testing123"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if numManifests := len(index.Manifests); numManifests == 0 {
+ t.Fatal("index contained 0 manifests")
+ }
+
+ index, err = remote.Referrers(rootRefDigest,
+ remote.WithFilter("artifactType", "application/testing123BADDDD"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if numManifests := len(index.Manifests); numManifests != 0 {
+ t.Fatalf("expected index to contain 0 manifests, but had %d", numManifests)
+ }
+ }
+}
diff --git a/pkg/v1/remote/transport/README.md b/pkg/v1/remote/transport/README.md
new file mode 100644
index 0000000..bd4d957
--- /dev/null
+++ b/pkg/v1/remote/transport/README.md
@@ -0,0 +1,129 @@
+# `transport`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/transport?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/transport)
+
+The [distribution protocol](https://github.com/opencontainers/distribution-spec) is fairly simple, but correctly [implementing authentication](../../../authn/README.md) is **hard**.
+
+This package [implements](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport#New) an [`http.RoundTripper`](https://godoc.org/net/http#RoundTripper)
+that transparently performs:
+* [Token
+Authentication](https://docs.docker.com/registry/spec/auth/token/) and
+* [OAuth2
+Authentication](https://docs.docker.com/registry/spec/auth/oauth/)
+
+for registry clients.
+
+## Raison d'être
+
+> Why not just use the [`docker/distribution`](https://godoc.org/github.com/docker/distribution/registry/client/auth) client?
+
+Great question! Mostly, because I don't want to depend on [`prometheus/client_golang`](https://github.com/prometheus/client_golang).
+
+As a performance optimization, that client uses [a cache](https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/registry/client/repository.go#L173) to keep track of a mapping between blob digests and their [descriptors](https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/blobs.go#L57-L86). Unfortunately, the cache [uses prometheus](https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/registry/storage/cache/cachedblobdescriptorstore.go#L44) to track hits and misses, so if you want to use that client you have to pull in all of prometheus, which is pretty large.
+
+![docker/distribution](../../../../images/docker.dot.svg)
+
+> Why does it matter if you depend on prometheus? Who cares?
+
+It's generally polite to your downstream to reduce the number of dependencies your package requires:
+
+* Downloading your package is faster, which helps our Australian friends and people on airplanes.
+* There is less code to compile, which speeds up builds and saves the planet from global warming.
+* You reduce the likelihood of inflicting dependency hell upon your consumers.
+* [Tim Hockin](https://twitter.com/thockin/status/958606077456654336) prefers it based on his experience working on Kubernetes, and he's a pretty smart guy.
+
+> Okay, what about [`containerd/containerd`](https://godoc.org/github.com/containerd/containerd/remotes/docker)?
+
+Similar reasons! That ends up pulling in grpc, protobuf, and logrus.
+
+![containerd/containerd](../../../../images/containerd.dot.svg)
+
+> Well... what about [`containers/image`](https://godoc.org/github.com/containers/image/docker)?
+
+That just uses the the `docker/distribution` client... and more!
+
+![containers/image](../../../../images/containers.dot.svg)
+
+> Wow, what about this package?
+
+Of course, this package isn't perfect either. `transport` depends on `authn`,
+which in turn depends on docker's config file parsing and handling package,
+which you don't strictly need but almost certainly want if you're going to be
+interacting with a registry.
+
+![google/go-containerregistry](../../../../images/ggcr.dot.svg)
+
+*These graphs were generated by
+[`kisielk/godepgraph`](https://github.com/kisielk/godepgraph).*
+
+## Usage
+
+This is heavily used by the
+[`remote`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote)
+package, which implements higher level image-centric functionality, but this
+package is useful if you want to interact directly with the registry to do
+something that `remote` doesn't support, e.g. [to handle with schema 1
+images](https://github.com/google/go-containerregistry/pull/509).
+
+This package also includes some [error
+handling](https://github.com/opencontainers/distribution-spec/blob/60be706c34ee7805bdd1d3d11affec53b0dfb8fb/spec.md#errors)
+facilities in the form of
+[`CheckError`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport#CheckError),
+which will parse the response body into a structured error for unexpected http
+status codes.
+
+Here's a "simple" program that writes the result of
+[listing tags](https://github.com/opencontainers/distribution-spec/blob/60be706c34ee7805bdd1d3d11affec53b0dfb8fb/spec.md#tags)
+for [`gcr.io/google-containers/pause`](https://gcr.io/google-containers/pause)
+to stdout.
+
+```go
+package main
+
+import (
+ "io"
+ "net/http"
+ "os"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+)
+
+func main() {
+ repo, err := name.NewRepository("gcr.io/google-containers/pause")
+ if err != nil {
+ panic(err)
+ }
+
+ // Fetch credentials based on your docker config file, which is $HOME/.docker/config.json or $DOCKER_CONFIG.
+ auth, err := authn.DefaultKeychain.Resolve(repo.Registry)
+ if err != nil {
+ panic(err)
+ }
+
+ // Construct an http.Client that is authorized to pull from gcr.io/google-containers/pause.
+ scopes := []string{repo.Scope(transport.PullScope)}
+ t, err := transport.New(repo.Registry, auth, http.DefaultTransport, scopes)
+ if err != nil {
+ panic(err)
+ }
+ client := &http.Client{Transport: t}
+
+ // Make the actual request.
+ resp, err := client.Get("https://gcr.io/v2/google-containers/pause/tags/list")
+ if err != nil {
+ panic(err)
+ }
+
+ // Assert that we get a 200, otherwise attempt to parse body as a structured error.
+ if err := transport.CheckError(resp, http.StatusOK); err != nil {
+ panic(err)
+ }
+
+ // Write the response to stdout.
+ if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
+ panic(err)
+ }
+}
+```
diff --git a/pkg/v1/remote/transport/basic.go b/pkg/v1/remote/transport/basic.go
new file mode 100644
index 0000000..fdb362b
--- /dev/null
+++ b/pkg/v1/remote/transport/basic.go
@@ -0,0 +1,62 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+)
+
+type basicTransport struct {
+ inner http.RoundTripper
+ auth authn.Authenticator
+ target string
+}
+
+var _ http.RoundTripper = (*basicTransport)(nil)
+
+// RoundTrip implements http.RoundTripper
+func (bt *basicTransport) RoundTrip(in *http.Request) (*http.Response, error) {
+ if bt.auth != authn.Anonymous {
+ auth, err := bt.auth.Authorization()
+ if err != nil {
+ return nil, err
+ }
+
+ // http.Client handles redirects at a layer above the http.RoundTripper
+ // abstraction, so to avoid forwarding Authorization headers to places
+ // we are redirected, only set it when the authorization header matches
+ // the host with which we are interacting.
+ // In case of redirect http.Client can use an empty Host, check URL too.
+ if in.Host == bt.target || in.URL.Host == bt.target {
+ if bearer := auth.RegistryToken; bearer != "" {
+ hdr := fmt.Sprintf("Bearer %s", bearer)
+ in.Header.Set("Authorization", hdr)
+ } else if user, pass := auth.Username, auth.Password; user != "" && pass != "" {
+ delimited := fmt.Sprintf("%s:%s", user, pass)
+ encoded := base64.StdEncoding.EncodeToString([]byte(delimited))
+ hdr := fmt.Sprintf("Basic %s", encoded)
+ in.Header.Set("Authorization", hdr)
+ } else if token := auth.Auth; token != "" {
+ hdr := fmt.Sprintf("Basic %s", token)
+ in.Header.Set("Authorization", hdr)
+ }
+ }
+ }
+ return bt.inner.RoundTrip(in)
+}
diff --git a/pkg/v1/remote/transport/basic_test.go b/pkg/v1/remote/transport/basic_test.go
new file mode 100644
index 0000000..68dd90e
--- /dev/null
+++ b/pkg/v1/remote/transport/basic_test.go
@@ -0,0 +1,138 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+)
+
+func TestBasicTransport(t *testing.T) {
+ username := "foo"
+ password := "bar"
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ hdr := r.Header.Get("Authorization")
+ if !strings.HasPrefix(hdr, "Basic ") {
+ t.Errorf("Header.Get(Authorization); got %v, want Basic prefix", hdr)
+ }
+ user, pass, _ := r.BasicAuth()
+ if user != username || pass != password {
+ t.Error("Invalid credentials.")
+ }
+ if r.URL.Path == "/v2/auth" {
+ http.Redirect(w, r, "/redirect", http.StatusMovedPermanently)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer server.Close()
+
+ inner := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ basic := &authn.Basic{Username: username, Password: password}
+ client := http.Client{Transport: &basicTransport{inner: inner, auth: basic, target: "gcr.io"}}
+
+ _, err := client.Get("http://gcr.io/v2/auth")
+ if err != nil {
+ t.Errorf("Unexpected error during Get: %v", err)
+ }
+}
+
+func TestBasicTransportRegistryToken(t *testing.T) {
+ token := "mytoken"
+ for _, tc := range []struct {
+ auth authn.Authenticator
+ hdr string
+ wantErr bool
+ }{{
+ auth: authn.FromConfig(authn.AuthConfig{RegistryToken: token}),
+ hdr: "Bearer mytoken",
+ }, {
+ auth: authn.FromConfig(authn.AuthConfig{Auth: token}),
+ hdr: "Basic mytoken",
+ }, {
+ auth: authn.Anonymous,
+ hdr: "",
+ }, {
+ auth: &badAuth{},
+ hdr: "",
+ wantErr: true,
+ }} {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ hdr := r.Header.Get("Authorization")
+ want := tc.hdr
+ if hdr != want {
+ t.Errorf("Header.Get(Authorization); got %v, want %s", hdr, want)
+ }
+ if r.URL.Path == "/v2/auth" {
+ http.Redirect(w, r, "/redirect", http.StatusMovedPermanently)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer server.Close()
+
+ inner := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ client := http.Client{Transport: &basicTransport{inner: inner, auth: tc.auth, target: "gcr.io"}}
+
+ _, err := client.Get("http://gcr.io/v2/auth")
+ if err != nil && !tc.wantErr {
+ t.Errorf("Unexpected error during Get: %v", err)
+ }
+ }
+}
+
+func TestBasicTransportWithEmptyAuthnCred(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if c, ok := r.Header["Authorization"]; ok && c[0] == "" {
+ t.Error("got empty Authorization header")
+ }
+ if r.URL.Path == "/v2/auth" {
+ http.Redirect(w, r, "/redirect", http.StatusMovedPermanently)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer server.Close()
+
+ inner := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ client := http.Client{Transport: &basicTransport{inner: inner, auth: authn.Anonymous, target: "gcr.io"}}
+ _, err := client.Get("http://gcr.io/v2/auth")
+ if err != nil {
+ t.Errorf("Unexpected error during Get: %v", err)
+ }
+}
diff --git a/pkg/v1/remote/transport/bearer.go b/pkg/v1/remote/transport/bearer.go
new file mode 100644
index 0000000..ea07ff6
--- /dev/null
+++ b/pkg/v1/remote/transport/bearer.go
@@ -0,0 +1,320 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "strings"
+
+ authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
+ "github.com/google/go-containerregistry/internal/redact"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+type bearerTransport struct {
+ // Wrapped by bearerTransport.
+ inner http.RoundTripper
+ // Basic credentials that we exchange for bearer tokens.
+ basic authn.Authenticator
+ // Holds the bearer response from the token service.
+ bearer authn.AuthConfig
+ // Registry to which we send bearer tokens.
+ registry name.Registry
+ // See https://tools.ietf.org/html/rfc6750#section-3
+ realm string
+ // See https://docs.docker.com/registry/spec/auth/token/
+ service string
+ scopes []string
+ // Scheme we should use, determined by ping response.
+ scheme string
+}
+
+var _ http.RoundTripper = (*bearerTransport)(nil)
+
+var portMap = map[string]string{
+ "http": "80",
+ "https": "443",
+}
+
+func stringSet(ss []string) map[string]struct{} {
+ set := make(map[string]struct{})
+ for _, s := range ss {
+ set[s] = struct{}{}
+ }
+ return set
+}
+
+// RoundTrip implements http.RoundTripper
+func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
+ sendRequest := func() (*http.Response, error) {
+ // http.Client handles redirects at a layer above the http.RoundTripper
+ // abstraction, so to avoid forwarding Authorization headers to places
+ // we are redirected, only set it when the authorization header matches
+ // the registry with which we are interacting.
+ // In case of redirect http.Client can use an empty Host, check URL too.
+ if matchesHost(bt.registry, in, bt.scheme) {
+ hdr := fmt.Sprintf("Bearer %s", bt.bearer.RegistryToken)
+ in.Header.Set("Authorization", hdr)
+ }
+ return bt.inner.RoundTrip(in)
+ }
+
+ res, err := sendRequest()
+ if err != nil {
+ return nil, err
+ }
+
+ // If we hit a WWW-Authenticate challenge, it might be due to expired tokens or insufficient scope.
+ if challenges := authchallenge.ResponseChallenges(res); len(challenges) != 0 {
+ // close out old response, since we will not return it.
+ res.Body.Close()
+
+ newScopes := []string{}
+ for _, wac := range challenges {
+ // TODO(jonjohnsonjr): Should we also update "realm" or "service"?
+ if want, ok := wac.Parameters["scope"]; ok {
+ // Add any scopes that we don't already request.
+ got := stringSet(bt.scopes)
+ if _, ok := got[want]; !ok {
+ newScopes = append(newScopes, want)
+ }
+ }
+ }
+
+ // Some registries seem to only look at the first scope parameter during a token exchange.
+ // If a request fails because it's missing a scope, we should put those at the beginning,
+ // otherwise the registry might just ignore it :/
+ newScopes = append(newScopes, bt.scopes...)
+ bt.scopes = newScopes
+
+ // TODO(jonjohnsonjr): Teach transport.Error about "error" and "error_description" from challenge.
+
+ // Retry the request to attempt to get a valid token.
+ if err = bt.refresh(in.Context()); err != nil {
+ return nil, err
+ }
+ return sendRequest()
+ }
+
+ return res, err
+}
+
+// It's unclear which authentication flow to use based purely on the protocol,
+// so we rely on heuristics and fallbacks to support as many registries as possible.
+// The basic token exchange is attempted first, falling back to the oauth flow.
+// If the IdentityToken is set, this indicates that we should start with the oauth flow.
+func (bt *bearerTransport) refresh(ctx context.Context) error {
+ auth, err := bt.basic.Authorization()
+ if err != nil {
+ return err
+ }
+
+ if auth.RegistryToken != "" {
+ bt.bearer.RegistryToken = auth.RegistryToken
+ return nil
+ }
+
+ var content []byte
+ if auth.IdentityToken != "" {
+ // If the secret being stored is an identity token,
+ // the Username should be set to <token>, which indicates
+ // we are using an oauth flow.
+ content, err = bt.refreshOauth(ctx)
+ var terr *Error
+ if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
+ // Note: Not all token servers implement oauth2.
+ // If the request to the endpoint returns 404 using the HTTP POST method,
+ // refer to Token Documentation for using the HTTP GET method supported by all token servers.
+ content, err = bt.refreshBasic(ctx)
+ }
+ } else {
+ content, err = bt.refreshBasic(ctx)
+ }
+ if err != nil {
+ return err
+ }
+
+ // Some registries don't have "token" in the response. See #54.
+ type tokenResponse struct {
+ Token string `json:"token"`
+ AccessToken string `json:"access_token"`
+ RefreshToken string `json:"refresh_token"`
+ // TODO: handle expiry?
+ }
+
+ var response tokenResponse
+ if err := json.Unmarshal(content, &response); err != nil {
+ return err
+ }
+
+ // Some registries set access_token instead of token.
+ if response.AccessToken != "" {
+ response.Token = response.AccessToken
+ }
+
+ // Find a token to turn into a Bearer authenticator
+ if response.Token != "" {
+ bt.bearer.RegistryToken = response.Token
+ } else {
+ return fmt.Errorf("no token in bearer response:\n%s", content)
+ }
+
+ // If we obtained a refresh token from the oauth flow, use that for refresh() now.
+ if response.RefreshToken != "" {
+ bt.basic = authn.FromConfig(authn.AuthConfig{
+ IdentityToken: response.RefreshToken,
+ })
+ }
+
+ return nil
+}
+
+func matchesHost(reg name.Registry, in *http.Request, scheme string) bool {
+ canonicalHeaderHost := canonicalAddress(in.Host, scheme)
+ canonicalURLHost := canonicalAddress(in.URL.Host, scheme)
+ canonicalRegistryHost := canonicalAddress(reg.RegistryStr(), scheme)
+ return canonicalHeaderHost == canonicalRegistryHost || canonicalURLHost == canonicalRegistryHost
+}
+
+func canonicalAddress(host, scheme string) (address string) {
+ // The host may be any one of:
+ // - hostname
+ // - hostname:port
+ // - ipv4
+ // - ipv4:port
+ // - ipv6
+ // - [ipv6]:port
+ // As net.SplitHostPort returns an error if the host does not contain a port, we should only attempt
+ // to call it when we know that the address contains a port
+ if strings.Count(host, ":") == 1 || (strings.Count(host, ":") >= 2 && strings.Contains(host, "]:")) {
+ hostname, port, err := net.SplitHostPort(host)
+ if err != nil {
+ return host
+ }
+ if port == "" {
+ port = portMap[scheme]
+ }
+
+ return net.JoinHostPort(hostname, port)
+ }
+
+ return net.JoinHostPort(host, portMap[scheme])
+}
+
+// https://docs.docker.com/registry/spec/auth/oauth/
+func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
+ auth, err := bt.basic.Authorization()
+ if err != nil {
+ return nil, err
+ }
+
+ u, err := url.Parse(bt.realm)
+ if err != nil {
+ return nil, err
+ }
+
+ v := url.Values{}
+ v.Set("scope", strings.Join(bt.scopes, " "))
+ if bt.service != "" {
+ v.Set("service", bt.service)
+ }
+ v.Set("client_id", defaultUserAgent)
+ if auth.IdentityToken != "" {
+ v.Set("grant_type", "refresh_token")
+ v.Set("refresh_token", auth.IdentityToken)
+ } else if auth.Username != "" && auth.Password != "" {
+ // TODO(#629): This is unreachable.
+ v.Set("grant_type", "password")
+ v.Set("username", auth.Username)
+ v.Set("password", auth.Password)
+ v.Set("access_type", "offline")
+ }
+
+ client := http.Client{Transport: bt.inner}
+ req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(v.Encode()))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ // We don't want to log credentials.
+ ctx = redact.NewContext(ctx, "oauth token response contains credentials")
+
+ resp, err := client.Do(req.WithContext(ctx))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if err := CheckError(resp, http.StatusOK); err != nil {
+ if bt.basic == authn.Anonymous {
+ logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
+ }
+ return nil, err
+ }
+
+ return io.ReadAll(resp.Body)
+}
+
+// https://docs.docker.com/registry/spec/auth/token/
+func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) {
+ u, err := url.Parse(bt.realm)
+ if err != nil {
+ return nil, err
+ }
+ b := &basicTransport{
+ inner: bt.inner,
+ auth: bt.basic,
+ target: u.Host,
+ }
+ client := http.Client{Transport: b}
+
+ v := u.Query()
+ v["scope"] = bt.scopes
+ v.Set("service", bt.service)
+ u.RawQuery = v.Encode()
+
+ req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // We don't want to log credentials.
+ ctx = redact.NewContext(ctx, "basic token response contains credentials")
+
+ resp, err := client.Do(req.WithContext(ctx))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if err := CheckError(resp, http.StatusOK); err != nil {
+ if bt.basic == authn.Anonymous {
+ logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
+ }
+ return nil, err
+ }
+
+ return io.ReadAll(resp.Body)
+}
diff --git a/pkg/v1/remote/transport/bearer_test.go b/pkg/v1/remote/transport/bearer_test.go
new file mode 100644
index 0000000..a03b1f9
--- /dev/null
+++ b/pkg/v1/remote/transport/bearer_test.go
@@ -0,0 +1,561 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+func TestBearerRefresh(t *testing.T) {
+ expectedToken := "Sup3rDup3rS3cr3tz"
+ expectedScope := "this-is-your-scope"
+ expectedService := "my-service.io"
+
+ cases := []struct {
+ tokenKey string
+ wantErr bool
+ }{{
+ tokenKey: "token",
+ wantErr: false,
+ }, {
+ tokenKey: "access_token",
+ wantErr: false,
+ }, {
+ tokenKey: "tolkien",
+ wantErr: true,
+ }}
+
+ for _, tc := range cases {
+ t.Run(tc.tokenKey, func(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ hdr := r.Header.Get("Authorization")
+ if !strings.HasPrefix(hdr, "Basic ") {
+ t.Errorf("Header.Get(Authorization); got %v, want Basic prefix", hdr)
+ }
+ if got, want := r.FormValue("scope"), expectedScope; got != want {
+ t.Errorf("FormValue(scope); got %v, want %v", got, want)
+ }
+ if got, want := r.FormValue("service"), expectedService; got != want {
+ t.Errorf("FormValue(service); got %v, want %v", got, want)
+ }
+ w.Write([]byte(fmt.Sprintf(`{%q: %q}`, tc.tokenKey, expectedToken)))
+ }))
+ defer server.Close()
+
+ basic := &authn.Basic{Username: "foo", Password: "bar"}
+ registry, err := name.NewRegistry(expectedService, name.WeakValidation)
+ if err != nil {
+ t.Errorf("Unexpected error during NewRegistry: %v", err)
+ }
+
+ bt := &bearerTransport{
+ inner: http.DefaultTransport,
+ basic: basic,
+ registry: registry,
+ realm: server.URL,
+ scopes: []string{expectedScope},
+ service: expectedService,
+ scheme: "http",
+ }
+
+ if err := bt.refresh(context.Background()); (err != nil) != tc.wantErr {
+ t.Errorf("refresh() = %v", err)
+ }
+ })
+ }
+}
+
+func TestBearerTransport(t *testing.T) {
+ expectedToken := "sdkjhfskjdhfkjshdf"
+
+ blobServer := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // We don't expect the blobServer to receive bearer tokens.
+ if got := r.Header.Get("Authorization"); got != "" {
+ t.Errorf("Header.Get(Authorization); got %v, want empty string", got)
+ }
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer blobServer.Close()
+
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if got, want := r.Header.Get("Authorization"), "Bearer "+expectedToken; got != want {
+ t.Errorf("Header.Get(Authorization); got %v, want %v", got, want)
+ }
+ if r.URL.Path == "/v2/auth" {
+ http.Redirect(w, r, "/redirect", http.StatusMovedPermanently)
+ return
+ }
+ if strings.Contains(r.URL.Path, "blobs") {
+ http.Redirect(w, r, blobServer.URL, http.StatusFound)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer server.Close()
+
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Errorf("Unexpected error during url.Parse: %v", err)
+ }
+ registry, err := name.NewRegistry(u.Host, name.WeakValidation)
+ if err != nil {
+ t.Errorf("Unexpected error during NewRegistry: %v", err)
+ }
+
+ client := http.Client{Transport: &bearerTransport{
+ inner: &http.Transport{},
+ bearer: authn.AuthConfig{RegistryToken: expectedToken},
+ registry: registry,
+ scheme: "http",
+ }}
+
+ _, err = client.Get(fmt.Sprintf("http://%s/v2/auth", u.Host))
+ if err != nil {
+ t.Errorf("Unexpected error during Get: %v", err)
+ }
+
+ _, err = client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host))
+ if err != nil {
+ t.Errorf("Unexpected error during Get: %v", err)
+ }
+}
+
+func TestBearerTransportTokenRefresh(t *testing.T) {
+ initialToken := "foo"
+ refreshedToken := "bar"
+
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ hdr := r.Header.Get("Authorization")
+ if hdr == "Bearer "+refreshedToken {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ if strings.HasPrefix(hdr, "Basic ") {
+ w.Write([]byte(fmt.Sprintf(`{"token": %q}`, refreshedToken)))
+ }
+
+ w.Header().Set("WWW-Authenticate", "scope=foo")
+ w.WriteHeader(http.StatusUnauthorized)
+ }))
+ defer server.Close()
+
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ registry, err := name.NewRegistry(u.Host, name.WeakValidation)
+ if err != nil {
+ t.Fatalf("Unexpected error during NewRegistry: %v", err)
+ }
+
+ // Pass Username/Password
+ transport := &bearerTransport{
+ inner: http.DefaultTransport,
+ bearer: authn.AuthConfig{RegistryToken: initialToken},
+ basic: &authn.Basic{Username: "foo", Password: "bar"},
+ registry: registry,
+ realm: server.URL,
+ scheme: "http",
+ }
+ client := http.Client{Transport: transport}
+
+ res, err := client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host))
+ if err != nil {
+ t.Errorf("Unexpected error during client.Get: %v", err)
+ return
+ }
+ if res.StatusCode != http.StatusOK {
+ t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK)
+ }
+ if got, want := transport.bearer.RegistryToken, refreshedToken; got != want {
+ t.Errorf("Expected Bearer token to be refreshed, got %v, want %v", got, want)
+ }
+
+ // Pass RegistryToken directly
+ transport.bearer = authn.AuthConfig{RegistryToken: initialToken}
+ transport.basic = &authn.Bearer{Token: refreshedToken}
+ client = http.Client{Transport: transport}
+
+ res, err = client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host))
+ if err != nil {
+ t.Errorf("Unexpected error during client.Get: %v", err)
+ return
+ }
+ if res.StatusCode != http.StatusOK {
+ t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK)
+ }
+ if got, want := transport.bearer.RegistryToken, refreshedToken; got != want {
+ t.Errorf("Expected Bearer token to be refreshed, got %v, want %v", got, want)
+ }
+}
+
+func TestBearerTransportOauthRefresh(t *testing.T) {
+ initialToken := "foo"
+ accessToken := "bar"
+ refreshToken := "baz"
+
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodPost {
+ if err := r.ParseForm(); err != nil {
+ t.Fatal(err)
+ }
+ if it := r.FormValue("refresh_token"); it != initialToken {
+ t.Errorf("want %s got %s", initialToken, it)
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(fmt.Sprintf(`{"access_token": %q, "refresh_token": %q}`, accessToken, refreshToken)))
+ return
+ }
+
+ hdr := r.Header.Get("Authorization")
+ if hdr == "Bearer "+accessToken {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ w.Header().Set("WWW-Authenticate", "scope=foo")
+ w.WriteHeader(http.StatusUnauthorized)
+ }))
+ defer server.Close()
+
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ registry, err := name.NewRegistry(u.Host, name.WeakValidation)
+ if err != nil {
+ t.Errorf("Unexpected error during NewRegistry: %v", err)
+ }
+
+ transport := &bearerTransport{
+ inner: http.DefaultTransport,
+ basic: authn.FromConfig(authn.AuthConfig{IdentityToken: initialToken}),
+ registry: registry,
+ realm: server.URL,
+ scheme: "http",
+ scopes: []string{"myscope"},
+ service: u.Host,
+ }
+ client := http.Client{Transport: transport}
+
+ res, err := client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host))
+ if err != nil {
+ t.Fatalf("Unexpected error during client.Get: %v", err)
+ }
+ if res.StatusCode != http.StatusOK {
+ t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK)
+ }
+ if want, got := transport.bearer.RegistryToken, accessToken; want != got {
+ t.Errorf("Expected Bearer token to be refreshed, got %v, want %v", got, want)
+ }
+ basicAuthConfig, err := transport.basic.Authorization()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, want := basicAuthConfig.IdentityToken, refreshToken; got != want {
+ t.Errorf("Expected Basic IdentityToken to be refreshed, got %v, want %v", got, want)
+ }
+}
+
+func TestBearerTransportOauth404Fallback(t *testing.T) {
+ basicAuth := "basic_auth"
+ identityToken := "identity_token"
+ accessToken := "access_token"
+
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodPost {
+ w.WriteHeader(http.StatusNotFound)
+ }
+
+ hdr := r.Header.Get("Authorization")
+ if hdr == "Basic "+basicAuth {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(fmt.Sprintf(`{"access_token": %q}`, accessToken)))
+ }
+ if hdr == "Bearer "+accessToken {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ w.Header().Set("WWW-Authenticate", "scope=foo")
+ w.WriteHeader(http.StatusUnauthorized)
+ }))
+ defer server.Close()
+
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ registry, err := name.NewRegistry(u.Host, name.WeakValidation)
+ if err != nil {
+ t.Errorf("Unexpected error during NewRegistry: %v", err)
+ }
+
+ transport := &bearerTransport{
+ inner: http.DefaultTransport,
+ basic: authn.FromConfig(authn.AuthConfig{
+ IdentityToken: identityToken,
+ Auth: basicAuth,
+ }),
+ registry: registry,
+ realm: server.URL,
+ scheme: "http",
+ scopes: []string{"myscope"},
+ service: u.Host,
+ }
+ client := http.Client{Transport: transport}
+
+ res, err := client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host))
+ if err != nil {
+ t.Fatalf("Unexpected error during client.Get: %v", err)
+ }
+ if res.StatusCode != http.StatusOK {
+ t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK)
+ }
+ if got, want := transport.bearer.RegistryToken, accessToken; got != want {
+ t.Errorf("Expected Bearer token to be refreshed, got %v, want %v", got, want)
+ }
+}
+
+type recorder struct {
+ reqs []*http.Request
+ resp *http.Response
+ err error
+}
+
+func newRecorder(resp *http.Response, err error) *recorder {
+ return &recorder{
+ reqs: []*http.Request{},
+ resp: resp,
+ err: err,
+ }
+}
+
+func (r *recorder) RoundTrip(in *http.Request) (*http.Response, error) {
+ r.reqs = append(r.reqs, in)
+ return r.resp, r.err
+}
+
+func TestSchemeOverride(t *testing.T) {
+ // Record the requests we get in the inner transport.
+ cannedResponse := http.Response{
+ Status: http.StatusText(http.StatusOK),
+ StatusCode: http.StatusOK,
+ }
+ rec := newRecorder(&cannedResponse, nil)
+ registry, err := name.NewRegistry("example.com")
+ if err != nil {
+ t.Fatalf("Unexpected error during NewRegistry: %v", err)
+ }
+ st := &schemeTransport{
+ inner: rec,
+ registry: registry,
+ scheme: "http",
+ }
+
+ // We should see the scheme be overridden to "http" for the registry, but the
+ // scheme for the token server should be unchanged.
+ tests := []struct {
+ url string
+ wantScheme string
+ }{{
+ url: "https://example.com",
+ wantScheme: "http",
+ }, {
+ url: "https://token.example.com",
+ wantScheme: "https",
+ }}
+
+ for i, tt := range tests {
+ req, err := http.NewRequest("GET", tt.url, nil)
+ if err != nil {
+ t.Fatalf("Unexpected error during NewRequest: %v", err)
+ }
+
+ if _, err := st.RoundTrip(req); err != nil {
+ t.Fatalf("Unexpected error during RoundTrip: %v", err)
+ }
+
+ if got, want := rec.reqs[i].URL.Scheme, tt.wantScheme; got != want {
+ t.Errorf("Wrong scheme: wanted %v, got %v", want, got)
+ }
+ }
+}
+
+func TestCanonicalAddressResolution(t *testing.T) {
+ registry, err := name.NewRegistry("does-not-matter", name.WeakValidation)
+ if err != nil {
+ t.Errorf("Unexpected error during NewRegistry: %v", err)
+ }
+
+ tests := []struct {
+ registry name.Registry
+ scheme string
+ address string
+ want string
+ }{{
+ registry: registry,
+ scheme: "http",
+ address: "registry.example.com",
+ want: "registry.example.com:80",
+ }, {
+ registry: registry,
+ scheme: "http",
+ address: "registry.example.com:12345",
+ want: "registry.example.com:12345",
+ }, {
+ registry: registry,
+ scheme: "https",
+ address: "registry.example.com",
+ want: "registry.example.com:443",
+ }, {
+ registry: registry,
+ scheme: "https",
+ address: "registry.example.com:12345",
+ want: "registry.example.com:12345",
+ }, {
+ registry: registry,
+ scheme: "http",
+ address: "registry.example.com:",
+ want: "registry.example.com:80",
+ }, {
+ registry: registry,
+ scheme: "https",
+ address: "registry.example.com:",
+ want: "registry.example.com:443",
+ }, {
+ registry: registry,
+ scheme: "http",
+ address: "2001:db8::1",
+ want: "[2001:db8::1]:80",
+ }, {
+ registry: registry,
+ scheme: "https",
+ address: "2001:db8::1",
+ want: "[2001:db8::1]:443",
+ }, {
+ registry: registry,
+ scheme: "http",
+ address: "[2001:db8::1]:12345",
+ want: "[2001:db8::1]:12345",
+ }, {
+ registry: registry,
+ scheme: "https",
+ address: "[2001:db8::1]:12345",
+ want: "[2001:db8::1]:12345",
+ }, {
+ registry: registry,
+ scheme: "http",
+ address: "[2001:db8::1]:",
+ want: "[2001:db8::1]:80",
+ }, {
+ registry: registry,
+ scheme: "https",
+ address: "[2001:db8::1]:",
+ want: "[2001:db8::1]:443",
+ }, {
+ registry: registry,
+ scheme: "https",
+ address: "something:is::wrong]:",
+ want: "something:is::wrong]:",
+ }}
+
+ for _, tt := range tests {
+ got := canonicalAddress(tt.address, tt.scheme)
+ if got != tt.want {
+ t.Errorf("Wrong canonical host: wanted %v got %v", tt.want, got)
+ }
+ }
+}
+
+func TestInsufficientScope(t *testing.T) {
+ wrong := "the-wrong-scope"
+ right := "the-right-scope"
+ realm := ""
+ expectedService := "my-service.io"
+ passed := false
+
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ query := r.URL.Query()
+
+ scopes := query["scope"]
+ switch {
+ case len(scopes) == 0:
+ if !passed {
+ w.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=%q,scope=%q", realm, right))
+ w.WriteHeader(http.StatusUnauthorized)
+ }
+ case len(scopes) == 1:
+ w.Write([]byte(`{"token": "arbitrary-token"}`))
+ default:
+ passed = true
+ w.Write([]byte(`{"token": "arbitrary-token-2"}`))
+ }
+ }))
+ defer server.Close()
+
+ basic := &authn.Basic{Username: "foo", Password: "bar"}
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Error("Unexpected error during url.Parse: ", err)
+ }
+ realm = u.Host
+
+ registry, err := name.NewRegistry(expectedService, name.WeakValidation)
+ if err != nil {
+ t.Error("Unexpected error during NewRegistry: ", err)
+ }
+
+ bt := &bearerTransport{
+ inner: http.DefaultTransport,
+ basic: basic,
+ registry: registry,
+ realm: server.URL,
+ scopes: []string{wrong},
+ service: expectedService,
+ scheme: "http",
+ }
+
+ client := http.Client{Transport: bt}
+
+ res, err := client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host))
+ if err != nil {
+ t.Error("Unexpected error during client.Get: ", err)
+ return
+ }
+ if res.StatusCode != http.StatusOK {
+ t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK)
+ }
+
+ if !passed {
+ t.Error("didn't refresh insufficient scope")
+ }
+}
diff --git a/pkg/v1/remote/transport/doc.go b/pkg/v1/remote/transport/doc.go
new file mode 100644
index 0000000..ff7025b
--- /dev/null
+++ b/pkg/v1/remote/transport/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package transport provides facilities for setting up an authenticated
+// http.RoundTripper given an Authenticator and base RoundTripper. See
+// transport.New for more information.
+package transport
diff --git a/pkg/v1/remote/transport/error.go b/pkg/v1/remote/transport/error.go
new file mode 100644
index 0000000..c0e4337
--- /dev/null
+++ b/pkg/v1/remote/transport/error.go
@@ -0,0 +1,173 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/google/go-containerregistry/internal/redact"
+)
+
+// Error implements error to support the following error specification:
+// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
+type Error struct {
+ Errors []Diagnostic `json:"errors,omitempty"`
+ // The http status code returned.
+ StatusCode int
+ // The request that failed.
+ Request *http.Request
+ // The raw body if we couldn't understand it.
+ rawBody string
+}
+
+// Check that Error implements error
+var _ error = (*Error)(nil)
+
+// Error implements error
+func (e *Error) Error() string {
+ prefix := ""
+ if e.Request != nil {
+ prefix = fmt.Sprintf("%s %s: ", e.Request.Method, redact.URL(e.Request.URL))
+ }
+ return prefix + e.responseErr()
+}
+
+func (e *Error) responseErr() string {
+ switch len(e.Errors) {
+ case 0:
+ if len(e.rawBody) == 0 {
+ if e.Request != nil && e.Request.Method == http.MethodHead {
+ return fmt.Sprintf("unexpected status code %d %s (HEAD responses have no body, use GET for details)", e.StatusCode, http.StatusText(e.StatusCode))
+ }
+ return fmt.Sprintf("unexpected status code %d %s", e.StatusCode, http.StatusText(e.StatusCode))
+ }
+ return fmt.Sprintf("unexpected status code %d %s: %s", e.StatusCode, http.StatusText(e.StatusCode), e.rawBody)
+ case 1:
+ return e.Errors[0].String()
+ default:
+ var errors []string
+ for _, d := range e.Errors {
+ errors = append(errors, d.String())
+ }
+ return fmt.Sprintf("multiple errors returned: %s",
+ strings.Join(errors, "; "))
+ }
+}
+
+// Temporary returns whether the request that preceded the error is temporary.
+func (e *Error) Temporary() bool {
+ if len(e.Errors) == 0 {
+ _, ok := temporaryStatusCodes[e.StatusCode]
+ return ok
+ }
+ for _, d := range e.Errors {
+ if _, ok := temporaryErrorCodes[d.Code]; !ok {
+ return false
+ }
+ }
+ return true
+}
+
+// Diagnostic represents a single error returned by a Docker registry interaction.
+type Diagnostic struct {
+ Code ErrorCode `json:"code"`
+ Message string `json:"message,omitempty"`
+ Detail any `json:"detail,omitempty"`
+}
+
+// String stringifies the Diagnostic in the form: $Code: $Message[; $Detail]
+func (d Diagnostic) String() string {
+ msg := fmt.Sprintf("%s: %s", d.Code, d.Message)
+ if d.Detail != nil {
+ msg = fmt.Sprintf("%s; %v", msg, d.Detail)
+ }
+ return msg
+}
+
+// ErrorCode is an enumeration of supported error codes.
+type ErrorCode string
+
+// The set of error conditions a registry may return:
+// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors-2
+const (
+ BlobUnknownErrorCode ErrorCode = "BLOB_UNKNOWN"
+ BlobUploadInvalidErrorCode ErrorCode = "BLOB_UPLOAD_INVALID"
+ BlobUploadUnknownErrorCode ErrorCode = "BLOB_UPLOAD_UNKNOWN"
+ DigestInvalidErrorCode ErrorCode = "DIGEST_INVALID"
+ ManifestBlobUnknownErrorCode ErrorCode = "MANIFEST_BLOB_UNKNOWN"
+ ManifestInvalidErrorCode ErrorCode = "MANIFEST_INVALID"
+ ManifestUnknownErrorCode ErrorCode = "MANIFEST_UNKNOWN"
+ ManifestUnverifiedErrorCode ErrorCode = "MANIFEST_UNVERIFIED"
+ NameInvalidErrorCode ErrorCode = "NAME_INVALID"
+ NameUnknownErrorCode ErrorCode = "NAME_UNKNOWN"
+ SizeInvalidErrorCode ErrorCode = "SIZE_INVALID"
+ TagInvalidErrorCode ErrorCode = "TAG_INVALID"
+ UnauthorizedErrorCode ErrorCode = "UNAUTHORIZED"
+ DeniedErrorCode ErrorCode = "DENIED"
+ UnsupportedErrorCode ErrorCode = "UNSUPPORTED"
+ TooManyRequestsErrorCode ErrorCode = "TOOMANYREQUESTS"
+ UnknownErrorCode ErrorCode = "UNKNOWN"
+
+ // This isn't defined by either docker or OCI spec, but is defined by docker/distribution:
+ // https://github.com/distribution/distribution/blob/6a977a5a754baa213041443f841705888107362a/registry/api/errcode/register.go#L60
+ UnavailableErrorCode ErrorCode = "UNAVAILABLE"
+)
+
+// TODO: Include other error types.
+var temporaryErrorCodes = map[ErrorCode]struct{}{
+ BlobUploadInvalidErrorCode: {},
+ TooManyRequestsErrorCode: {},
+ UnknownErrorCode: {},
+ UnavailableErrorCode: {},
+}
+
+var temporaryStatusCodes = map[int]struct{}{
+ http.StatusRequestTimeout: {},
+ http.StatusInternalServerError: {},
+ http.StatusBadGateway: {},
+ http.StatusServiceUnavailable: {},
+ http.StatusGatewayTimeout: {},
+}
+
+// CheckError returns a structured error if the response status is not in codes.
+func CheckError(resp *http.Response, codes ...int) error {
+ for _, code := range codes {
+ if resp.StatusCode == code {
+ // This is one of the supported status codes.
+ return nil
+ }
+ }
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ // https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
+ structuredError := &Error{}
+
+ // This can fail if e.g. the response body is not valid JSON. That's fine,
+ // we'll construct an appropriate error string from the body and status code.
+ _ = json.Unmarshal(b, structuredError)
+
+ structuredError.rawBody = string(b)
+ structuredError.StatusCode = resp.StatusCode
+ structuredError.Request = resp.Request
+
+ return structuredError
+}
diff --git a/pkg/v1/remote/transport/error_test.go b/pkg/v1/remote/transport/error_test.go
new file mode 100644
index 0000000..e42ce3a
--- /dev/null
+++ b/pkg/v1/remote/transport/error_test.go
@@ -0,0 +1,236 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestTemporary(t *testing.T) {
+ tests := []struct {
+ error *Error
+ retry bool
+ }{{
+ error: &Error{},
+ retry: false,
+ }, {
+ error: &Error{
+ Errors: []Diagnostic{{
+ Code: BlobUploadInvalidErrorCode,
+ }},
+ },
+ retry: true,
+ }, {
+ error: &Error{
+ Errors: []Diagnostic{{
+ Code: BlobUploadInvalidErrorCode,
+ }, {
+ Code: DeniedErrorCode,
+ }},
+ },
+ retry: false,
+ }, {
+ error: &Error{
+ Errors: []Diagnostic{{
+ Code: TooManyRequestsErrorCode,
+ }},
+ },
+ retry: true,
+ }, {
+ error: &Error{
+ Errors: []Diagnostic{{
+ Code: UnavailableErrorCode,
+ }},
+ },
+ retry: true,
+ }, {
+ error: &Error{
+ StatusCode: http.StatusInternalServerError,
+ },
+ retry: true,
+ }}
+
+ for _, test := range tests {
+ retry := test.error.Temporary()
+
+ if test.retry != retry {
+ t.Errorf("Temporary(%s) = %t, wanted %t", test.error, retry, test.retry)
+ }
+ }
+}
+
+func TestCheckErrorNil(t *testing.T) {
+ tests := []int{
+ http.StatusOK,
+ http.StatusAccepted,
+ http.StatusCreated,
+ http.StatusMovedPermanently,
+ http.StatusInternalServerError,
+ }
+
+ for _, code := range tests {
+ resp := &http.Response{StatusCode: code}
+
+ if err := CheckError(resp, code); err != nil {
+ t.Errorf("CheckError(%d) = %v", code, err)
+ }
+ }
+}
+
+func TestCheckErrorNotError(t *testing.T) {
+ tests := []struct {
+ code int
+ body string
+ msg string
+ request *http.Request
+ }{{
+ code: http.StatusBadRequest,
+ body: "",
+ msg: "unexpected status code 400 Bad Request",
+ }, {
+ code: http.StatusUnauthorized,
+ // Valid JSON, but not a structured error -- we should still print the body.
+ body: `{"details":"incorrect username or password"}`,
+ msg: `unexpected status code 401 Unauthorized: {"details":"incorrect username or password"}`,
+ }, {
+ code: http.StatusUnauthorized,
+ body: "Not JSON",
+ msg: "GET https://example.com/somepath?access_token=REDACTED&scope=foo&service=bar: unexpected status code 401 Unauthorized: Not JSON",
+ request: &http.Request{
+ Method: http.MethodGet,
+ URL: &url.URL{
+ Scheme: "https",
+ Host: "example.com",
+ Path: "somepath",
+ RawQuery: url.Values{
+ "scope": []string{"foo"},
+ "service": []string{"bar"},
+ "access_token": []string{"hunter2"},
+ }.Encode(),
+ },
+ },
+ }, {
+ code: http.StatusUnauthorized,
+ body: "",
+ msg: "HEAD https://example.com/somepath: unexpected status code 401 Unauthorized (HEAD responses have no body, use GET for details)",
+ request: &http.Request{
+ Method: http.MethodHead,
+ URL: &url.URL{
+ Scheme: "https",
+ Host: "example.com",
+ Path: "somepath",
+ },
+ },
+ }}
+
+ for _, test := range tests {
+ resp := &http.Response{
+ StatusCode: test.code,
+ Body: io.NopCloser(bytes.NewBufferString(test.body)),
+ Request: test.request,
+ }
+
+ err := CheckError(resp, http.StatusOK)
+ if err == nil {
+ t.Fatalf("CheckError(%d, %s) = nil, wanted error", test.code, test.body)
+ }
+ var terr *Error
+ if !errors.As(err, &terr) {
+ t.Fatalf("CheckError(%d, %s) = %v, wanted error type", test.code, test.body, err)
+ }
+
+ if terr.StatusCode != test.code {
+ t.Errorf("Incorrect status code, got %d, want %d", terr.StatusCode, test.code)
+ }
+
+ if terr.Error() != test.msg {
+ t.Errorf("Incorrect message, got %q, want %q", terr.Error(), test.msg)
+ }
+ }
+}
+
+func TestCheckErrorWithError(t *testing.T) {
+ tests := []struct {
+ name string
+ code int
+ errorBody string
+ msg string
+ }{{
+ name: "Invalid name error",
+ code: http.StatusBadRequest,
+ errorBody: `{"errors":[{"code":"NAME_INVALID","message":"a message for you"}],"StatusCode":400}`,
+ msg: "NAME_INVALID: a message for you",
+ }, {
+ name: "Only status code is provided",
+ code: http.StatusBadRequest,
+ errorBody: `{"StatusCode":400}`,
+ msg: "unexpected status code 400 Bad Request: {\"StatusCode\":400}",
+ }, {
+ name: "Multiple diagnostics",
+ code: http.StatusBadRequest,
+ errorBody: `{"errors":[{"code":"NAME_INVALID","message":"a message for you"}, {"code":"SIZE_INVALID","message":"another message for you", "detail": "with some details"}],"StatusCode":400,"Request":null}`,
+ msg: "multiple errors returned: NAME_INVALID: a message for you; SIZE_INVALID: another message for you; with some details",
+ }}
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ resp := &http.Response{
+ StatusCode: test.code,
+ Body: io.NopCloser(bytes.NewBuffer([]byte(test.errorBody))),
+ }
+
+ var terr *Error
+ if err := CheckError(resp, http.StatusOK); err == nil {
+ t.Errorf("CheckError(%d, %s) = nil, wanted error", test.code, test.errorBody)
+ } else if !errors.As(err, &terr) {
+ t.Errorf("CheckError(%d, %s) = %T, wanted *transport.Error", test.code, test.errorBody, err)
+ } else if diff := cmp.Diff(test.msg, err.Error()); diff != "" {
+ t.Errorf("CheckError(%d, %s).Error(); (-want +got) %s", test.code, test.errorBody, diff)
+ }
+ })
+ }
+}
+
+func TestBodyError(t *testing.T) {
+ expectedErr := errors.New("whoops")
+ resp := &http.Response{
+ StatusCode: http.StatusOK,
+ Body: &errReadCloser{expectedErr},
+ }
+ if err := CheckError(resp, http.StatusNotFound); err == nil {
+ t.Errorf("CheckError() = nil, wanted error %v", expectedErr)
+ } else if !errors.Is(err, expectedErr) {
+ t.Errorf("CheckError() = %v, wanted %v", err, expectedErr)
+ }
+}
+
+type errReadCloser struct {
+ err error
+}
+
+func (e *errReadCloser) Read(p []byte) (int, error) {
+ return 0, e.err
+}
+
+func (e *errReadCloser) Close() error {
+ return e.err
+}
diff --git a/pkg/v1/remote/transport/logger.go b/pkg/v1/remote/transport/logger.go
new file mode 100644
index 0000000..c341f84
--- /dev/null
+++ b/pkg/v1/remote/transport/logger.go
@@ -0,0 +1,91 @@
+// Copyright 2020 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httputil"
+ "time"
+
+ "github.com/google/go-containerregistry/internal/redact"
+ "github.com/google/go-containerregistry/pkg/logs"
+)
+
+type logTransport struct {
+ inner http.RoundTripper
+}
+
+// NewLogger returns a transport that logs requests and responses to
+// github.com/google/go-containerregistry/pkg/logs.Debug.
+func NewLogger(inner http.RoundTripper) http.RoundTripper {
+ return &logTransport{inner}
+}
+
+func (t *logTransport) RoundTrip(in *http.Request) (out *http.Response, err error) {
+ // Inspired by: github.com/motemen/go-loghttp
+
+ // We redact token responses and binary blobs in response/request.
+ omitBody, reason := redact.FromContext(in.Context())
+ if omitBody {
+ logs.Debug.Printf("--> %s %s [body redacted: %s]", in.Method, in.URL, reason)
+ } else {
+ logs.Debug.Printf("--> %s %s", in.Method, in.URL)
+ }
+
+ // Save these headers so we can redact Authorization.
+ savedHeaders := in.Header.Clone()
+ if in.Header != nil && in.Header.Get("authorization") != "" {
+ in.Header.Set("authorization", "<redacted>")
+ }
+
+ b, err := httputil.DumpRequestOut(in, !omitBody)
+ if err == nil {
+ logs.Debug.Println(string(b))
+ } else {
+ logs.Debug.Printf("Failed to dump request %s %s: %v", in.Method, in.URL, err)
+ }
+
+ // Restore the non-redacted headers.
+ in.Header = savedHeaders
+
+ start := time.Now()
+ out, err = t.inner.RoundTrip(in)
+ duration := time.Since(start)
+ if err != nil {
+ logs.Debug.Printf("<-- %v %s %s (%s)", err, in.Method, in.URL, duration)
+ }
+ if out != nil {
+ msg := fmt.Sprintf("<-- %d", out.StatusCode)
+ if out.Request != nil {
+ msg = fmt.Sprintf("%s %s", msg, out.Request.URL)
+ }
+ msg = fmt.Sprintf("%s (%s)", msg, duration)
+
+ if omitBody {
+ msg = fmt.Sprintf("%s [body redacted: %s]", msg, reason)
+ }
+
+ logs.Debug.Print(msg)
+
+ b, err := httputil.DumpResponse(out, !omitBody)
+ if err == nil {
+ logs.Debug.Println(string(b))
+ } else {
+ logs.Debug.Printf("Failed to dump response %s %s: %v", in.Method, in.URL, err)
+ }
+ }
+ return
+}
diff --git a/pkg/v1/remote/transport/logger_test.go b/pkg/v1/remote/transport/logger_test.go
new file mode 100644
index 0000000..d5b57d3
--- /dev/null
+++ b/pkg/v1/remote/transport/logger_test.go
@@ -0,0 +1,93 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/redact"
+ "github.com/google/go-containerregistry/pkg/logs"
+)
+
+func TestLogger(t *testing.T) {
+ canary := "logs.Debug canary"
+ secret := "super secret do not log"
+ auth := "my token pls do not log"
+ reason := "should not log the secret"
+
+ ctx := redact.NewContext(context.Background(), reason)
+
+ req, err := http.NewRequestWithContext(ctx, "GET", "http://example.com", nil)
+ if err != nil {
+ t.Fatalf("Unexpected error during NewRequest: %v", err)
+ }
+ req.Header.Set("authorization", auth)
+
+ var b bytes.Buffer
+ logs.Debug.SetOutput(&b)
+ cannedResponse := http.Response{
+ Status: http.StatusText(http.StatusOK),
+ StatusCode: http.StatusOK,
+ Header: http.Header{
+ "Foo": []string{canary},
+ },
+ Body: io.NopCloser(strings.NewReader(secret)),
+ Request: req,
+ }
+ tr := NewLogger(newRecorder(&cannedResponse, nil))
+ if _, err := tr.RoundTrip(req); err != nil {
+ t.Fatalf("Unexpected error during RoundTrip: %v", err)
+ }
+
+ logged := b.String()
+ if !strings.Contains(logged, canary) {
+ t.Errorf("Expected logs to contain %s, got %s", canary, logged)
+ }
+ if !strings.Contains(logged, reason) {
+ t.Errorf("Expected logs to contain %s, got %s", canary, logged)
+ }
+ if strings.Contains(logged, secret) {
+ t.Errorf("Expected logs NOT to contain %s, got %s", secret, logged)
+ }
+ if strings.Contains(logged, auth) {
+ t.Errorf("Expected logs NOT to contain %s, got %s", auth, logged)
+ }
+}
+
+func TestLoggerError(t *testing.T) {
+ canary := "logs.Debug canary ERROR"
+ req, err := http.NewRequest("GET", "http://example.com", nil)
+ if err != nil {
+ t.Fatalf("Unexpected error during NewRequest: %v", err)
+ }
+
+ var b bytes.Buffer
+ logs.Debug.SetOutput(&b)
+ tr := NewLogger(newRecorder(nil, errors.New(canary)))
+ if _, err := tr.RoundTrip(req); err == nil {
+ t.Fatalf("Expected error during RoundTrip, got nil")
+ }
+
+ logged := b.String()
+ if !strings.Contains(logged, canary) {
+ t.Errorf("Expected logs to contain %s, got %s", canary, logged)
+ }
+}
diff --git a/pkg/v1/remote/transport/ping.go b/pkg/v1/remote/transport/ping.go
new file mode 100644
index 0000000..d852ef8
--- /dev/null
+++ b/pkg/v1/remote/transport/ping.go
@@ -0,0 +1,227 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+type challenge string
+
+const (
+ anonymous challenge = "anonymous"
+ basic challenge = "basic"
+ bearer challenge = "bearer"
+)
+
+// 300ms is the default fallback period for go's DNS dialer but we could make this configurable.
+var fallbackDelay = 300 * time.Millisecond
+
+type pingResp struct {
+ challenge challenge
+
+ // Following the challenge there are often key/value pairs
+ // e.g. Bearer service="gcr.io",realm="https://auth.gcr.io/v36/tokenz"
+ parameters map[string]string
+
+ // The registry's scheme to use. Communicates whether we fell back to http.
+ scheme string
+}
+
+func (c challenge) Canonical() challenge {
+ return challenge(strings.ToLower(string(c)))
+}
+
+func ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*pingResp, error) {
+ // This first attempts to use "https" for every request, falling back to http
+ // if the registry matches our localhost heuristic or if it is intentionally
+ // set to insecure via name.NewInsecureRegistry.
+ schemes := []string{"https"}
+ if reg.Scheme() == "http" {
+ schemes = append(schemes, "http")
+ }
+ if len(schemes) == 1 {
+ return pingSingle(ctx, reg, t, schemes[0])
+ }
+ return pingParallel(ctx, reg, t, schemes)
+}
+
+func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, scheme string) (*pingResp, error) {
+ client := http.Client{Transport: t}
+ url := fmt.Sprintf("%s://%s/v2/", scheme, reg.Name())
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := client.Do(req.WithContext(ctx))
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ // By draining the body, make sure to reuse the connection made by
+ // the ping for the following access to the registry
+ io.Copy(io.Discard, resp.Body)
+ resp.Body.Close()
+ }()
+
+ switch resp.StatusCode {
+ case http.StatusOK:
+ // If we get a 200, then no authentication is needed.
+ return &pingResp{
+ challenge: anonymous,
+ scheme: scheme,
+ }, nil
+ case http.StatusUnauthorized:
+ if challenges := authchallenge.ResponseChallenges(resp); len(challenges) != 0 {
+ // If we hit more than one, let's try to find one that we know how to handle.
+ wac := pickFromMultipleChallenges(challenges)
+ return &pingResp{
+ challenge: challenge(wac.Scheme).Canonical(),
+ parameters: wac.Parameters,
+ scheme: scheme,
+ }, nil
+ }
+ // Otherwise, just return the challenge without parameters.
+ return &pingResp{
+ challenge: challenge(resp.Header.Get("WWW-Authenticate")).Canonical(),
+ scheme: scheme,
+ }, nil
+ default:
+ return nil, CheckError(resp, http.StatusOK, http.StatusUnauthorized)
+ }
+}
+
+// Based on the golang happy eyeballs dialParallel impl in net/dial.go.
+func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, schemes []string) (*pingResp, error) {
+ returned := make(chan struct{})
+ defer close(returned)
+
+ type pingResult struct {
+ *pingResp
+ error
+ primary bool
+ done bool
+ }
+
+ results := make(chan pingResult)
+
+ startRacer := func(ctx context.Context, scheme string) {
+ pr, err := pingSingle(ctx, reg, t, scheme)
+ select {
+ case results <- pingResult{pingResp: pr, error: err, primary: scheme == "https", done: true}:
+ case <-returned:
+ if pr != nil {
+ logs.Debug.Printf("%s lost race", scheme)
+ }
+ }
+ }
+
+ var primary, fallback pingResult
+
+ primaryCtx, primaryCancel := context.WithCancel(ctx)
+ defer primaryCancel()
+ go startRacer(primaryCtx, schemes[0])
+
+ fallbackTimer := time.NewTimer(fallbackDelay)
+ defer fallbackTimer.Stop()
+
+ for {
+ select {
+ case <-fallbackTimer.C:
+ fallbackCtx, fallbackCancel := context.WithCancel(ctx)
+ defer fallbackCancel()
+ go startRacer(fallbackCtx, schemes[1])
+
+ case res := <-results:
+ if res.error == nil {
+ return res.pingResp, nil
+ }
+ if res.primary {
+ primary = res
+ } else {
+ fallback = res
+ }
+ if primary.done && fallback.done {
+ return nil, multierrs([]error{primary.error, fallback.error})
+ }
+ if res.primary && fallbackTimer.Stop() {
+ // Primary failed and we haven't started the fallback,
+ // reset time to start fallback immediately.
+ fallbackTimer.Reset(0)
+ }
+ }
+ }
+}
+
+func pickFromMultipleChallenges(challenges []authchallenge.Challenge) authchallenge.Challenge {
+ // It might happen there are multiple www-authenticate headers, e.g. `Negotiate` and `Basic`.
+ // Picking simply the first one could result eventually in `unrecognized challenge` error,
+ // that's why we're looping through the challenges in search for one that can be handled.
+ allowedSchemes := []string{"basic", "bearer"}
+
+ for _, wac := range challenges {
+ currentScheme := strings.ToLower(wac.Scheme)
+ for _, allowed := range allowedSchemes {
+ if allowed == currentScheme {
+ return wac
+ }
+ }
+ }
+
+ return challenges[0]
+}
+
+type multierrs []error
+
+func (m multierrs) Error() string {
+ var b strings.Builder
+ hasWritten := false
+ for _, err := range m {
+ if hasWritten {
+ b.WriteString("; ")
+ }
+ hasWritten = true
+ b.WriteString(err.Error())
+ }
+ return b.String()
+}
+
+func (m multierrs) As(target any) bool {
+ for _, err := range m {
+ if errors.As(err, target) {
+ return true
+ }
+ }
+ return false
+}
+
+func (m multierrs) Is(target error) bool {
+ for _, err := range m {
+ if errors.Is(err, target) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/pkg/v1/remote/transport/ping_test.go b/pkg/v1/remote/transport/ping_test.go
new file mode 100644
index 0000000..c2ad119
--- /dev/null
+++ b/pkg/v1/remote/transport/ping_test.go
@@ -0,0 +1,260 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+var (
+ testRegistry, _ = name.NewRegistry("localhost:8080", name.StrictValidation)
+)
+
+func TestPingNoChallenge(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ pr, err := ping(context.Background(), testRegistry, tprt)
+ if err != nil {
+ t.Errorf("ping() = %v", err)
+ }
+ if pr.challenge != anonymous {
+ t.Errorf("ping(); got %v, want %v", pr.challenge, anonymous)
+ }
+ if pr.scheme != "http" {
+ t.Errorf("ping(); got %v, want %v", pr.scheme, "http")
+ }
+}
+
+func TestPingBasicChallengeNoParams(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("WWW-Authenticate", `BASIC`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ pr, err := ping(context.Background(), testRegistry, tprt)
+ if err != nil {
+ t.Errorf("ping() = %v", err)
+ }
+ if pr.challenge != basic {
+ t.Errorf("ping(); got %v, want %v", pr.challenge, basic)
+ }
+ if got, want := len(pr.parameters), 0; got != want {
+ t.Errorf("ping(); got %v, want %v", got, want)
+ }
+}
+
+func TestPingBearerChallengeWithParams(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("WWW-Authenticate", `Bearer realm="http://auth.example.com/token"`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ pr, err := ping(context.Background(), testRegistry, tprt)
+ if err != nil {
+ t.Errorf("ping() = %v", err)
+ }
+ if pr.challenge != bearer {
+ t.Errorf("ping(); got %v, want %v", pr.challenge, bearer)
+ }
+ if got, want := len(pr.parameters), 1; got != want {
+ t.Errorf("ping(); got %v, want %v", got, want)
+ }
+}
+
+func TestPingMultipleChallenges(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("WWW-Authenticate", "Negotiate")
+ w.Header().Add("WWW-Authenticate", `Basic realm="http://auth.example.com/token"`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ pr, err := ping(context.Background(), testRegistry, tprt)
+ if err != nil {
+ t.Errorf("ping() = %v", err)
+ }
+ if pr.challenge != basic {
+ t.Errorf("ping(); got %v, want %v", pr.challenge, basic)
+ }
+ if got, want := len(pr.parameters), 1; got != want {
+ t.Errorf("ping(); got %v, want %v", got, want)
+ }
+}
+
+func TestPingMultipleNotSupportedChallenges(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("WWW-Authenticate", "Negotiate")
+ w.Header().Add("WWW-Authenticate", "Digest")
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ pr, err := ping(context.Background(), testRegistry, tprt)
+ if err != nil {
+ t.Errorf("ping() = %v", err)
+ }
+ if pr.challenge != "negotiate" {
+ t.Errorf("ping(); got %v, want %v", pr.challenge, "negotiate")
+ }
+}
+
+func TestUnsupportedStatus(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("WWW-Authenticate", `Bearer realm="http://auth.example.com/token`)
+ http.Error(w, "Forbidden", http.StatusForbidden)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ pr, err := ping(context.Background(), testRegistry, tprt)
+ if err == nil {
+ t.Errorf("ping() = %v", pr)
+ }
+}
+
+func TestPingHttpFallback(t *testing.T) {
+ tests := []struct {
+ reg name.Registry
+ wantCount int64
+ err string
+ contains []string
+ }{{
+ reg: mustRegistry("gcr.io"),
+ wantCount: 1,
+ err: `Get "https://gcr.io/v2/": http: server gave HTTP response to HTTPS client`,
+ }, {
+ reg: mustRegistry("ko.local"),
+ wantCount: 2,
+ }, {
+ reg: mustInsecureRegistry("us.gcr.io"),
+ wantCount: 0,
+ contains: []string{"https://us.gcr.io/v2/", "http://us.gcr.io/v2/"},
+ }}
+
+ gotCount := int64(0)
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ atomic.AddInt64(&gotCount, 1)
+ if r.URL.Scheme != "http" {
+ // Sleep a little bit so we can exercise the
+ // happy eyeballs race.
+ time.Sleep(5 * time.Millisecond)
+ }
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer server.Close()
+
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ fallbackDelay = 2 * time.Millisecond
+
+ for _, test := range tests {
+ // This is the last one, fatal error it.
+ if strings.Contains(test.reg.String(), "us.gcr.io") {
+ server.Close()
+ }
+
+ _, err := ping(context.Background(), test.reg, tprt)
+ if got, want := gotCount, test.wantCount; got != want {
+ t.Errorf("%s: got %d requests, wanted %d", test.reg.String(), got, want)
+ }
+ gotCount = 0
+
+ if err == nil {
+ if test.err != "" {
+ t.Error("expected err, got nil")
+ }
+ continue
+ }
+ if len(test.contains) != 0 {
+ for _, c := range test.contains {
+ if !strings.Contains(err.Error(), c) {
+ t.Errorf("expected err to contain %q but did not: %q", c, err)
+ }
+ }
+ } else if got, want := err.Error(), test.err; got != want {
+ t.Errorf("got %q want %q", got, want)
+ }
+ }
+}
+
+func mustRegistry(r string) name.Registry {
+ reg, err := name.NewRegistry(r)
+ if err != nil {
+ panic(err)
+ }
+ return reg
+}
+
+func mustInsecureRegistry(r string) name.Registry {
+ reg, err := name.NewRegistry(r, name.Insecure)
+ if err != nil {
+ panic(err)
+ }
+ return reg
+}
diff --git a/pkg/v1/remote/transport/retry.go b/pkg/v1/remote/transport/retry.go
new file mode 100644
index 0000000..e5621e3
--- /dev/null
+++ b/pkg/v1/remote/transport/retry.go
@@ -0,0 +1,111 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/google/go-containerregistry/internal/retry"
+)
+
+// Sleep for 0.1 then 0.3 seconds. This should cover networking blips.
+var defaultBackoff = retry.Backoff{
+ Duration: 100 * time.Millisecond,
+ Factor: 3.0,
+ Jitter: 0.1,
+ Steps: 3,
+}
+
+var _ http.RoundTripper = (*retryTransport)(nil)
+
+// retryTransport wraps a RoundTripper and retries temporary network errors.
+type retryTransport struct {
+ inner http.RoundTripper
+ backoff retry.Backoff
+ predicate retry.Predicate
+ codes []int
+}
+
+// Option is a functional option for retryTransport.
+type Option func(*options)
+
+type options struct {
+ backoff retry.Backoff
+ predicate retry.Predicate
+ codes []int
+}
+
+// Backoff is an alias of retry.Backoff to expose this configuration option to consumers of this lib
+type Backoff = retry.Backoff
+
+// WithRetryBackoff sets the backoff for retry operations.
+func WithRetryBackoff(backoff Backoff) Option {
+ return func(o *options) {
+ o.backoff = backoff
+ }
+}
+
+// WithRetryPredicate sets the predicate for retry operations.
+func WithRetryPredicate(predicate func(error) bool) Option {
+ return func(o *options) {
+ o.predicate = predicate
+ }
+}
+
+// WithRetryStatusCodes sets which http response codes will be retried.
+func WithRetryStatusCodes(codes ...int) Option {
+ return func(o *options) {
+ o.codes = codes
+ }
+}
+
+// NewRetry returns a transport that retries errors.
+func NewRetry(inner http.RoundTripper, opts ...Option) http.RoundTripper {
+ o := &options{
+ backoff: defaultBackoff,
+ predicate: retry.IsTemporary,
+ }
+
+ for _, opt := range opts {
+ opt(o)
+ }
+
+ return &retryTransport{
+ inner: inner,
+ backoff: o.backoff,
+ predicate: o.predicate,
+ codes: o.codes,
+ }
+}
+
+func (t *retryTransport) RoundTrip(in *http.Request) (out *http.Response, err error) {
+ roundtrip := func() error {
+ out, err = t.inner.RoundTrip(in)
+ if !retry.Ever(in.Context()) {
+ return nil
+ }
+ if out != nil {
+ for _, code := range t.codes {
+ if out.StatusCode == code {
+ return CheckError(out)
+ }
+ }
+ }
+ return err
+ }
+ retry.Retry(roundtrip, t.predicate, t.backoff)
+ return
+}
diff --git a/pkg/v1/remote/transport/retry_test.go b/pkg/v1/remote/transport/retry_test.go
new file mode 100644
index 0000000..ded0ce0
--- /dev/null
+++ b/pkg/v1/remote/transport/retry_test.go
@@ -0,0 +1,177 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "context"
+ "errors"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/go-containerregistry/internal/retry"
+)
+
+type mockTransport struct {
+ errs []error
+ resps []*http.Response
+ count int
+}
+
+func (t *mockTransport) RoundTrip(in *http.Request) (out *http.Response, err error) {
+ defer func() { t.count++ }()
+ if t.count < len(t.resps) {
+ out = t.resps[t.count]
+ }
+ if t.count < len(t.errs) {
+ err = t.errs[t.count]
+ }
+ return
+}
+
+type perm struct{}
+
+func (e perm) Error() string {
+ return "permanent error"
+}
+
+type temp struct{}
+
+func (e temp) Error() string {
+ return "temporary error"
+}
+
+func (e temp) Temporary() bool {
+ return true
+}
+
+func resp(code int) *http.Response {
+ return &http.Response{
+ StatusCode: code,
+ Body: io.NopCloser(strings.NewReader("hi")),
+ }
+}
+
+func TestRetryTransport(t *testing.T) {
+ for _, test := range []struct {
+ errs []error
+ resps []*http.Response
+ ctx context.Context
+ count int
+ }{{
+ // Don't retry retry.Never.
+ errs: []error{temp{}},
+ ctx: retry.Never(context.Background()),
+ count: 1,
+ }, {
+ // Don't retry permanent.
+ errs: []error{perm{}},
+ count: 1,
+ }, {
+ // Do retry temp.
+ errs: []error{temp{}, perm{}},
+ count: 2,
+ }, {
+ // Stop at some max.
+ errs: []error{temp{}, temp{}, temp{}, temp{}, temp{}},
+ count: 3,
+ }, {
+ // Retry http errors.
+ errs: []error{nil, nil, temp{}, temp{}, temp{}},
+ resps: []*http.Response{
+ resp(http.StatusRequestTimeout),
+ resp(http.StatusInternalServerError),
+ nil,
+ },
+ count: 3,
+ }} {
+ mt := mockTransport{
+ errs: test.errs,
+ resps: test.resps,
+ }
+
+ tr := NewRetry(&mt,
+ WithRetryBackoff(retry.Backoff{Steps: 3}),
+ WithRetryPredicate(retry.IsTemporary),
+ WithRetryStatusCodes(http.StatusRequestTimeout, http.StatusInternalServerError),
+ )
+
+ ctx := context.Background()
+ if test.ctx != nil {
+ ctx = test.ctx
+ }
+ req, err := http.NewRequestWithContext(ctx, "GET", "example.com", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tr.RoundTrip(req)
+ if mt.count != test.count {
+ t.Errorf("wrong count, wanted %d, got %d", test.count, mt.count)
+ }
+ }
+}
+
+func TestRetryDefaults(t *testing.T) {
+ tr := NewRetry(http.DefaultTransport)
+ rt, ok := tr.(*retryTransport)
+ if !ok {
+ t.Fatal("could not cast to retryTransport")
+ }
+
+ if rt.backoff != defaultBackoff {
+ t.Fatalf("default backoff wrong: %v", rt.backoff)
+ }
+
+ if rt.predicate == nil {
+ t.Fatal("default predicate not set")
+ }
+}
+
+func TestTimeoutContext(t *testing.T) {
+ tr := NewRetry(http.DefaultTransport)
+
+ slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // hanging request
+ time.Sleep(time.Second * 1)
+ }))
+ defer func() { go func() { slowServer.Close() }() }()
+
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*20))
+ defer cancel()
+ req, err := http.NewRequest("GET", slowServer.URL, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ req = req.WithContext(ctx)
+
+ result := make(chan error)
+
+ go func() {
+ _, err := tr.RoundTrip(req)
+ result <- err
+ }()
+
+ select {
+ case err := <-result:
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("got: %v, want: %v", err, context.DeadlineExceeded)
+ }
+ case <-time.After(time.Millisecond * 100):
+ t.Fatalf("deadline was not recognized by transport")
+ }
+}
diff --git a/pkg/v1/remote/transport/schemer.go b/pkg/v1/remote/transport/schemer.go
new file mode 100644
index 0000000..d70b6a8
--- /dev/null
+++ b/pkg/v1/remote/transport/schemer.go
@@ -0,0 +1,44 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "net/http"
+
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+type schemeTransport struct {
+ // Scheme we should use, determined by ping response.
+ scheme string
+
+ // Registry we're talking to.
+ registry name.Registry
+
+ // Wrapped by schemeTransport.
+ inner http.RoundTripper
+}
+
+// RoundTrip implements http.RoundTripper
+func (st *schemeTransport) RoundTrip(in *http.Request) (*http.Response, error) {
+ // When we ping() the registry, we determine whether to use http or https
+ // based on which scheme was successful. That is only valid for the
+ // registry server and not e.g. a separate token server or blob storage,
+ // so we should only override the scheme if the host is the registry.
+ if matchesHost(st.registry, in, st.scheme) {
+ in.URL.Scheme = st.scheme
+ }
+ return st.inner.RoundTrip(in)
+}
diff --git a/pkg/v1/remote/transport/scope.go b/pkg/v1/remote/transport/scope.go
new file mode 100644
index 0000000..c3b56f7
--- /dev/null
+++ b/pkg/v1/remote/transport/scope.go
@@ -0,0 +1,24 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+// Scopes suitable to qualify each Repository
+const (
+ PullScope string = "pull"
+ PushScope string = "push,pull"
+ // For now DELETE is PUSH, which is the read/write ACL.
+ DeleteScope string = PushScope
+ CatalogScope string = "catalog"
+)
diff --git a/pkg/v1/remote/transport/transport.go b/pkg/v1/remote/transport/transport.go
new file mode 100644
index 0000000..01fe1fa
--- /dev/null
+++ b/pkg/v1/remote/transport/transport.go
@@ -0,0 +1,116 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+// New returns a new RoundTripper based on the provided RoundTripper that has been
+// setup to authenticate with the remote registry "reg", in the capacity
+// laid out by the specified scopes.
+//
+// Deprecated: Use NewWithContext.
+func New(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string) (http.RoundTripper, error) {
+ return NewWithContext(context.Background(), reg, auth, t, scopes)
+}
+
+// NewWithContext returns a new RoundTripper based on the provided RoundTripper that has been
+// set up to authenticate with the remote registry "reg", in the capacity
+// laid out by the specified scopes.
+// In case the RoundTripper is already of the type Wrapper it assumes
+// authentication was already done prior to this call, so it just returns
+// the provided RoundTripper without further action
+func NewWithContext(ctx context.Context, reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string) (http.RoundTripper, error) {
+ // When the transport provided is of the type Wrapper this function assumes that the caller already
+ // executed the necessary login and check.
+ switch t.(type) {
+ case *Wrapper:
+ return t, nil
+ }
+ // The handshake:
+ // 1. Use "t" to ping() the registry for the authentication challenge.
+ //
+ // 2a. If we get back a 200, then simply use "t".
+ //
+ // 2b. If we get back a 401 with a Basic challenge, then use a transport
+ // that just attachs auth each roundtrip.
+ //
+ // 2c. If we get back a 401 with a Bearer challenge, then use a transport
+ // that attaches a bearer token to each request, and refreshes is on 401s.
+ // Perform an initial refresh to seed the bearer token.
+
+ // First we ping the registry to determine the parameters of the authentication handshake
+ // (if one is even necessary).
+ pr, err := ping(ctx, reg, t)
+ if err != nil {
+ return nil, err
+ }
+
+ // Wrap t with a useragent transport unless we already have one.
+ if _, ok := t.(*userAgentTransport); !ok {
+ t = NewUserAgent(t, "")
+ }
+
+ // Wrap t in a transport that selects the appropriate scheme based on the ping response.
+ t = &schemeTransport{
+ scheme: pr.scheme,
+ registry: reg,
+ inner: t,
+ }
+
+ switch pr.challenge.Canonical() {
+ case anonymous, basic:
+ return &Wrapper{&basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}}, nil
+ case bearer:
+ // We require the realm, which tells us where to send our Basic auth to turn it into Bearer auth.
+ realm, ok := pr.parameters["realm"]
+ if !ok {
+ return nil, fmt.Errorf("malformed www-authenticate, missing realm: %v", pr.parameters)
+ }
+ service := pr.parameters["service"]
+ bt := &bearerTransport{
+ inner: t,
+ basic: auth,
+ realm: realm,
+ registry: reg,
+ service: service,
+ scopes: scopes,
+ scheme: pr.scheme,
+ }
+ if err := bt.refresh(ctx); err != nil {
+ return nil, err
+ }
+ return &Wrapper{bt}, nil
+ default:
+ return nil, fmt.Errorf("unrecognized challenge: %s", pr.challenge)
+ }
+}
+
+// Wrapper results in *not* wrapping supplied transport with additional logic such as retries, useragent and debug logging
+// Consumers are opt-ing into providing their own transport without any additional wrapping.
+type Wrapper struct {
+ inner http.RoundTripper
+}
+
+// RoundTrip delegates to the inner RoundTripper
+func (w *Wrapper) RoundTrip(in *http.Request) (*http.Response, error) {
+ return w.inner.RoundTrip(in)
+}
diff --git a/pkg/v1/remote/transport/transport_test.go b/pkg/v1/remote/transport/transport_test.go
new file mode 100644
index 0000000..10389b7
--- /dev/null
+++ b/pkg/v1/remote/transport/transport_test.go
@@ -0,0 +1,282 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+var (
+ testReference, _ = name.NewTag("localhost:8080/user/image:latest", name.StrictValidation)
+)
+
+func TestTransportNoActionIfTransportIsAlreadyWrapper(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("WWW-Authenticate", `Bearer realm="http://foo.io"`)
+ http.Error(w, "Should not contact the server", http.StatusBadRequest)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ wTprt := &Wrapper{inner: tprt}
+
+ if _, err := NewWithContext(context.Background(), testReference.Context().Registry, nil, wTprt, []string{testReference.Scope(PullScope)}); err != nil {
+ t.Errorf("NewWithContext unexpected error %s", err)
+ }
+}
+
+func TestTransportSelectionAnonymous(t *testing.T) {
+ // Record the requests we get in the inner transport.
+ cannedResponse := http.Response{
+ Status: http.StatusText(http.StatusOK),
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(strings.NewReader("")),
+ }
+ recorder := newRecorder(&cannedResponse, nil)
+
+ basic := &authn.Basic{Username: "foo", Password: "bar"}
+ reg := testReference.Context().Registry
+
+ tp, err := NewWithContext(context.Background(), reg, basic, recorder, []string{testReference.Scope(PullScope)})
+ if err != nil {
+ t.Errorf("NewWithContext() = %v", err)
+ }
+
+ req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v2/anything", reg), nil)
+ if err != nil {
+ t.Fatalf("Unexpected error during NewRequest: %v", err)
+ }
+ if _, err := tp.RoundTrip(req); err != nil {
+ t.Fatalf("Unexpected error during RoundTrip: %v", err)
+ }
+
+ if got, want := len(recorder.reqs), 2; got != want {
+ t.Fatalf("expected %d requests, got %d", want, got)
+ }
+ recorded := recorder.reqs[1]
+ if got, want := recorded.URL.Scheme, "https"; got != want {
+ t.Errorf("wrong scheme, want %s got %s", want, got)
+ }
+}
+
+func TestTransportSelectionBasic(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("WWW-Authenticate", `Basic`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ basic := &authn.Basic{Username: "foo", Password: "bar"}
+
+ tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)})
+ if err != nil {
+ t.Errorf("NewWithContext() = %v", err)
+ }
+ if tpw, ok := tp.(*Wrapper); !ok {
+ t.Errorf("NewWithContext(); got %T, want *Wrapper", tp)
+ } else if _, ok := tpw.inner.(*basicTransport); !ok {
+ t.Errorf("NewWithContext(); got %T, want *basicTransport", tp)
+ }
+}
+
+type badAuth struct{}
+
+func (a *badAuth) Authorization() (*authn.AuthConfig, error) {
+ return nil, errors.New("sorry dave, I'm afraid I can't let you do that")
+}
+
+func TestTransportBadAuth(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("WWW-Authenticate", `Bearer realm="http://foo.io"`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ if _, err := NewWithContext(context.Background(), testReference.Context().Registry, &badAuth{}, tprt, []string{testReference.Scope(PullScope)}); err == nil {
+ t.Errorf("NewWithContext() expected err, got nil")
+ }
+}
+
+func TestTransportSelectionBearer(t *testing.T) {
+ request := 0
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ request++
+ switch request {
+ case 1:
+ // This is an https request that fails, causing us to fall back to http.
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ case 2:
+ w.Header().Set("WWW-Authenticate", `Bearer realm="http://foo.io"`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ case 3:
+ hdr := r.Header.Get("Authorization")
+ if !strings.HasPrefix(hdr, "Basic ") {
+ t.Errorf("Header.Get(Authorization); got %v, want Basic prefix", hdr)
+ }
+ if got, want := r.FormValue("scope"), testReference.Scope(PullScope); got != want {
+ t.Errorf("FormValue(scope); got %v, want %v", got, want)
+ }
+ // Check that the service isn't set (we didn't specify it above)
+ // https://github.com/google/go-containerregistry/issues/1359
+ if got, want := r.FormValue("service"), ""; got != want {
+ t.Errorf("FormValue(service); got %q, want %q", got, want)
+ }
+ w.Write([]byte(`{"token": "dfskdjhfkhsjdhfkjhsdf"}`))
+ }
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ basic := &authn.Basic{Username: "foo", Password: "bar"}
+ tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)})
+ if err != nil {
+ t.Errorf("NewWithContext() = %v", err)
+ }
+ if tpw, ok := tp.(*Wrapper); !ok {
+ t.Errorf("NewWithContext(); got %T, want *Wrapper", tp)
+ } else if _, ok := tpw.inner.(*bearerTransport); !ok {
+ t.Errorf("NewWithContext(); got %T, want *bearerTransport", tp)
+ }
+}
+
+func TestTransportSelectionBearerMissingRealm(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("WWW-Authenticate", `Bearer service="gcr.io"`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ basic := &authn.Basic{Username: "foo", Password: "bar"}
+ tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)})
+ if err == nil || !strings.Contains(err.Error(), "missing realm") {
+ t.Errorf("NewWithContext() = %v, %v", tp, err)
+ }
+}
+
+func TestTransportSelectionBearerAuthError(t *testing.T) {
+ request := 0
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ request++
+ switch request {
+ case 1:
+ w.Header().Set("WWW-Authenticate", `Bearer realm="http://foo.io"`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ case 2:
+ http.Error(w, "Oops", http.StatusInternalServerError)
+ }
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ basic := &authn.Basic{Username: "foo", Password: "bar"}
+ tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)})
+ if err == nil {
+ t.Errorf("NewWithContext() = %v", tp)
+ }
+}
+
+func TestTransportSelectionUnrecognizedChallenge(t *testing.T) {
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("WWW-Authenticate", `Unrecognized`)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ }))
+ defer server.Close()
+ tprt := &http.Transport{
+ Proxy: func(req *http.Request) (*url.URL, error) {
+ return url.Parse(server.URL)
+ },
+ }
+
+ basic := &authn.Basic{Username: "foo", Password: "bar"}
+ tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)})
+ if err == nil || !strings.Contains(err.Error(), "challenge") {
+ t.Errorf("NewWithContext() = %v, %v", tp, err)
+ }
+}
+
+func TestTransportAlwaysTriesHttps(t *testing.T) {
+ // Use a NewTLSServer so that this speaks TLS even though it's localhost.
+ // This ensures that we try https even for local registries.
+ count := 0
+ server := httptest.NewTLSServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ count++
+ w.Write([]byte(`{"token": "dfskdjhfkhsjdhfkjhsdf"}`))
+ }))
+ defer server.Close()
+
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Errorf("Unexpected error during url.Parse: %v", err)
+ }
+ registry, err := name.NewRegistry(u.Host, name.WeakValidation)
+ if err != nil {
+ t.Errorf("Unexpected error during NewRegistry: %v", err)
+ }
+
+ basic := &authn.Basic{Username: "foo", Password: "bar"}
+ tp, err := NewWithContext(context.Background(), registry, basic, server.Client().Transport, []string{testReference.Scope(PullScope)})
+ if err != nil {
+ t.Fatalf("NewWithContext() = %v, %v", tp, err)
+ }
+ if count == 0 {
+ t.Errorf("failed to call TLS localhost server")
+ }
+}
diff --git a/pkg/v1/remote/transport/useragent.go b/pkg/v1/remote/transport/useragent.go
new file mode 100644
index 0000000..74a9e71
--- /dev/null
+++ b/pkg/v1/remote/transport/useragent.go
@@ -0,0 +1,94 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package transport
+
+import (
+ "fmt"
+ "net/http"
+ "runtime/debug"
+)
+
+var (
+ // Version can be set via:
+ // -ldflags="-X 'github.com/google/go-containerregistry/pkg/v1/remote/transport.Version=$TAG'"
+ Version string
+
+ ggcrVersion = defaultUserAgent
+)
+
+const (
+ defaultUserAgent = "go-containerregistry"
+ moduleName = "github.com/google/go-containerregistry"
+)
+
+type userAgentTransport struct {
+ inner http.RoundTripper
+ ua string
+}
+
+func init() {
+ if v := version(); v != "" {
+ ggcrVersion = fmt.Sprintf("%s/%s", defaultUserAgent, v)
+ }
+}
+
+func version() string {
+ if Version != "" {
+ // Version was set via ldflags, just return it.
+ return Version
+ }
+
+ info, ok := debug.ReadBuildInfo()
+ if !ok {
+ return ""
+ }
+
+ // Happens for crane and gcrane.
+ if info.Main.Path == moduleName {
+ return info.Main.Version
+ }
+
+ // Anything else.
+ for _, dep := range info.Deps {
+ if dep.Path == moduleName {
+ return dep.Version
+ }
+ }
+
+ return ""
+}
+
+// NewUserAgent returns an http.Roundtripper that sets the user agent to
+// The provided string plus additional go-containerregistry information,
+// e.g. if provided "crane/v0.1.4" and this modules was built at v0.1.4:
+//
+// User-Agent: crane/v0.1.4 go-containerregistry/v0.1.4
+func NewUserAgent(inner http.RoundTripper, ua string) http.RoundTripper {
+ if ua == "" {
+ ua = ggcrVersion
+ } else {
+ ua = fmt.Sprintf("%s %s", ua, ggcrVersion)
+ }
+ return &userAgentTransport{
+ inner: inner,
+ ua: ua,
+ }
+}
+
+// RoundTrip implements http.RoundTripper
+func (ut *userAgentTransport) RoundTrip(in *http.Request) (*http.Response, error) {
+ in.Header.Set("User-Agent", ut.ua)
+ return ut.inner.RoundTrip(in)
+}
diff --git a/pkg/v1/remote/write.go b/pkg/v1/remote/write.go
new file mode 100644
index 0000000..5dbaa7c
--- /dev/null
+++ b/pkg/v1/remote/write.go
@@ -0,0 +1,1003 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "sort"
+ "strings"
+
+ "github.com/google/go-containerregistry/internal/redact"
+ "github.com/google/go-containerregistry/internal/retry"
+ "github.com/google/go-containerregistry/pkg/logs"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "golang.org/x/sync/errgroup"
+)
+
+// Taggable is an interface that enables a manifest PUT (e.g. for tagging).
+type Taggable interface {
+ RawManifest() ([]byte, error)
+}
+
+// Write pushes the provided img to the specified image reference.
+func Write(ref name.Reference, img v1.Image, options ...Option) (rerr error) {
+ o, err := makeOptions(ref.Context(), options...)
+ if err != nil {
+ return err
+ }
+
+ var p *progress
+ if o.updates != nil {
+ p = &progress{updates: o.updates}
+ p.lastUpdate = &v1.Update{}
+ p.lastUpdate.Total, err = countImage(img, o.allowNondistributableArtifacts)
+ if err != nil {
+ return err
+ }
+ defer close(o.updates)
+ defer func() { _ = p.err(rerr) }()
+ }
+ return writeImage(o.context, ref, img, o, p)
+}
+
+func writeImage(ctx context.Context, ref name.Reference, img v1.Image, o *options, progress *progress) error {
+ ls, err := img.Layers()
+ if err != nil {
+ return err
+ }
+ scopes := scopesForUploadingImage(ref.Context(), ls)
+ tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
+ if err != nil {
+ return err
+ }
+ w := writer{
+ repo: ref.Context(),
+ client: &http.Client{Transport: tr},
+ progress: progress,
+ backoff: o.retryBackoff,
+ predicate: o.retryPredicate,
+ }
+
+ // Upload individual blobs and collect any errors.
+ blobChan := make(chan v1.Layer, 2*o.jobs)
+ g, gctx := errgroup.WithContext(ctx)
+ for i := 0; i < o.jobs; i++ {
+ // Start N workers consuming blobs to upload.
+ g.Go(func() error {
+ for b := range blobChan {
+ if err := w.uploadOne(gctx, b); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ }
+
+ // Upload individual layers in goroutines and collect any errors.
+ // If we can dedupe by the layer digest, try to do so. If we can't determine
+ // the digest for whatever reason, we can't dedupe and might re-upload.
+ g.Go(func() error {
+ defer close(blobChan)
+ uploaded := map[v1.Hash]bool{}
+ for _, l := range ls {
+ l := l
+
+ // Handle foreign layers.
+ mt, err := l.MediaType()
+ if err != nil {
+ return err
+ }
+ if !mt.IsDistributable() && !o.allowNondistributableArtifacts {
+ continue
+ }
+
+ // Streaming layers calculate their digests while uploading them. Assume
+ // an error here indicates we need to upload the layer.
+ h, err := l.Digest()
+ if err == nil {
+ // If we can determine the layer's digest ahead of
+ // time, use it to dedupe uploads.
+ if uploaded[h] {
+ continue // Already uploading.
+ }
+ uploaded[h] = true
+ }
+ select {
+ case blobChan <- l:
+ case <-gctx.Done():
+ return gctx.Err()
+ }
+ }
+ return nil
+ })
+
+ if l, err := partial.ConfigLayer(img); err != nil {
+ // We can't read the ConfigLayer, possibly because of streaming layers,
+ // since the layer DiffIDs haven't been calculated yet. Attempt to wait
+ // for the other layers to be uploaded, then try the config again.
+ if err := g.Wait(); err != nil {
+ return err
+ }
+
+ // Now that all the layers are uploaded, try to upload the config file blob.
+ l, err := partial.ConfigLayer(img)
+ if err != nil {
+ return err
+ }
+ if err := w.uploadOne(ctx, l); err != nil {
+ return err
+ }
+ } else {
+ // We *can* read the ConfigLayer, so upload it concurrently with the layers.
+ g.Go(func() error {
+ return w.uploadOne(gctx, l)
+ })
+
+ // Wait for the layers + config.
+ if err := g.Wait(); err != nil {
+ return err
+ }
+ }
+
+ // With all of the constituent elements uploaded, upload the manifest
+ // to commit the image.
+ return w.commitManifest(ctx, img, ref)
+}
+
+// writer writes the elements of an image to a remote image reference.
+type writer struct {
+ repo name.Repository
+ client *http.Client
+
+ progress *progress
+ backoff Backoff
+ predicate retry.Predicate
+}
+
+// url returns a url.Url for the specified path in the context of this remote image reference.
+func (w *writer) url(path string) url.URL {
+ return url.URL{
+ Scheme: w.repo.Registry.Scheme(),
+ Host: w.repo.RegistryStr(),
+ Path: path,
+ }
+}
+
+// nextLocation extracts the fully-qualified URL to which we should send the next request in an upload sequence.
+func (w *writer) nextLocation(resp *http.Response) (string, error) {
+ loc := resp.Header.Get("Location")
+ if len(loc) == 0 {
+ return "", errors.New("missing Location header")
+ }
+ u, err := url.Parse(loc)
+ if err != nil {
+ return "", err
+ }
+
+ // If the location header returned is just a url path, then fully qualify it.
+ // We cannot simply call w.url, since there might be an embedded query string.
+ return resp.Request.URL.ResolveReference(u).String(), nil
+}
+
+// checkExistingBlob checks if a blob exists already in the repository by making a
+// HEAD request to the blob store API. GCR performs an existence check on the
+// initiation if "mount" is specified, even if no "from" sources are specified.
+// However, this is not broadly applicable to all registries, e.g. ECR.
+func (w *writer) checkExistingBlob(ctx context.Context, h v1.Hash) (bool, error) {
+ u := w.url(fmt.Sprintf("/v2/%s/blobs/%s", w.repo.RepositoryStr(), h.String()))
+
+ req, err := http.NewRequest(http.MethodHead, u.String(), nil)
+ if err != nil {
+ return false, err
+ }
+
+ resp, err := w.client.Do(req.WithContext(ctx))
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil {
+ return false, err
+ }
+
+ return resp.StatusCode == http.StatusOK, nil
+}
+
+// checkExistingManifest checks if a manifest exists already in the repository
+// by making a HEAD request to the manifest API.
+func (w *writer) checkExistingManifest(ctx context.Context, h v1.Hash, mt types.MediaType) (bool, error) {
+ u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), h.String()))
+
+ req, err := http.NewRequest(http.MethodHead, u.String(), nil)
+ if err != nil {
+ return false, err
+ }
+ req.Header.Set("Accept", string(mt))
+
+ resp, err := w.client.Do(req.WithContext(ctx))
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil {
+ return false, err
+ }
+
+ return resp.StatusCode == http.StatusOK, nil
+}
+
+// initiateUpload initiates the blob upload, which starts with a POST that can
+// optionally include the hash of the layer and a list of repositories from
+// which that layer might be read. On failure, an error is returned.
+// On success, the layer was either mounted (nothing more to do) or a blob
+// upload was initiated and the body of that blob should be sent to the returned
+// location.
+func (w *writer) initiateUpload(ctx context.Context, from, mount, origin string) (location string, mounted bool, err error) {
+ u := w.url(fmt.Sprintf("/v2/%s/blobs/uploads/", w.repo.RepositoryStr()))
+ uv := url.Values{}
+ if mount != "" && from != "" {
+ // Quay will fail if we specify a "mount" without a "from".
+ uv.Set("mount", mount)
+ uv.Set("from", from)
+ if origin != "" {
+ uv.Set("origin", origin)
+ }
+ }
+ u.RawQuery = uv.Encode()
+
+ // Make the request to initiate the blob upload.
+ req, err := http.NewRequest(http.MethodPost, u.String(), nil)
+ if err != nil {
+ return "", false, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := w.client.Do(req.WithContext(ctx))
+ if err != nil {
+ return "", false, err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusCreated, http.StatusAccepted); err != nil {
+ if origin != "" && origin != w.repo.RegistryStr() {
+ // https://github.com/google/go-containerregistry/issues/1404
+ logs.Warn.Printf("retrying without mount: %v", err)
+ return w.initiateUpload(ctx, "", "", "")
+ }
+ return "", false, err
+ }
+
+ // Check the response code to determine the result.
+ switch resp.StatusCode {
+ case http.StatusCreated:
+ // We're done, we were able to fast-path.
+ return "", true, nil
+ case http.StatusAccepted:
+ // Proceed to PATCH, upload has begun.
+ loc, err := w.nextLocation(resp)
+ return loc, false, err
+ default:
+ panic("Unreachable: initiateUpload")
+ }
+}
+
+// streamBlob streams the contents of the blob to the specified location.
+// On failure, this will return an error. On success, this will return the location
+// header indicating how to commit the streamed blob.
+func (w *writer) streamBlob(ctx context.Context, layer v1.Layer, streamLocation string) (commitLocation string, rerr error) {
+ reset := func() {}
+ defer func() {
+ if rerr != nil {
+ reset()
+ }
+ }()
+ blob, err := layer.Compressed()
+ if err != nil {
+ return "", err
+ }
+
+ getBody := layer.Compressed
+ if w.progress != nil {
+ var count int64
+ blob = &progressReader{rc: blob, progress: w.progress, count: &count}
+ getBody = func() (io.ReadCloser, error) {
+ blob, err := layer.Compressed()
+ if err != nil {
+ return nil, err
+ }
+ return &progressReader{rc: blob, progress: w.progress, count: &count}, nil
+ }
+ reset = func() {
+ w.progress.complete(-count)
+ }
+ }
+
+ req, err := http.NewRequest(http.MethodPatch, streamLocation, blob)
+ if err != nil {
+ return "", err
+ }
+ if _, ok := layer.(*stream.Layer); !ok {
+ // We can't retry streaming layers.
+ req.GetBody = getBody
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ resp, err := w.client.Do(req.WithContext(ctx))
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusNoContent, http.StatusAccepted, http.StatusCreated); err != nil {
+ return "", err
+ }
+
+ // The blob has been uploaded, return the location header indicating
+ // how to commit this layer.
+ return w.nextLocation(resp)
+}
+
+// commitBlob commits this blob by sending a PUT to the location returned from
+// streaming the blob.
+func (w *writer) commitBlob(ctx context.Context, location, digest string) error {
+ u, err := url.Parse(location)
+ if err != nil {
+ return err
+ }
+ v := u.Query()
+ v.Set("digest", digest)
+ u.RawQuery = v.Encode()
+
+ req, err := http.NewRequest(http.MethodPut, u.String(), nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ resp, err := w.client.Do(req.WithContext(ctx))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return transport.CheckError(resp, http.StatusCreated)
+}
+
+// incrProgress increments and sends a progress update, if WithProgress is used.
+func (w *writer) incrProgress(written int64) {
+ if w.progress == nil {
+ return
+ }
+ w.progress.complete(written)
+}
+
+// uploadOne performs a complete upload of a single layer.
+func (w *writer) uploadOne(ctx context.Context, l v1.Layer) error {
+ tryUpload := func() error {
+ ctx := retry.Never(ctx)
+ var from, mount, origin string
+ if h, err := l.Digest(); err == nil {
+ // If we know the digest, this isn't a streaming layer. Do an existence
+ // check so we can skip uploading the layer if possible.
+ existing, err := w.checkExistingBlob(ctx, h)
+ if err != nil {
+ return err
+ }
+ if existing {
+ size, err := l.Size()
+ if err != nil {
+ return err
+ }
+ w.incrProgress(size)
+ logs.Progress.Printf("existing blob: %v", h)
+ return nil
+ }
+
+ mount = h.String()
+ }
+ if ml, ok := l.(*MountableLayer); ok {
+ from = ml.Reference.Context().RepositoryStr()
+ origin = ml.Reference.Context().RegistryStr()
+ }
+
+ location, mounted, err := w.initiateUpload(ctx, from, mount, origin)
+ if err != nil {
+ return err
+ } else if mounted {
+ size, err := l.Size()
+ if err != nil {
+ return err
+ }
+ w.incrProgress(size)
+ h, err := l.Digest()
+ if err != nil {
+ return err
+ }
+ logs.Progress.Printf("mounted blob: %s", h.String())
+ return nil
+ }
+
+ // Only log layers with +json or +yaml. We can let through other stuff if it becomes popular.
+ // TODO(opencontainers/image-spec#791): Would be great to have an actual parser.
+ mt, err := l.MediaType()
+ if err != nil {
+ return err
+ }
+ smt := string(mt)
+ if !(strings.HasSuffix(smt, "+json") || strings.HasSuffix(smt, "+yaml")) {
+ ctx = redact.NewContext(ctx, "omitting binary blobs from logs")
+ }
+
+ location, err = w.streamBlob(ctx, l, location)
+ if err != nil {
+ return err
+ }
+
+ h, err := l.Digest()
+ if err != nil {
+ return err
+ }
+ digest := h.String()
+
+ if err := w.commitBlob(ctx, location, digest); err != nil {
+ return err
+ }
+ logs.Progress.Printf("pushed blob: %s", digest)
+ return nil
+ }
+
+ return retry.Retry(tryUpload, w.predicate, w.backoff)
+}
+
+type withLayer interface {
+ Layer(v1.Hash) (v1.Layer, error)
+}
+
+func (w *writer) writeIndex(ctx context.Context, ref name.Reference, ii v1.ImageIndex, options ...Option) error {
+ index, err := ii.IndexManifest()
+ if err != nil {
+ return err
+ }
+
+ o, err := makeOptions(ref.Context(), options...)
+ if err != nil {
+ return err
+ }
+
+ // TODO(#803): Pipe through remote.WithJobs and upload these in parallel.
+ for _, desc := range index.Manifests {
+ ref := ref.Context().Digest(desc.Digest.String())
+ exists, err := w.checkExistingManifest(ctx, desc.Digest, desc.MediaType)
+ if err != nil {
+ return err
+ }
+ if exists {
+ logs.Progress.Print("existing manifest: ", desc.Digest)
+ continue
+ }
+
+ switch desc.MediaType {
+ case types.OCIImageIndex, types.DockerManifestList:
+ ii, err := ii.ImageIndex(desc.Digest)
+ if err != nil {
+ return err
+ }
+ if err := w.writeIndex(ctx, ref, ii, options...); err != nil {
+ return err
+ }
+ case types.OCIManifestSchema1, types.DockerManifestSchema2:
+ img, err := ii.Image(desc.Digest)
+ if err != nil {
+ return err
+ }
+ if err := writeImage(ctx, ref, img, o, w.progress); err != nil {
+ return err
+ }
+ default:
+ // Workaround for #819.
+ if wl, ok := ii.(withLayer); ok {
+ layer, err := wl.Layer(desc.Digest)
+ if err != nil {
+ return err
+ }
+ if err := w.uploadOne(ctx, layer); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ // With all of the constituent elements uploaded, upload the manifest
+ // to commit the image.
+ return w.commitManifest(ctx, ii, ref)
+}
+
+type withMediaType interface {
+ MediaType() (types.MediaType, error)
+}
+
+// This is really silly, but go interfaces don't let me satisfy remote.Taggable
+// with remote.Descriptor because of name collisions between method names and
+// struct fields.
+//
+// Use reflection to either pull the v1.Descriptor out of remote.Descriptor or
+// create a descriptor based on the RawManifest and (optionally) MediaType.
+func unpackTaggable(t Taggable) ([]byte, *v1.Descriptor, error) {
+ if d, ok := t.(*Descriptor); ok {
+ return d.Manifest, &d.Descriptor, nil
+ }
+ b, err := t.RawManifest()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // A reasonable default if Taggable doesn't implement MediaType.
+ mt := types.DockerManifestSchema2
+
+ if wmt, ok := t.(withMediaType); ok {
+ m, err := wmt.MediaType()
+ if err != nil {
+ return nil, nil, err
+ }
+ mt = m
+ }
+
+ h, sz, err := v1.SHA256(bytes.NewReader(b))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return b, &v1.Descriptor{
+ MediaType: mt,
+ Size: sz,
+ Digest: h,
+ }, nil
+}
+
+// commitSubjectReferrers is responsible for updating the fallback tag manifest to track descriptors referring to a subject for registries that don't yet support the Referrers API.
+// TODO: use conditional requests to avoid race conditions
+func (w *writer) commitSubjectReferrers(ctx context.Context, sub name.Digest, add v1.Descriptor) error {
+ // Check if the registry supports Referrers API.
+ // TODO: This should be done once per registry, not once per subject.
+ u := w.url(fmt.Sprintf("/v2/%s/referrers/%s", w.repo.RepositoryStr(), sub.DigestStr()))
+ req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Accept", string(types.OCIImageIndex))
+ resp, err := w.client.Do(req.WithContext(ctx))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest); err != nil {
+ return err
+ }
+ if resp.StatusCode == http.StatusOK {
+ // The registry supports Referrers API. The registry is responsible for updating the referrers list.
+ return nil
+ }
+
+ // The registry doesn't support Referrers API, we need to update the manifest tagged with the fallback tag.
+ // Make the request to GET the current manifest.
+ t := fallbackTag(sub)
+ u = w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), t.Identifier()))
+ req, err = http.NewRequest(http.MethodGet, u.String(), nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Accept", string(types.OCIImageIndex))
+ resp, err = w.client.Do(req.WithContext(ctx))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ var im v1.IndexManifest
+ if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil {
+ return err
+ } else if resp.StatusCode == http.StatusNotFound {
+ // Not found just means there are no attachments. Start with an empty index.
+ im = v1.IndexManifest{
+ SchemaVersion: 2,
+ MediaType: types.OCIImageIndex,
+ Manifests: []v1.Descriptor{add},
+ }
+ } else {
+ if err := json.NewDecoder(resp.Body).Decode(&im); err != nil {
+ return err
+ }
+ if im.SchemaVersion != 2 {
+ return fmt.Errorf("fallback tag manifest is not a schema version 2: %d", im.SchemaVersion)
+ }
+ if im.MediaType != types.OCIImageIndex {
+ return fmt.Errorf("fallback tag manifest is not an OCI image index: %s", im.MediaType)
+ }
+ for _, desc := range im.Manifests {
+ if desc.Digest == add.Digest {
+ // The digest is already attached, nothing to do.
+ logs.Progress.Printf("fallback tag %s already had referrer", t.Identifier())
+ return nil
+ }
+ }
+ // Append the new descriptor to the index.
+ im.Manifests = append(im.Manifests, add)
+ }
+
+ // Sort the manifests for reproducibility.
+ sort.Slice(im.Manifests, func(i, j int) bool {
+ return im.Manifests[i].Digest.String() < im.Manifests[j].Digest.String()
+ })
+ logs.Progress.Printf("updating fallback tag %s with new referrer", t.Identifier())
+ if err := w.commitManifest(ctx, fallbackTaggable{im}, t); err != nil {
+ return err
+ }
+ return nil
+}
+
+type fallbackTaggable struct {
+ im v1.IndexManifest
+}
+
+func (f fallbackTaggable) RawManifest() ([]byte, error) { return json.Marshal(f.im) }
+func (f fallbackTaggable) MediaType() (types.MediaType, error) { return types.OCIImageIndex, nil }
+
+// commitManifest does a PUT of the image's manifest.
+func (w *writer) commitManifest(ctx context.Context, t Taggable, ref name.Reference) error {
+ // If the manifest refers to a subject, we need to check whether we need to update the fallback tag manifest.
+ raw, err := t.RawManifest()
+ if err != nil {
+ return err
+ }
+ var mf struct {
+ MediaType types.MediaType `json:"mediaType"`
+ Subject *v1.Descriptor `json:"subject,omitempty"`
+ Config struct {
+ MediaType types.MediaType `json:"mediaType"`
+ } `json:"config"`
+ }
+ if err := json.Unmarshal(raw, &mf); err != nil {
+ return err
+ }
+
+ tryUpload := func() error {
+ ctx := retry.Never(ctx)
+ raw, desc, err := unpackTaggable(t)
+ if err != nil {
+ return err
+ }
+
+ u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), ref.Identifier()))
+
+ // Make the request to PUT the serialized manifest
+ req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewBuffer(raw))
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", string(desc.MediaType))
+
+ resp, err := w.client.Do(req.WithContext(ctx))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if err := transport.CheckError(resp, http.StatusOK, http.StatusCreated, http.StatusAccepted); err != nil {
+ return err
+ }
+
+ // If the manifest referred to a subject, we may need to update the fallback tag manifest.
+ // TODO: If this fails, we'll retry the whole upload. We should retry just this part.
+ if mf.Subject != nil {
+ h, size, err := v1.SHA256(bytes.NewReader(raw))
+ if err != nil {
+ return err
+ }
+ desc := v1.Descriptor{
+ ArtifactType: string(mf.Config.MediaType),
+ MediaType: mf.MediaType,
+ Digest: h,
+ Size: size,
+ }
+ if err := w.commitSubjectReferrers(ctx,
+ ref.Context().Digest(mf.Subject.Digest.String()),
+ desc); err != nil {
+ return err
+ }
+ }
+
+ // The image was successfully pushed!
+ logs.Progress.Printf("%v: digest: %v size: %d", ref, desc.Digest, desc.Size)
+ w.incrProgress(int64(len(raw)))
+ return nil
+ }
+
+ return retry.Retry(tryUpload, w.predicate, w.backoff)
+}
+
+func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
+ // use a map as set to remove duplicates scope strings
+ scopeSet := map[string]struct{}{}
+
+ for _, l := range layers {
+ if ml, ok := l.(*MountableLayer); ok {
+ // we will add push scope for ref.Context() after the loop.
+ // for now we ask pull scope for references of the same registry
+ if ml.Reference.Context().String() != repo.String() && ml.Reference.Context().Registry.String() == repo.Registry.String() {
+ scopeSet[ml.Reference.Scope(transport.PullScope)] = struct{}{}
+ }
+ }
+ }
+
+ scopes := make([]string, 0)
+ // Push scope should be the first element because a few registries just look at the first scope to determine access.
+ scopes = append(scopes, repo.Scope(transport.PushScope))
+
+ for scope := range scopeSet {
+ scopes = append(scopes, scope)
+ }
+
+ return scopes
+}
+
+// WriteIndex pushes the provided ImageIndex to the specified image reference.
+// WriteIndex will attempt to push all of the referenced manifests before
+// attempting to push the ImageIndex, to retain referential integrity.
+func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) (rerr error) {
+ o, err := makeOptions(ref.Context(), options...)
+ if err != nil {
+ return err
+ }
+
+ scopes := []string{ref.Scope(transport.PushScope)}
+ tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
+ if err != nil {
+ return err
+ }
+ w := writer{
+ repo: ref.Context(),
+ client: &http.Client{Transport: tr},
+ backoff: o.retryBackoff,
+ predicate: o.retryPredicate,
+ }
+
+ if o.updates != nil {
+ w.progress = &progress{updates: o.updates}
+ w.progress.lastUpdate = &v1.Update{}
+
+ defer close(o.updates)
+ defer func() { w.progress.err(rerr) }()
+
+ w.progress.lastUpdate.Total, err = countIndex(ii, o.allowNondistributableArtifacts)
+ if err != nil {
+ return err
+ }
+ }
+
+ return w.writeIndex(o.context, ref, ii, options...)
+}
+
+// countImage counts the total size of all layers + config blob + manifest for
+// an image. It de-dupes duplicate layers.
+func countImage(img v1.Image, allowNondistributableArtifacts bool) (int64, error) {
+ var total int64
+ ls, err := img.Layers()
+ if err != nil {
+ return 0, err
+ }
+ seen := map[v1.Hash]bool{}
+ for _, l := range ls {
+ // Handle foreign layers.
+ mt, err := l.MediaType()
+ if err != nil {
+ return 0, err
+ }
+ if !mt.IsDistributable() && !allowNondistributableArtifacts {
+ continue
+ }
+
+ // TODO: support streaming layers which update the total count as they write.
+ if _, ok := l.(*stream.Layer); ok {
+ return 0, errors.New("cannot use stream.Layer and WithProgress")
+ }
+
+ // Dedupe layers.
+ d, err := l.Digest()
+ if err != nil {
+ return 0, err
+ }
+ if seen[d] {
+ continue
+ }
+ seen[d] = true
+
+ size, err := l.Size()
+ if err != nil {
+ return 0, err
+ }
+ total += size
+ }
+ b, err := img.RawConfigFile()
+ if err != nil {
+ return 0, err
+ }
+ total += int64(len(b))
+ size, err := img.Size()
+ if err != nil {
+ return 0, err
+ }
+ total += size
+ return total, nil
+}
+
+// countIndex counts the total size of all images + sub-indexes for an index.
+// It does not attempt to de-dupe duplicate images, etc.
+func countIndex(idx v1.ImageIndex, allowNondistributableArtifacts bool) (int64, error) {
+ var total int64
+ mf, err := idx.IndexManifest()
+ if err != nil {
+ return 0, err
+ }
+
+ for _, desc := range mf.Manifests {
+ switch desc.MediaType {
+ case types.OCIImageIndex, types.DockerManifestList:
+ sidx, err := idx.ImageIndex(desc.Digest)
+ if err != nil {
+ return 0, err
+ }
+ size, err := countIndex(sidx, allowNondistributableArtifacts)
+ if err != nil {
+ return 0, err
+ }
+ total += size
+ case types.OCIManifestSchema1, types.DockerManifestSchema2:
+ simg, err := idx.Image(desc.Digest)
+ if err != nil {
+ return 0, err
+ }
+ size, err := countImage(simg, allowNondistributableArtifacts)
+ if err != nil {
+ return 0, err
+ }
+ total += size
+ default:
+ // Workaround for #819.
+ if wl, ok := idx.(withLayer); ok {
+ layer, err := wl.Layer(desc.Digest)
+ if err != nil {
+ return 0, err
+ }
+ size, err := layer.Size()
+ if err != nil {
+ return 0, err
+ }
+ total += size
+ }
+ }
+ }
+
+ size, err := idx.Size()
+ if err != nil {
+ return 0, err
+ }
+ total += size
+ return total, nil
+}
+
+// WriteLayer uploads the provided Layer to the specified repo.
+func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) (rerr error) {
+ o, err := makeOptions(repo, options...)
+ if err != nil {
+ return err
+ }
+ scopes := scopesForUploadingImage(repo, []v1.Layer{layer})
+ tr, err := transport.NewWithContext(o.context, repo.Registry, o.auth, o.transport, scopes)
+ if err != nil {
+ return err
+ }
+ w := writer{
+ repo: repo,
+ client: &http.Client{Transport: tr},
+ backoff: o.retryBackoff,
+ predicate: o.retryPredicate,
+ }
+
+ if o.updates != nil {
+ w.progress = &progress{updates: o.updates}
+ w.progress.lastUpdate = &v1.Update{}
+
+ defer close(o.updates)
+ defer func() { w.progress.err(rerr) }()
+
+ // TODO: support streaming layers which update the total count as they write.
+ if _, ok := layer.(*stream.Layer); ok {
+ return errors.New("cannot use stream.Layer and WithProgress")
+ }
+ size, err := layer.Size()
+ if err != nil {
+ return err
+ }
+ w.progress.total(size)
+ }
+ return w.uploadOne(o.context, layer)
+}
+
+// Tag adds a tag to the given Taggable via PUT /v2/.../manifests/<tag>
+//
+// Notable implementations of Taggable are v1.Image, v1.ImageIndex, and
+// remote.Descriptor.
+//
+// If t implements MediaType, we will use that for the Content-Type, otherwise
+// we will default to types.DockerManifestSchema2.
+//
+// Tag does not attempt to write anything other than the manifest, so callers
+// should ensure that all blobs or manifests that are referenced by t exist
+// in the target registry.
+func Tag(tag name.Tag, t Taggable, options ...Option) error {
+ return Put(tag, t, options...)
+}
+
+// Put adds a manifest from the given Taggable via PUT /v1/.../manifest/<ref>
+//
+// Notable implementations of Taggable are v1.Image, v1.ImageIndex, and
+// remote.Descriptor.
+//
+// If t implements MediaType, we will use that for the Content-Type, otherwise
+// we will default to types.DockerManifestSchema2.
+//
+// Put does not attempt to write anything other than the manifest, so callers
+// should ensure that all blobs or manifests that are referenced by t exist
+// in the target registry.
+func Put(ref name.Reference, t Taggable, options ...Option) error {
+ o, err := makeOptions(ref.Context(), options...)
+ if err != nil {
+ return err
+ }
+ scopes := []string{ref.Scope(transport.PushScope)}
+
+ // TODO: This *always* does a token exchange. For some registries,
+ // that's pretty slow. Some ideas;
+ // * Tag could take a list of tags.
+ // * Allow callers to pass in a transport.Transport, typecheck
+ // it to allow them to reuse the transport across multiple calls.
+ // * WithTag option to do multiple manifest PUTs in commitManifest.
+ tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
+ if err != nil {
+ return err
+ }
+ w := writer{
+ repo: ref.Context(),
+ client: &http.Client{Transport: tr},
+ backoff: o.retryBackoff,
+ predicate: o.retryPredicate,
+ }
+
+ return w.commitManifest(o.context, t, ref)
+}
diff --git a/pkg/v1/remote/write_test.go b/pkg/v1/remote/write_test.go
new file mode 100644
index 0000000..7235c96
--- /dev/null
+++ b/pkg/v1/remote/write_test.go
@@ -0,0 +1,1643 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "strings"
+ "sync/atomic"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func mustNewTag(t *testing.T, s string) name.Tag {
+ tag, err := name.NewTag(s, name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag(%v) = %v", s, err)
+ }
+ return tag
+}
+
+func TestUrl(t *testing.T) {
+ tests := []struct {
+ tag string
+ path string
+ url string
+ }{{
+ tag: "gcr.io/foo/bar:latest",
+ path: "/v2/foo/bar/manifests/latest",
+ url: "https://gcr.io/v2/foo/bar/manifests/latest",
+ }, {
+ tag: "localhost:8080/foo/bar:baz",
+ path: "/v2/foo/bar/blobs/upload",
+ url: "http://localhost:8080/v2/foo/bar/blobs/upload",
+ }}
+
+ for _, test := range tests {
+ w := &writer{
+ repo: mustNewTag(t, test.tag).Context(),
+ }
+ if got, want := w.url(test.path), test.url; got.String() != want {
+ t.Errorf("url(%v) = %v, want %v", test.path, got.String(), want)
+ }
+ }
+}
+
+func TestNextLocation(t *testing.T) {
+ tests := []struct {
+ location string
+ url string
+ }{{
+ location: "https://gcr.io/v2/foo/bar/blobs/uploads/1234567?baz=blah",
+ url: "https://gcr.io/v2/foo/bar/blobs/uploads/1234567?baz=blah",
+ }, {
+ location: "/v2/foo/bar/blobs/uploads/1234567?baz=blah",
+ url: "https://gcr.io/v2/foo/bar/blobs/uploads/1234567?baz=blah",
+ }}
+
+ ref := mustNewTag(t, "gcr.io/foo/bar:latest")
+ w := &writer{
+ repo: ref.Context(),
+ }
+
+ for _, test := range tests {
+ resp := &http.Response{
+ Header: map[string][]string{
+ "Location": {test.location},
+ },
+ Request: &http.Request{
+ URL: &url.URL{
+ Scheme: ref.Registry.Scheme(),
+ Host: ref.RegistryStr(),
+ },
+ },
+ }
+
+ got, err := w.nextLocation(resp)
+ if err != nil {
+ t.Errorf("nextLocation(%v) = %v", resp, err)
+ }
+ want := test.url
+ if got != want {
+ t.Errorf("nextLocation(%v) = %v, want %v", resp, got, want)
+ }
+ }
+}
+
+type closer interface {
+ Close()
+}
+
+func setupImage(t *testing.T) v1.Image {
+ rnd, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatalf("random.Image() = %v", err)
+ }
+ return rnd
+}
+
+func setupIndex(t *testing.T, children int64) v1.ImageIndex {
+ rnd, err := random.Index(1024, 1, children)
+ if err != nil {
+ t.Fatalf("random.Index() = %v", err)
+ }
+ return rnd
+}
+
+func mustConfigName(t *testing.T, img v1.Image) v1.Hash {
+ h, err := img.ConfigName()
+ if err != nil {
+ t.Fatalf("ConfigName() = %v", err)
+ }
+ return h
+}
+
+func setupWriter(repo string, handler http.HandlerFunc) (*writer, closer, error) {
+ server := httptest.NewServer(handler)
+ return setupWriterWithServer(server, repo)
+}
+
+func setupWriterWithServer(server *httptest.Server, repo string) (*writer, closer, error) {
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ server.Close()
+ return nil, nil, err
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, repo), name.WeakValidation)
+ if err != nil {
+ server.Close()
+ return nil, nil, err
+ }
+
+ return &writer{
+ repo: tag.Context(),
+ client: http.DefaultClient,
+ predicate: defaultRetryPredicate,
+ backoff: defaultRetryBackoff,
+ }, server, nil
+}
+
+func TestCheckExistingBlob(t *testing.T) {
+ tests := []struct {
+ name string
+ status int
+ existing bool
+ wantErr bool
+ }{{
+ name: "success",
+ status: http.StatusOK,
+ existing: true,
+ }, {
+ name: "not found",
+ status: http.StatusNotFound,
+ existing: false,
+ }, {
+ name: "error",
+ status: http.StatusInternalServerError,
+ existing: false,
+ wantErr: true,
+ }}
+
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "foo/bar"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, h.String())
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodHead {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ http.Error(w, http.StatusText(test.status), test.status)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ existing, err := w.checkExistingBlob(context.Background(), h)
+ if test.existing != existing {
+ t.Errorf("checkExistingBlob() = %v, want %v", existing, test.existing)
+ }
+ if err != nil && !test.wantErr {
+ t.Errorf("checkExistingBlob() = %v", err)
+ } else if err == nil && test.wantErr {
+ t.Error("checkExistingBlob() wanted err, got nil")
+ }
+ })
+ }
+}
+
+func TestInitiateUploadNoMountsExists(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "foo/bar"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{"baz/bar"},
+ }.Encode()
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestInitiateUploadNoMountsInitiated(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "baz/blah"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{"baz/bar"},
+ }.Encode()
+ expectedLocation := "https://somewhere.io/upload?foo=bar"
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ w.Header().Set("Location", expectedLocation)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ location, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if mounted {
+ t.Error("initiateUpload() = mounted, want !mounted")
+ }
+ if location != expectedLocation {
+ t.Errorf("initiateUpload(); got %v, want %v", location, expectedLocation)
+ }
+}
+
+func TestInitiateUploadNoMountsBadStatus(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "ugh/another"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{"baz/bar"},
+ }.Encode()
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Unknown", http.StatusNoContent)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ location, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "")
+ if err == nil {
+ t.Errorf("intiateUpload() = %v, %v; wanted error", location, mounted)
+ }
+}
+
+func TestInitiateUploadMountsWithMountFromDifferentRegistry(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "yet/again"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{"baz/bar"},
+ }.Encode()
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestInitiateUploadMountsWithMountFromTheSameRegistry(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedMountRepo := "a/different/repo"
+ expectedRepo := "yet/again"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{expectedMountRepo},
+ }.Encode()
+
+ serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ })
+ server := httptest.NewServer(serverHandler)
+
+ w, closer, err := setupWriterWithServer(server, expectedRepo)
+ if err != nil {
+ t.Fatalf("setupWriterWithServer() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), expectedMountRepo, h.String(), "")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestInitiateUploadMountsWithOrigin(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedMountRepo := "a/different/repo"
+ expectedRepo := "yet/again"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedOrigin := "fakeOrigin"
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{expectedMountRepo},
+ "origin": []string{expectedOrigin},
+ }.Encode()
+
+ serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ })
+ server := httptest.NewServer(serverHandler)
+
+ w, closer, err := setupWriterWithServer(server, expectedRepo)
+ if err != nil {
+ t.Fatalf("setupWriterWithServer() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), expectedMountRepo, h.String(), "fakeOrigin")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestInitiateUploadMountsWithOriginFallback(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedMountRepo := "a/different/repo"
+ expectedRepo := "yet/again"
+ expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ expectedOrigin := "fakeOrigin"
+ expectedQuery := url.Values{
+ "mount": []string{h.String()},
+ "from": []string{expectedMountRepo},
+ "origin": []string{expectedOrigin},
+ }.Encode()
+
+ queries := []string{expectedQuery, ""}
+ queryCount := 0
+
+ serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != queries[queryCount] {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ if queryCount == 0 {
+ http.Error(w, "nope", http.StatusUnauthorized)
+ } else {
+ http.Error(w, "Mounted", http.StatusCreated)
+ }
+ queryCount++
+ })
+ server := httptest.NewServer(serverHandler)
+
+ w, closer, err := setupWriterWithServer(server, expectedRepo)
+ if err != nil {
+ t.Fatalf("setupWriterWithServer() = %v", err)
+ }
+ defer closer.Close()
+
+ _, mounted, err := w.initiateUpload(context.Background(), expectedMountRepo, h.String(), "fakeOrigin")
+ if err != nil {
+ t.Errorf("intiateUpload() = %v", err)
+ }
+ if !mounted {
+ t.Error("initiateUpload() = !mounted, want mounted")
+ }
+}
+
+func TestDedupeLayers(t *testing.T) {
+ newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, 10000))) }
+
+ img, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatalf("random.Image: %v", err)
+ }
+
+ // Append three identical tarball.Layers, which should be deduped
+ // because contents can be hashed before uploading.
+ for i := 0; i < 3; i++ {
+ tl, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { return newBlob(), nil })
+ if err != nil {
+ t.Fatalf("LayerFromOpener(#%d): %v", i, err)
+ }
+ img, err = mutate.AppendLayers(img, tl)
+ if err != nil {
+ t.Fatalf("mutate.AppendLayer(#%d): %v", i, err)
+ }
+ }
+
+ // Append three identical stream.Layers, whose uploads will *not* be
+ // deduped since Write can't tell they're identical ahead of time.
+ for i := 0; i < 3; i++ {
+ sl := stream.NewLayer(newBlob())
+ img, err = mutate.AppendLayers(img, sl)
+ if err != nil {
+ t.Fatalf("mutate.AppendLayer(#%d): %v", i, err)
+ }
+ }
+
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ uploadPath := "/upload"
+ commitPath := "/commit"
+ var numUploads int32
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ w.Header().Set("Location", uploadPath)
+ http.Error(w, "Accepted", http.StatusAccepted)
+ case uploadPath:
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ atomic.AddInt32(&numUploads, 1)
+ w.Header().Set("Location", commitPath)
+ http.Error(w, "Created", http.StatusCreated)
+ case commitPath:
+ http.Error(w, "Created", http.StatusCreated)
+ case manifestPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := Write(tag, img); err != nil {
+ t.Errorf("Write: %v", err)
+ }
+
+ // 3 random layers, 1 tarball layer (deduped), 3 stream layers (not deduped), 1 image config blob
+ wantUploads := int32(3 + 1 + 3 + 1)
+ if numUploads != wantUploads {
+ t.Fatalf("Write uploaded %d blobs, want %d", numUploads, wantUploads)
+ }
+}
+
+func TestStreamBlob(t *testing.T) {
+ img := setupImage(t)
+ expectedPath := "/vWhatever/I/decide"
+ expectedCommitLocation := "https://commit.io/v12/blob"
+
+ w, closer, err := setupWriter("what/ever", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ got, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Errorf("ReadAll(Body) = %v", err)
+ }
+ want, err := img.RawConfigFile()
+ if err != nil {
+ t.Errorf("RawConfigFile() = %v", err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Errorf("bytes.Equal(); got %v, want %v", got, want)
+ }
+ w.Header().Set("Location", expectedCommitLocation)
+ http.Error(w, "Created", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ streamLocation := w.url(expectedPath)
+
+ l, err := partial.ConfigLayer(img)
+ if err != nil {
+ t.Fatalf("ConfigLayer: %v", err)
+ }
+
+ commitLocation, err := w.streamBlob(context.Background(), l, streamLocation.String())
+ if err != nil {
+ t.Errorf("streamBlob() = %v", err)
+ }
+ if commitLocation != expectedCommitLocation {
+ t.Errorf("streamBlob(); got %v, want %v", commitLocation, expectedCommitLocation)
+ }
+}
+
+func TestStreamLayer(t *testing.T) {
+ var n, wantSize int64 = 10000, 49
+ newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, int(n)))) }
+ wantDigest := "sha256:3d7c465be28d9e1ed810c42aeb0e747b44441424f566722ba635dc93c947f30e"
+
+ expectedPath := "/vWhatever/I/decide"
+ expectedCommitLocation := "https://commit.io/v12/blob"
+ w, closer, err := setupWriter("what/ever", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+
+ h := crypto.SHA256.New()
+ s, err := io.Copy(h, r.Body)
+ if err != nil {
+ t.Errorf("Reading body: %v", err)
+ }
+ if s != wantSize {
+ t.Errorf("Received %d bytes, want %d", s, wantSize)
+ }
+ gotDigest := "sha256:" + hex.EncodeToString(h.Sum(nil))
+ if gotDigest != wantDigest {
+ t.Errorf("Received bytes with digest %q, want %q", gotDigest, wantDigest)
+ }
+
+ w.Header().Set("Location", expectedCommitLocation)
+ http.Error(w, "Created", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ streamLocation := w.url(expectedPath)
+ sl := stream.NewLayer(newBlob())
+
+ commitLocation, err := w.streamBlob(context.Background(), sl, streamLocation.String())
+ if err != nil {
+ t.Errorf("streamBlob: %v", err)
+ }
+ if commitLocation != expectedCommitLocation {
+ t.Errorf("streamBlob(); got %v, want %v", commitLocation, expectedCommitLocation)
+ }
+}
+
+func TestCommitBlob(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedPath := "/no/commitment/issues"
+ expectedQuery := url.Values{
+ "digest": []string{h.String()},
+ }.Encode()
+
+ w, closer, err := setupWriter("what/ever", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if r.URL.RawQuery != expectedQuery {
+ t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ commitLocation := w.url(expectedPath)
+
+ if err := w.commitBlob(context.Background(), commitLocation.String(), h.String()); err != nil {
+ t.Errorf("commitBlob() = %v", err)
+ }
+}
+
+func TestUploadOne(t *testing.T) {
+ img := setupImage(t)
+ h := mustConfigName(t, img)
+ expectedRepo := "baz/blah"
+ headPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, h.String())
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ streamPath := "/path/to/upload"
+ commitPath := "/path/to/commit"
+ ctx := context.Background()
+
+ uploaded := false
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case headPath:
+ if r.Method != http.MethodHead {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
+ }
+ if uploaded {
+ return
+ }
+ http.Error(w, "NotFound", http.StatusNotFound)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ w.Header().Set("Location", streamPath)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ case streamPath:
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ got, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Errorf("ReadAll(Body) = %v", err)
+ }
+ want, err := img.RawConfigFile()
+ if err != nil {
+ t.Errorf("RawConfigFile() = %v", err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Errorf("bytes.Equal(); got %v, want %v", got, want)
+ }
+ w.Header().Set("Location", commitPath)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ case commitPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ uploaded = true
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ l, err := partial.ConfigLayer(img)
+ if err != nil {
+ t.Fatalf("ConfigLayer: %v", err)
+ }
+ ml := &MountableLayer{
+ Layer: l,
+ Reference: w.repo.Digest(h.String()),
+ }
+ if err := w.uploadOne(ctx, ml); err != nil {
+ t.Errorf("uploadOne() = %v", err)
+ }
+ // Hit the existing blob path.
+ if err := w.uploadOne(ctx, l); err != nil {
+ t.Errorf("uploadOne() = %v", err)
+ }
+}
+
+func TestUploadOneStreamedLayer(t *testing.T) {
+ expectedRepo := "baz/blah"
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ streamPath := "/path/to/upload"
+ commitPath := "/path/to/commit"
+ ctx := context.Background()
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ w.Header().Set("Location", streamPath)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ case streamPath:
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ // TODO(jasonhall): What should we check here?
+ w.Header().Set("Location", commitPath)
+ http.Error(w, "Initiated", http.StatusAccepted)
+ case commitPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ var n, wantSize int64 = 10000, 49
+ newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, int(n)))) }
+ wantDigest := "sha256:3d7c465be28d9e1ed810c42aeb0e747b44441424f566722ba635dc93c947f30e"
+ wantDiffID := "sha256:27dd1f61b867b6a0f6e9d8a41c43231de52107e53ae424de8f847b821db4b711"
+ l := stream.NewLayer(newBlob())
+ if err := w.uploadOne(ctx, l); err != nil {
+ t.Fatalf("uploadOne: %v", err)
+ }
+
+ if dig, err := l.Digest(); err != nil {
+ t.Errorf("Digest: %v", err)
+ } else if dig.String() != wantDigest {
+ t.Errorf("Digest got %q, want %q", dig, wantDigest)
+ }
+ if diffID, err := l.DiffID(); err != nil {
+ t.Errorf("DiffID: %v", err)
+ } else if diffID.String() != wantDiffID {
+ t.Errorf("DiffID got %q, want %q", diffID, wantDiffID)
+ }
+ if size, err := l.Size(); err != nil {
+ t.Errorf("Size: %v", err)
+ } else if size != wantSize {
+ t.Errorf("Size got %d, want %d", size, wantSize)
+ }
+}
+
+func TestCommitImage(t *testing.T) {
+ img := setupImage(t)
+ ctx := context.Background()
+
+ expectedRepo := "foo/bar"
+ expectedPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ got, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Errorf("ReadAll(Body) = %v", err)
+ }
+ want, err := img.RawManifest()
+ if err != nil {
+ t.Errorf("RawManifest() = %v", err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Errorf("bytes.Equal(); got %v, want %v", got, want)
+ }
+ mt, err := img.MediaType()
+ if err != nil {
+ t.Errorf("MediaType() = %v", err)
+ }
+ if got, want := r.Header.Get("Content-Type"), string(mt); got != want {
+ t.Errorf("Header; got %v, want %v", got, want)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ if err := w.commitManifest(ctx, img, w.repo.Tag("latest")); err != nil {
+ t.Error("commitManifest() = ", err)
+ }
+}
+
+func TestWrite(t *testing.T) {
+ img := setupImage(t)
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ case manifestPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := Write(tag, img); err != nil {
+ t.Errorf("Write() = %v", err)
+ }
+}
+
+func TestWriteWithErrors(t *testing.T) {
+ img := setupImage(t)
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+
+ errorBody := `{"errors":[{"code":"NAME_INVALID","message":"some explanation of how things were messed up."}],"StatusCode":400}`
+ expectedErrMsg, err := regexp.Compile(`POST .+ NAME_INVALID: some explanation of how things were messed up.`)
+ if err != nil {
+ t.Error(err)
+ }
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte(errorBody))
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ c := make(chan v1.Update, 100)
+
+ var terr *transport.Error
+ if err := Write(tag, img, WithProgress(c)); err == nil {
+ t.Error("Write() = nil; wanted error")
+ } else if !errors.As(err, &terr) {
+ t.Errorf("Write() = %T; wanted *transport.Error", err)
+ } else if !expectedErrMsg.Match([]byte(terr.Error())) {
+ diff := cmp.Diff(expectedErrMsg, terr.Error())
+ t.Errorf("Write(); (-want +got) = %s", diff)
+ }
+
+ var last v1.Update
+ for update := range c {
+ last = update
+ }
+ if last.Error == nil {
+ t.Error("Progress chan didn't report error")
+ }
+}
+
+func TestDockerhubScopes(t *testing.T) {
+ src, err := name.ParseReference("busybox")
+ if err != nil {
+ t.Fatal(err)
+ }
+ rl, err := random.Layer(1024, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ml := &MountableLayer{
+ Layer: rl,
+ Reference: src,
+ }
+ want := src.Scope(transport.PullScope)
+
+ for _, s := range []string{
+ "jonjohnson/busybox",
+ "docker.io/jonjohnson/busybox",
+ "index.docker.io/jonjohnson/busybox",
+ } {
+ dst, err := name.ParseReference(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ scopes := scopesForUploadingImage(dst.Context(), []v1.Layer{ml})
+
+ if len(scopes) != 2 {
+ t.Errorf("Should have two scopes (src and dst), got %d", len(scopes))
+ } else if diff := cmp.Diff(want, scopes[1]); diff != "" {
+ t.Errorf("TestDockerhubScopes %q: (-want +got) = %v", s, diff)
+ }
+ }
+}
+
+func TestScopesForUploadingImage(t *testing.T) {
+ referenceToUpload, err := name.NewTag("example.com/sample/sample:latest", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ sameReference, err := name.NewTag("example.com/sample/sample:previous", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ anotherRepo1, err := name.NewTag("example.com/sample/another_repo1:latest", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ anotherRepo2, err := name.NewTag("example.com/sample/another_repo2:latest", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ repoOnOtherRegistry, err := name.NewTag("other-domain.com/sample/any_repo:latest", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("name.NewTag() = %v", err)
+ }
+
+ img := setupImage(t)
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatalf("img.Layers() = %v", err)
+ }
+ dummyLayer := layers[0]
+
+ testCases := []struct {
+ name string
+ reference name.Reference
+ layers []v1.Layer
+ expected []string
+ }{
+ {
+ name: "empty layers",
+ reference: referenceToUpload,
+ layers: []v1.Layer{},
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ },
+ },
+ {
+ name: "mountable layers with same reference",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: sameReference,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ },
+ },
+ {
+ name: "mountable layers with single reference with no-duplicate",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ anotherRepo1.Scope(transport.PullScope),
+ },
+ },
+ {
+ name: "mountable layers with single reference with duplicate",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ anotherRepo1.Scope(transport.PullScope),
+ },
+ },
+ {
+ name: "mountable layers with multiple references with no-duplicates",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo2,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ anotherRepo1.Scope(transport.PullScope),
+ anotherRepo2.Scope(transport.PullScope),
+ },
+ },
+ {
+ name: "mountable layers with multiple references with duplicates",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo2,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo1,
+ },
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: anotherRepo2,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ anotherRepo1.Scope(transport.PullScope),
+ anotherRepo2.Scope(transport.PullScope),
+ },
+ },
+ {
+ name: "cross repository mountable layer",
+ reference: referenceToUpload,
+ layers: []v1.Layer{
+ &MountableLayer{
+ Layer: dummyLayer,
+ Reference: repoOnOtherRegistry,
+ },
+ },
+ expected: []string{
+ referenceToUpload.Scope(transport.PushScope),
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ actual := scopesForUploadingImage(tc.reference.Context(), tc.layers)
+
+ if want, got := tc.expected[0], actual[0]; want != got {
+ t.Errorf("TestScopesForUploadingImage() %s: Wrong first scope; want %v, got %v", tc.name, want, got)
+ }
+
+ less := func(a, b string) bool {
+ return strings.Compare(a, b) <= -1
+ }
+ if diff := cmp.Diff(tc.expected[1:], actual[1:], cmpopts.SortSlices(less)); diff != "" {
+ t.Errorf("TestScopesForUploadingImage() %s: Wrong scopes (-want +got) = %v", tc.name, diff)
+ }
+ }
+}
+
+func TestCheckExistingManifest(t *testing.T) {
+ tests := []struct {
+ name string
+ status int
+ existing bool
+ wantErr bool
+ }{{
+ name: "success",
+ status: http.StatusOK,
+ existing: true,
+ }, {
+ name: "not found",
+ status: http.StatusNotFound,
+ existing: false,
+ }, {
+ name: "error",
+ status: http.StatusInternalServerError,
+ existing: false,
+ wantErr: true,
+ }}
+
+ img := setupImage(t)
+ h := mustDigest(t, img)
+ mt := mustMediaType(t, img)
+ expectedRepo := "foo/bar"
+ expectedPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, h.String())
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodHead {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead)
+ }
+ if r.URL.Path != expectedPath {
+ t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath)
+ }
+ if got, want := r.Header.Get("Accept"), string(mt); got != want {
+ t.Errorf("r.Header['Accept']; got %v, want %v", got, want)
+ }
+ http.Error(w, http.StatusText(test.status), test.status)
+ }))
+ if err != nil {
+ t.Fatalf("setupWriter() = %v", err)
+ }
+ defer closer.Close()
+
+ existing, err := w.checkExistingManifest(context.Background(), h, mt)
+ if test.existing != existing {
+ t.Errorf("checkExistingManifest() = %v, want %v", existing, test.existing)
+ }
+ if err != nil && !test.wantErr {
+ t.Errorf("checkExistingManifest() = %v", err)
+ } else if err == nil && test.wantErr {
+ t.Error("checkExistingManifest() wanted err, got nil")
+ }
+ })
+ }
+}
+
+func TestWriteIndex(t *testing.T) {
+ idx := setupIndex(t, 2)
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ childDigest := mustIndexManifest(t, idx).Manifests[0].Digest
+ childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest)
+ existinChildDigest := mustIndexManifest(t, idx).Manifests[1].Digest
+ existingChildPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, existinChildDigest)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ http.Error(w, "Mounted", http.StatusCreated)
+ case manifestPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ case existingChildPath:
+ if r.Method == http.MethodHead {
+ http.Error(w, http.StatusText(http.StatusOK), http.StatusOK)
+ return
+ }
+ t.Errorf("Unexpected method; got %v, want %v", r.Method, http.MethodHead)
+ case childPath:
+ if r.Method == http.MethodHead {
+ http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+ return
+ }
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := WriteIndex(tag, idx); err != nil {
+ t.Errorf("WriteIndex() = %v", err)
+ }
+}
+
+// If we actually attempt to read the contents, this will fail the test.
+type fakeForeignLayer struct {
+ t *testing.T
+}
+
+func (l *fakeForeignLayer) MediaType() (types.MediaType, error) {
+ return types.DockerForeignLayer, nil
+}
+
+func (l *fakeForeignLayer) Size() (int64, error) {
+ return 0, nil
+}
+
+func (l *fakeForeignLayer) Digest() (v1.Hash, error) {
+ return v1.Hash{Algorithm: "sha256", Hex: strings.Repeat("a", 64)}, nil
+}
+
+func (l *fakeForeignLayer) DiffID() (v1.Hash, error) {
+ return v1.Hash{Algorithm: "sha256", Hex: strings.Repeat("a", 64)}, nil
+}
+
+func (l *fakeForeignLayer) Compressed() (io.ReadCloser, error) {
+ l.t.Helper()
+ l.t.Errorf("foreign layer not skipped: Compressed")
+ return nil, nil
+}
+
+func (l *fakeForeignLayer) Uncompressed() (io.ReadCloser, error) {
+ l.t.Helper()
+ l.t.Errorf("foreign layer not skipped: Uncompressed")
+ return nil, nil
+}
+
+func TestSkipForeignLayersByDefault(t *testing.T) {
+ // Set up an image with a foreign layer.
+ base := setupImage(t)
+ img, err := mutate.AppendLayers(base, &fakeForeignLayer{t: t})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dst := fmt.Sprintf("%s/test/foreign/upload", u.Host)
+ ref, err := name.ParseReference(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Write(ref, img); err != nil {
+ t.Errorf("failed to Write: %v", err)
+ }
+}
+
+func TestWriteForeignLayerIfOptionSet(t *testing.T) {
+ // Set up an image with a foreign layer.
+ base := setupImage(t)
+ foreignLayer, err := random.Layer(1024, types.DockerForeignLayer)
+ if err != nil {
+ t.Fatal("random.Layer:", err)
+ }
+ img, err := mutate.AppendLayers(base, foreignLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expectedRepo := "write/time"
+ headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+ initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+ manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
+ uploadPath := "/upload"
+ commitPath := "/commit"
+ var numUploads int32
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+ http.Error(w, "NotFound", http.StatusNotFound)
+ return
+ }
+ switch r.URL.Path {
+ case "/v2/":
+ w.WriteHeader(http.StatusOK)
+ case initiatePath:
+ if r.Method != http.MethodPost {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+ }
+ w.Header().Set("Location", uploadPath)
+ http.Error(w, "Accepted", http.StatusAccepted)
+ case uploadPath:
+ if r.Method != http.MethodPatch {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch)
+ }
+ atomic.AddInt32(&numUploads, 1)
+ w.Header().Set("Location", commitPath)
+ http.Error(w, "Created", http.StatusCreated)
+ case commitPath:
+ http.Error(w, "Created", http.StatusCreated)
+ case manifestPath:
+ if r.Method != http.MethodPut {
+ t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+ }
+ http.Error(w, "Created", http.StatusCreated)
+ default:
+ t.Fatalf("Unexpected path: %v", r.URL.Path)
+ }
+ }))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ if err != nil {
+ t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+ }
+ tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation)
+ if err != nil {
+ t.Fatalf("NewTag() = %v", err)
+ }
+
+ if err := Write(tag, img, WithNondistributable); err != nil {
+ t.Errorf("Write: %v", err)
+ }
+
+ // 1 random layer, 1 foreign layer, 1 image config blob
+ wantUploads := int32(1 + 1 + 1)
+ if numUploads != wantUploads {
+ t.Fatalf("Write uploaded %d blobs, want %d", numUploads, wantUploads)
+ }
+}
+
+func TestTag(t *testing.T) {
+ idx := setupIndex(t, 3)
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ src := fmt.Sprintf("%s/test/tag:src", u.Host)
+ srcRef, err := name.NewTag(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := WriteIndex(srcRef, idx); err != nil {
+ t.Fatal(err)
+ }
+
+ dst := fmt.Sprintf("%s/test/tag:dst", u.Host)
+ dstRef, err := name.NewTag(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Tag(dstRef, idx); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := Index(dstRef)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Index(got); err != nil {
+ t.Errorf("Validate() = %v", err)
+ }
+}
+
+func TestTagDescriptor(t *testing.T) {
+ idx := setupIndex(t, 3)
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ src := fmt.Sprintf("%s/test/tag:src", u.Host)
+ srcRef, err := name.NewTag(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := WriteIndex(srcRef, idx); err != nil {
+ t.Fatal(err)
+ }
+
+ desc, err := Get(srcRef)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dst := fmt.Sprintf("%s/test/tag:dst", u.Host)
+ dstRef, err := name.NewTag(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Tag(dstRef, desc); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestNestedIndex(t *testing.T) {
+ // Set up a fake registry.
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ src := fmt.Sprintf("%s/test/tag:src", u.Host)
+ srcRef, err := name.NewTag(src)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ child, err := random.Index(1024, 1, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ parent := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{
+ Add: child,
+ Descriptor: v1.Descriptor{
+ URLs: []string{"example.com/url"},
+ },
+ })
+
+ l, err := random.Layer(100, types.DockerLayer)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ parent = mutate.AppendManifests(parent, mutate.IndexAddendum{
+ Add: l,
+ })
+
+ if err := WriteIndex(srcRef, parent); err != nil {
+ t.Fatal(err)
+ }
+ pulled, err := Index(srcRef)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Index(pulled); err != nil {
+ t.Fatalf("validate.Index: %v", err)
+ }
+
+ digest, err := child.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pulledChild, err := pulled.ImageIndex(digest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ desc, err := partial.Descriptor(pulledChild)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(desc.URLs) != 1 {
+ t.Fatalf("expected url for pulledChild")
+ }
+
+ if want, got := "example.com/url", desc.URLs[0]; want != got {
+ t.Errorf("pulledChild.urls[0] = %s != %s", got, want)
+ }
+}
+
+func BenchmarkWrite(b *testing.B) {
+ // unfortunately the registry _and_ the img have caching behaviour, so we need a new registry
+ // and image every iteration of benchmarking.
+ for i := 0; i < b.N; i++ {
+ // set up the registry
+ s := httptest.NewServer(registry.New())
+ defer s.Close()
+
+ // load the image
+ img, err := random.Image(50*1024*1024, 10)
+ if err != nil {
+ b.Fatalf("random.Image(...): %v", err)
+ }
+
+ b.ResetTimer()
+
+ tagStr := strings.TrimPrefix(s.URL+"/test/image:tag", "http://")
+ tag, err := name.NewTag(tagStr)
+ if err != nil {
+ b.Fatalf("parsing tag (%s): %v", tagStr, err)
+ }
+
+ err = Write(tag, img)
+ if err != nil {
+ b.Fatalf("pushing tag one: %v", err)
+ }
+ }
+}
diff --git a/pkg/v1/static/layer.go b/pkg/v1/static/layer.go
new file mode 100644
index 0000000..a4bbe69
--- /dev/null
+++ b/pkg/v1/static/layer.go
@@ -0,0 +1,68 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package static
+
+import (
+ "bytes"
+ "io"
+ "sync"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// NewLayer returns a layer containing the given bytes, with the given mediaType.
+//
+// Contents will not be compressed.
+func NewLayer(b []byte, mt types.MediaType) v1.Layer {
+ return &staticLayer{b: b, mt: mt}
+}
+
+type staticLayer struct {
+ b []byte
+ mt types.MediaType
+
+ once sync.Once
+ h v1.Hash
+}
+
+func (l *staticLayer) Digest() (v1.Hash, error) {
+ var err error
+ // Only calculate digest the first time we're asked.
+ l.once.Do(func() {
+ l.h, _, err = v1.SHA256(bytes.NewReader(l.b))
+ })
+ return l.h, err
+}
+
+func (l *staticLayer) DiffID() (v1.Hash, error) {
+ return l.Digest()
+}
+
+func (l *staticLayer) Compressed() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(l.b)), nil
+}
+
+func (l *staticLayer) Uncompressed() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(l.b)), nil
+}
+
+func (l *staticLayer) Size() (int64, error) {
+ return int64(len(l.b)), nil
+}
+
+func (l *staticLayer) MediaType() (types.MediaType, error) {
+ return l.mt, nil
+}
diff --git a/pkg/v1/static/static_test.go b/pkg/v1/static/static_test.go
new file mode 100644
index 0000000..1dcc86a
--- /dev/null
+++ b/pkg/v1/static/static_test.go
@@ -0,0 +1,83 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package static
+
+import (
+ "io"
+ "strings"
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestNewLayer(t *testing.T) {
+ b := []byte(strings.Repeat(".", 10))
+ l := NewLayer(b, types.OCILayer)
+
+ // This does basically nothing.
+ if err := validate.Layer(l, validate.Fast); err != nil {
+ t.Fatal(err)
+ }
+
+ // Digest and DiffID match, and match expectations.
+ h, err := l.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ h2, err := l.DiffID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if h != h2 {
+ t.Errorf("Digest != DiffID; digest is %v, diffid is %v", h, h2)
+ }
+ wantDigest, err := v1.NewHash("sha256:537f3fb69ba01fc388a3a5c920c485b2873d5f327305c3dd2004d6a04451659b")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if h != wantDigest {
+ t.Errorf("Digest mismatch; got %v, want %v", h, wantDigest)
+ }
+
+ sz, err := l.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if sz != 10 {
+ t.Errorf("Size mismatch; got %d, want %d", sz, 10)
+ }
+
+ mt, err := l.MediaType()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if mt != types.OCILayer {
+ t.Errorf("MediaType mismatch; got %v, want %v", mt, types.OCILayer)
+ }
+
+ r, err := l.Uncompressed()
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := io.ReadAll(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(got) != string(b) {
+ t.Errorf("Contents mismatch: got %q, want %q", string(got), string(b))
+ }
+}
diff --git a/pkg/v1/stream/README.md b/pkg/v1/stream/README.md
new file mode 100644
index 0000000..da0dda4
--- /dev/null
+++ b/pkg/v1/stream/README.md
@@ -0,0 +1,68 @@
+# `stream`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream)
+
+The `stream` package contains an implementation of
+[`v1.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1#Layer)
+that supports _streaming_ access, i.e. the layer contents are read once and not
+buffered.
+
+## Usage
+
+```go
+package main
+
+import (
+ "os"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+)
+
+// upload the contents of stdin as a layer to a local registry
+func main() {
+ repo, err := name.NewRepository("localhost:5000/stream")
+ if err != nil {
+ panic(err)
+ }
+
+ layer := stream.NewLayer(os.Stdin)
+
+ if err := remote.WriteLayer(repo, layer); err != nil {
+ panic(err)
+ }
+}
+```
+
+## Structure
+
+This implements the layer portion of an [image
+upload](/pkg/v1/remote#anatomy-of-an-image-upload). We launch a goroutine that
+is responsible for hashing the uncompressed contents to compute the `DiffID`,
+gzipping them to produce the `Compressed` contents, and hashing/counting the
+bytes to produce the `Digest`/`Size`. This goroutine writes to an
+`io.PipeWriter`, which blocks until `Compressed` reads the gzipped contents from
+the corresponding `io.PipeReader`.
+
+<p align="center">
+ <img src="/images/stream.dot.svg" />
+</p>
+
+## Caveats
+
+This assumes that you have an uncompressed layer (i.e. a tarball) and would like
+to compress it. Calling `Uncompressed` is always an error. Likewise, other
+methods are invalid until the contents of `Compressed` have been completely
+consumed and `Close`d.
+
+Using a `stream.Layer` will likely not work without careful consideration. For
+example, in the `mutate` package, we defer computing the manifest and config
+file until they are actually called. This allows you to `mutate.Append` a
+streaming layer to an image without accidentally consuming it. Similarly, in
+`remote.Write`, if calling `Digest` on a layer fails, we attempt to upload the
+layer anyway, understanding that we may be dealing with a `stream.Layer` whose
+contents need to be uploaded before we can upload the config file.
+
+Given the [structure](#structure) of how this is implemented, forgetting to
+`Close` a `stream.Layer` will leak a goroutine.
diff --git a/pkg/v1/stream/layer.go b/pkg/v1/stream/layer.go
new file mode 100644
index 0000000..d6f2df8
--- /dev/null
+++ b/pkg/v1/stream/layer.go
@@ -0,0 +1,273 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package stream implements a single-pass streaming v1.Layer.
+package stream
+
+import (
+ "bufio"
+ "compress/gzip"
+ "crypto"
+ "encoding/hex"
+ "errors"
+ "hash"
+ "io"
+ "os"
+ "sync"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+var (
+ // ErrNotComputed is returned when the requested value is not yet
+ // computed because the stream has not been consumed yet.
+ ErrNotComputed = errors.New("value not computed until stream is consumed")
+
+ // ErrConsumed is returned by Compressed when the underlying stream has
+ // already been consumed and closed.
+ ErrConsumed = errors.New("stream was already consumed")
+)
+
+// Layer is a streaming implementation of v1.Layer.
+type Layer struct {
+ blob io.ReadCloser
+ consumed bool
+ compression int
+
+ mu sync.Mutex
+ digest, diffID *v1.Hash
+ size int64
+ mediaType types.MediaType
+}
+
+var _ v1.Layer = (*Layer)(nil)
+
+// LayerOption applies options to layer
+type LayerOption func(*Layer)
+
+// WithCompressionLevel sets the gzip compression. See `gzip.NewWriterLevel` for possible values.
+func WithCompressionLevel(level int) LayerOption {
+ return func(l *Layer) {
+ l.compression = level
+ }
+}
+
+// WithMediaType is a functional option for overriding the layer's media type.
+func WithMediaType(mt types.MediaType) LayerOption {
+ return func(l *Layer) {
+ l.mediaType = mt
+ }
+}
+
+// NewLayer creates a Layer from an io.ReadCloser.
+func NewLayer(rc io.ReadCloser, opts ...LayerOption) *Layer {
+ layer := &Layer{
+ blob: rc,
+ compression: gzip.BestSpeed,
+ // We use DockerLayer for now as uncompressed layers
+ // are unimplemented
+ mediaType: types.DockerLayer,
+ }
+
+ for _, opt := range opts {
+ opt(layer)
+ }
+
+ return layer
+}
+
+// Digest implements v1.Layer.
+func (l *Layer) Digest() (v1.Hash, error) {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ if l.digest == nil {
+ return v1.Hash{}, ErrNotComputed
+ }
+ return *l.digest, nil
+}
+
+// DiffID implements v1.Layer.
+func (l *Layer) DiffID() (v1.Hash, error) {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ if l.diffID == nil {
+ return v1.Hash{}, ErrNotComputed
+ }
+ return *l.diffID, nil
+}
+
+// Size implements v1.Layer.
+func (l *Layer) Size() (int64, error) {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ if l.size == 0 {
+ return 0, ErrNotComputed
+ }
+ return l.size, nil
+}
+
+// MediaType implements v1.Layer
+func (l *Layer) MediaType() (types.MediaType, error) {
+ return l.mediaType, nil
+}
+
+// Uncompressed implements v1.Layer.
+func (l *Layer) Uncompressed() (io.ReadCloser, error) {
+ return nil, errors.New("NYI: stream.Layer.Uncompressed is not implemented")
+}
+
+// Compressed implements v1.Layer.
+func (l *Layer) Compressed() (io.ReadCloser, error) {
+ if l.consumed {
+ return nil, ErrConsumed
+ }
+ return newCompressedReader(l)
+}
+
+// finalize sets the layer to consumed and computes all hash and size values.
+func (l *Layer) finalize(uncompressed, compressed hash.Hash, size int64) error {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+
+ diffID, err := v1.NewHash("sha256:" + hex.EncodeToString(uncompressed.Sum(nil)))
+ if err != nil {
+ return err
+ }
+ l.diffID = &diffID
+
+ digest, err := v1.NewHash("sha256:" + hex.EncodeToString(compressed.Sum(nil)))
+ if err != nil {
+ return err
+ }
+ l.digest = &digest
+
+ l.size = size
+ l.consumed = true
+ return nil
+}
+
+type compressedReader struct {
+ pr io.Reader
+ closer func() error
+}
+
+func newCompressedReader(l *Layer) (*compressedReader, error) {
+ // Collect digests of compressed and uncompressed stream and size of
+ // compressed stream.
+ h := crypto.SHA256.New()
+ zh := crypto.SHA256.New()
+ count := &countWriter{}
+
+ // gzip.Writer writes to the output stream via pipe, a hasher to
+ // capture compressed digest, and a countWriter to capture compressed
+ // size.
+ pr, pw := io.Pipe()
+
+ // Write compressed bytes to be read by the pipe.Reader, hashed by zh, and counted by count.
+ mw := io.MultiWriter(pw, zh, count)
+
+ // Buffer the output of the gzip writer so we don't have to wait on pr to keep writing.
+ // 64K ought to be small enough for anybody.
+ bw := bufio.NewWriterSize(mw, 2<<16)
+ zw, err := gzip.NewWriterLevel(bw, l.compression)
+ if err != nil {
+ return nil, err
+ }
+
+ doneDigesting := make(chan struct{})
+
+ cr := &compressedReader{
+ pr: pr,
+ closer: func() error {
+ // Immediately close pw without error. There are three ways to get
+ // here.
+ //
+ // 1. There was a copy error due from the underlying reader, in which
+ // case the error will not be overwritten.
+ // 2. Copying from the underlying reader completed successfully.
+ // 3. Close has been called before the underlying reader has been
+ // fully consumed. In this case pw must be closed in order to
+ // keep the flush of bw from blocking indefinitely.
+ //
+ // NOTE: pw.Close never returns an error. The signature is only to
+ // implement io.Closer.
+ _ = pw.Close()
+
+ // Close the inner ReadCloser.
+ //
+ // NOTE: net/http will call close on success, so if we've already
+ // closed the inner rc, it's not an error.
+ if err := l.blob.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
+ return err
+ }
+
+ // Finalize layer with its digest and size values.
+ <-doneDigesting
+ return l.finalize(h, zh, count.n)
+ },
+ }
+ go func() {
+ // Copy blob into the gzip writer, which also hashes and counts the
+ // size of the compressed output, and hasher of the raw contents.
+ _, copyErr := io.Copy(io.MultiWriter(h, zw), l.blob)
+
+ // Close the gzip writer once copying is done. If this is done in the
+ // Close method of compressedReader instead, then it can cause a panic
+ // when the compressedReader is closed before the blob is fully
+ // consumed and io.Copy in this goroutine is still blocking.
+ closeErr := zw.Close()
+
+ // Check errors from writing and closing streams.
+ if copyErr != nil {
+ close(doneDigesting)
+ pw.CloseWithError(copyErr)
+ return
+ }
+ if closeErr != nil {
+ close(doneDigesting)
+ pw.CloseWithError(closeErr)
+ return
+ }
+
+ // Flush the buffer once all writes are complete to the gzip writer.
+ if err := bw.Flush(); err != nil {
+ close(doneDigesting)
+ pw.CloseWithError(err)
+ return
+ }
+
+ // Notify closer that digests are done being written.
+ close(doneDigesting)
+
+ // Close the compressed reader to calculate digest/diffID/size. This
+ // will cause pr to return EOF which will cause readers of the
+ // Compressed stream to finish reading.
+ pw.CloseWithError(cr.Close())
+ }()
+
+ return cr, nil
+}
+
+func (cr *compressedReader) Read(b []byte) (int, error) { return cr.pr.Read(b) }
+
+func (cr *compressedReader) Close() error { return cr.closer() }
+
+// countWriter counts bytes written to it.
+type countWriter struct{ n int64 }
+
+func (c *countWriter) Write(p []byte) (int, error) {
+ c.n += int64(len(p))
+ return len(p), nil
+}
diff --git a/pkg/v1/stream/layer_test.go b/pkg/v1/stream/layer_test.go
new file mode 100644
index 0000000..e65452b
--- /dev/null
+++ b/pkg/v1/stream/layer_test.go
@@ -0,0 +1,298 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package stream
+
+import (
+ "archive/tar"
+ "bytes"
+ "crypto/rand"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+func TestStreamVsBuffer(t *testing.T) {
+ var n, wantSize int64 = 10000, 49
+ newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, int(n)))) }
+ wantDigest := "sha256:3d7c465be28d9e1ed810c42aeb0e747b44441424f566722ba635dc93c947f30e"
+ wantDiffID := "sha256:27dd1f61b867b6a0f6e9d8a41c43231de52107e53ae424de8f847b821db4b711"
+
+ // Check that streaming some content results in the expected digest/diffID/size.
+ l := NewLayer(newBlob())
+ if c, err := l.Compressed(); err != nil {
+ t.Errorf("Compressed: %v", err)
+ } else {
+ if _, err := io.Copy(io.Discard, c); err != nil {
+ t.Errorf("error reading Compressed: %v", err)
+ }
+ if err := c.Close(); err != nil {
+ t.Errorf("Close: %v", err)
+ }
+ }
+ if d, err := l.Digest(); err != nil {
+ t.Errorf("Digest: %v", err)
+ } else if d.String() != wantDigest {
+ t.Errorf("stream Digest got %q, want %q", d.String(), wantDigest)
+ }
+ if d, err := l.DiffID(); err != nil {
+ t.Errorf("DiffID: %v", err)
+ } else if d.String() != wantDiffID {
+ t.Errorf("stream DiffID got %q, want %q", d.String(), wantDiffID)
+ }
+ if s, err := l.Size(); err != nil {
+ t.Errorf("Size: %v", err)
+ } else if s != wantSize {
+ t.Errorf("stream Size got %d, want %d", s, wantSize)
+ }
+
+ // Test that buffering the same contents and using
+ // tarball.LayerFromOpener results in the same digest/diffID/size.
+ tl, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { return newBlob(), nil })
+ if err != nil {
+ t.Fatalf("LayerFromOpener: %v", err)
+ }
+ if d, err := tl.Digest(); err != nil {
+ t.Errorf("Digest: %v", err)
+ } else if d.String() != wantDigest {
+ t.Errorf("tarball Digest got %q, want %q", d.String(), wantDigest)
+ }
+ if d, err := tl.DiffID(); err != nil {
+ t.Errorf("DiffID: %v", err)
+ } else if d.String() != wantDiffID {
+ t.Errorf("tarball DiffID got %q, want %q", d.String(), wantDiffID)
+ }
+ if s, err := tl.Size(); err != nil {
+ t.Errorf("Size: %v", err)
+ } else if s != wantSize {
+ t.Errorf("stream Size got %d, want %d", s, wantSize)
+ }
+
+ // Test with different compression
+ l2 := NewLayer(newBlob(), WithCompressionLevel(2))
+ l2WantDigest := "sha256:c9afe7b0da6783232e463e12328cb306142548384accf3995806229c9a6a707f"
+ if c, err := l2.Compressed(); err != nil {
+ t.Errorf("Compressed: %v", err)
+ } else {
+ if _, err := io.Copy(io.Discard, c); err != nil {
+ t.Errorf("error reading Compressed: %v", err)
+ }
+ if err := c.Close(); err != nil {
+ t.Errorf("Close: %v", err)
+ }
+ }
+ if d, err := l2.Digest(); err != nil {
+ t.Errorf("Digest: %v", err)
+ } else if d.String() != l2WantDigest {
+ t.Errorf("stream Digest got %q, want %q", d.String(), l2WantDigest)
+ }
+}
+
+func TestLargeStream(t *testing.T) {
+ var n, wantSize int64 = 10000000, 10000788 // "Compressing" n random bytes results in this many bytes.
+ sl := NewLayer(io.NopCloser(io.LimitReader(rand.Reader, n)))
+ rc, err := sl.Compressed()
+ if err != nil {
+ t.Fatalf("Uncompressed: %v", err)
+ }
+ if _, err := io.Copy(io.Discard, rc); err != nil {
+ t.Fatalf("Reading layer: %v", err)
+ }
+ if err := rc.Close(); err != nil {
+ t.Fatalf("Close: %v", err)
+ }
+
+ if dig, err := sl.Digest(); err != nil {
+ t.Errorf("Digest: %v", err)
+ } else if dig.String() == (v1.Hash{}).String() {
+ t.Errorf("Digest got %q, want anything else", (v1.Hash{}).String())
+ }
+ if diffID, err := sl.DiffID(); err != nil {
+ t.Errorf("DiffID: %v", err)
+ } else if diffID.String() == (v1.Hash{}).String() {
+ t.Errorf("DiffID got %q, want anything else", (v1.Hash{}).String())
+ }
+ if size, err := sl.Size(); err != nil {
+ t.Errorf("Size: %v", err)
+ } else if size != wantSize {
+ t.Errorf("Size got %d, want %d", size, wantSize)
+ }
+}
+
+func TestStreamableLayerFromTarball(t *testing.T) {
+ pr, pw := io.Pipe()
+ tw := tar.NewWriter(pw)
+ go func() {
+ // "Stream" a bunch of files into the layer.
+ pw.CloseWithError(func() error {
+ for i := 0; i < 1000; i++ {
+ name := fmt.Sprintf("file-%d.txt", i)
+ body := fmt.Sprintf("i am file number %d", i)
+ if err := tw.WriteHeader(&tar.Header{
+ Name: name,
+ Mode: 0600,
+ Size: int64(len(body)),
+ Typeflag: tar.TypeReg,
+ }); err != nil {
+ return err
+ }
+ if _, err := tw.Write([]byte(body)); err != nil {
+ return err
+ }
+ }
+ if err := tw.Close(); err != nil {
+ return err
+ }
+ return nil
+ }())
+ }()
+
+ l := NewLayer(pr)
+ rc, err := l.Compressed()
+ if err != nil {
+ t.Fatalf("Compressed: %v", err)
+ }
+ if _, err := io.Copy(io.Discard, rc); err != nil {
+ t.Fatalf("Copy: %v", err)
+ }
+ if err := rc.Close(); err != nil {
+ t.Fatalf("Close: %v", err)
+ }
+
+ wantDigest := "sha256:ed80efd7e7e884fb59db568f234332283b341b96155e872d638de42d55a34198"
+ if got, err := l.Digest(); err != nil {
+ t.Errorf("Digest: %v", err)
+ } else if got.String() != wantDigest {
+ t.Errorf("Digest: got %q, want %q", got.String(), wantDigest)
+ }
+}
+
+// TestNotComputed tests that Digest/DiffID/Size return ErrNotComputed before
+// the stream has been consumed.
+func TestNotComputed(t *testing.T) {
+ l := NewLayer(io.NopCloser(bytes.NewBufferString("hi")))
+
+ // All methods should return ErrNotComputed until the stream has been
+ // consumed and closed.
+ if _, err := l.Size(); !errors.Is(err, ErrNotComputed) {
+ t.Errorf("Size: got %v, want %v", err, ErrNotComputed)
+ }
+ if _, err := l.Digest(); err == nil {
+ t.Errorf("Digest: got %v, want %v", err, ErrNotComputed)
+ }
+ if _, err := l.DiffID(); err == nil {
+ t.Errorf("DiffID: got %v, want %v", err, ErrNotComputed)
+ }
+}
+
+// TestConsumed tests that Compressed returns ErrConsumed when the stream has
+// already been consumed.
+func TestConsumed(t *testing.T) {
+ l := NewLayer(io.NopCloser(strings.NewReader("hello")))
+ rc, err := l.Compressed()
+ if err != nil {
+ t.Errorf("Compressed: %v", err)
+ }
+ if _, err := io.Copy(io.Discard, rc); err != nil {
+ t.Errorf("Error reading contents: %v", err)
+ }
+ if err := rc.Close(); err != nil {
+ t.Errorf("Close: %v", err)
+ }
+
+ if _, err := l.Compressed(); !errors.Is(err, ErrConsumed) {
+ t.Errorf("Compressed() after consuming; got %v, want %v", err, ErrConsumed)
+ }
+}
+
+func TestCloseTextStreamBeforeConsume(t *testing.T) {
+ // Create stream layer from tar pipe
+ l := NewLayer(io.NopCloser(strings.NewReader("hello")))
+ rc, err := l.Compressed()
+ if err != nil {
+ t.Fatalf("Compressed: %v", err)
+ }
+
+ // Close stream layer before consuming
+ if err := rc.Close(); err != nil {
+ t.Fatalf("Close: %v", err)
+ }
+}
+
+func TestCloseTarStreamBeforeConsume(t *testing.T) {
+ // Write small tar to pipe
+ pr, pw := io.Pipe()
+ tw := tar.NewWriter(pw)
+ go func() {
+ pw.CloseWithError(func() error {
+ body := "test file"
+ if err := tw.WriteHeader(&tar.Header{
+ Name: "test.txt",
+ Mode: 0600,
+ Size: int64(len(body)),
+ Typeflag: tar.TypeReg,
+ }); err != nil {
+ return err
+ }
+ if _, err := tw.Write([]byte(body)); err != nil {
+ return err
+ }
+ return tw.Close()
+ }())
+ }()
+
+ // Create stream layer from tar pipe
+ l := NewLayer(pr)
+ rc, err := l.Compressed()
+ if err != nil {
+ t.Fatalf("Compressed: %v", err)
+ }
+
+ // Close stream layer before consuming
+ if err := rc.Close(); err != nil {
+ t.Fatalf("Close: %v", err)
+ }
+}
+
+func TestMediaType(t *testing.T) {
+ l := NewLayer(io.NopCloser(strings.NewReader("hello")))
+ mediaType, err := l.MediaType()
+
+ if err != nil {
+ t.Fatalf("MediaType(): %v", err)
+ }
+
+ if got, want := mediaType, types.DockerLayer; got != want {
+ t.Errorf("MediaType(): want %q, got %q", want, got)
+ }
+}
+
+func TestMediaTypeOption(t *testing.T) {
+ l := NewLayer(io.NopCloser(strings.NewReader("hello")), WithMediaType(types.OCILayer))
+ mediaType, err := l.MediaType()
+
+ if err != nil {
+ t.Fatalf("MediaType(): %v", err)
+ }
+
+ if got, want := mediaType, types.OCILayer; got != want {
+ t.Errorf("MediaType(): want %q, got %q", want, got)
+ }
+}
diff --git a/pkg/v1/tarball/README.md b/pkg/v1/tarball/README.md
new file mode 100644
index 0000000..03f339b
--- /dev/null
+++ b/pkg/v1/tarball/README.md
@@ -0,0 +1,280 @@
+# `tarball`
+
+[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball)
+
+This package produces tarballs that can consumed via `docker load`. Note
+that this is a _different_ format from the [`legacy`](/pkg/legacy/tarball)
+tarballs that are produced by `docker save`, but this package is still able to
+read the legacy tarballs produced by `docker save`.
+
+## Usage
+
+```go
+package main
+
+import (
+ "os"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+func main() {
+ // Read a tarball from os.Args[1] that contains ubuntu.
+ tag, err := name.NewTag("ubuntu")
+ if err != nil {
+ panic(err)
+ }
+ img, err := tarball.ImageFromPath(os.Args[1], &tag)
+ if err != nil {
+ panic(err)
+ }
+
+ // Write that tarball to os.Args[2] with a different tag.
+ newTag, err := name.NewTag("ubuntu:newest")
+ if err != nil {
+ panic(err)
+ }
+ f, err := os.Create(os.Args[2])
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+
+ if err := tarball.Write(newTag, img, f); err != nil {
+ panic(err)
+ }
+}
+```
+
+## Structure
+
+<p align="center">
+ <img src="/images/tarball.dot.svg" />
+</p>
+
+Let's look at what happens when we write out a tarball:
+
+
+### `ubuntu:latest`
+
+```
+$ crane pull ubuntu ubuntu.tar && mkdir ubuntu && tar xf ubuntu.tar -C ubuntu && rm ubuntu.tar
+$ tree ubuntu/
+ubuntu/
+├── 423ae2b273f4c17ceee9e8482fa8d071d90c7d052ae208e1fe4963fceb3d6954.tar.gz
+├── b6b53be908de2c0c78070fff0a9f04835211b3156c4e73785747af365e71a0d7.tar.gz
+├── de83a2304fa1f7c4a13708a0d15b9704f5945c2be5cbb2b3ed9b2ccb718d0b3d.tar.gz
+├── f9a83bce3af0648efaa60b9bb28225b09136d2d35d0bed25ac764297076dec1b.tar.gz
+├── manifest.json
+└── sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c
+
+0 directories, 6 files
+```
+
+There are a couple interesting files here.
+
+`manifest.json` is the entrypoint: a list of [`tarball.Descriptor`s](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball#Descriptor)
+that describe the images contained in this tarball.
+
+For each image, this has the `RepoTags` (how it was pulled), a `Config` file
+that points to the image's config file, a list of `Layers`, and (optionally)
+`LayerSources`.
+
+```
+$ jq < ubuntu/manifest.json
+[
+ {
+ "Config": "sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c",
+ "RepoTags": [
+ "ubuntu"
+ ],
+ "Layers": [
+ "423ae2b273f4c17ceee9e8482fa8d071d90c7d052ae208e1fe4963fceb3d6954.tar.gz",
+ "de83a2304fa1f7c4a13708a0d15b9704f5945c2be5cbb2b3ed9b2ccb718d0b3d.tar.gz",
+ "f9a83bce3af0648efaa60b9bb28225b09136d2d35d0bed25ac764297076dec1b.tar.gz",
+ "b6b53be908de2c0c78070fff0a9f04835211b3156c4e73785747af365e71a0d7.tar.gz"
+ ]
+ }
+]
+```
+
+The config file and layers are exactly what you would expect, and match the
+registry representations of the same artifacts. You'll notice that the
+`manifest.json` contains similar information as the registry manifest, but isn't
+quite the same:
+
+```
+$ crane manifest ubuntu@sha256:0925d086715714114c1988f7c947db94064fd385e171a63c07730f1fa014e6f9
+{
+ "schemaVersion": 2,
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "config": {
+ "mediaType": "application/vnd.docker.container.image.v1+json",
+ "size": 3408,
+ "digest": "sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c"
+ },
+ "layers": [
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "size": 26692096,
+ "digest": "sha256:423ae2b273f4c17ceee9e8482fa8d071d90c7d052ae208e1fe4963fceb3d6954"
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "size": 35365,
+ "digest": "sha256:de83a2304fa1f7c4a13708a0d15b9704f5945c2be5cbb2b3ed9b2ccb718d0b3d"
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "size": 852,
+ "digest": "sha256:f9a83bce3af0648efaa60b9bb28225b09136d2d35d0bed25ac764297076dec1b"
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "size": 163,
+ "digest": "sha256:b6b53be908de2c0c78070fff0a9f04835211b3156c4e73785747af365e71a0d7"
+ }
+ ]
+}
+```
+
+This makes it difficult to maintain image digests when roundtripping images
+through the tarball format, so it's not a great format if you care about
+provenance.
+
+The ubuntu example didn't have any `LayerSources` -- let's look at another image
+that does.
+
+### `hello-world:nanoserver`
+
+```
+$ crane pull hello-world:nanoserver@sha256:63c287625c2b0b72900e562de73c0e381472a83b1b39217aef3856cd398eca0b nanoserver.tar
+$ mkdir nanoserver && tar xf nanoserver.tar -C nanoserver && rm nanoserver.tar
+$ tree nanoserver/
+nanoserver/
+├── 10d1439be4eb8819987ec2e9c140d44d74d6b42a823d57fe1953bd99948e1bc0.tar.gz
+├── a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053.tar.gz
+├── be21f08f670160cbae227e3053205b91d6bfa3de750b90c7e00bd2c511ccb63a.tar.gz
+├── manifest.json
+└── sha256:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6
+
+0 directories, 5 files
+
+$ jq < nanoserver/manifest.json
+[
+ {
+ "Config": "sha256:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6",
+ "RepoTags": [
+ "index.docker.io/library/hello-world:i-was-a-digest"
+ ],
+ "Layers": [
+ "a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053.tar.gz",
+ "be21f08f670160cbae227e3053205b91d6bfa3de750b90c7e00bd2c511ccb63a.tar.gz",
+ "10d1439be4eb8819987ec2e9c140d44d74d6b42a823d57fe1953bd99948e1bc0.tar.gz"
+ ],
+ "LayerSources": {
+ "sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e": {
+ "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip",
+ "size": 101145811,
+ "digest": "sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053",
+ "urls": [
+ "https://mcr.microsoft.com/v2/windows/nanoserver/blobs/sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053"
+ ]
+ }
+ }
+ }
+]
+```
+
+A couple things to note about this `manifest.json` versus the other:
+* The `RepoTags` field is a bit weird here. `hello-world` is a multi-platform
+ image, so We had to pull this image by digest, since we're (I'm) on
+ amd64/linux and wanted to grab a windows image. Since the tarball format
+ expects a tag under `RepoTags`, and we didn't pull by tag, we replace the
+ digest with a sentinel `i-was-a-digest` "tag" to appease docker.
+* The `LayerSources` has enough information to reconstruct the foreign layers
+ pointer when pushing/pulling from the registry. For legal reasons, microsoft
+ doesn't want anyone but them to serve windows base images, so the mediaType
+ here indicates a "foreign" or "non-distributable" layer with an URL for where
+ you can download it from microsoft (see the [OCI
+ image-spec](https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers)).
+
+We can look at what's in the registry to explain both of these things:
+```
+$ crane manifest hello-world:nanoserver | jq .
+{
+ "manifests": [
+ {
+ "digest": "sha256:63c287625c2b0b72900e562de73c0e381472a83b1b39217aef3856cd398eca0b",
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "platform": {
+ "architecture": "amd64",
+ "os": "windows",
+ "os.version": "10.0.17763.1040"
+ },
+ "size": 1124
+ }
+ ],
+ "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
+ "schemaVersion": 2
+}
+
+
+# Note the media type and "urls" field.
+$ crane manifest hello-world:nanoserver@sha256:63c287625c2b0b72900e562de73c0e381472a83b1b39217aef3856cd398eca0b | jq .
+{
+ "schemaVersion": 2,
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "config": {
+ "mediaType": "application/vnd.docker.container.image.v1+json",
+ "size": 1721,
+ "digest": "sha256:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6"
+ },
+ "layers": [
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip",
+ "size": 101145811,
+ "digest": "sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053",
+ "urls": [
+ "https://mcr.microsoft.com/v2/windows/nanoserver/blobs/sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053"
+ ]
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "size": 1669,
+ "digest": "sha256:be21f08f670160cbae227e3053205b91d6bfa3de750b90c7e00bd2c511ccb63a"
+ },
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "size": 949,
+ "digest": "sha256:10d1439be4eb8819987ec2e9c140d44d74d6b42a823d57fe1953bd99948e1bc0"
+ }
+ ]
+}
+```
+
+The `LayerSources` map is keyed by the diffid. Note that `sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e` matches the first layer in the config file:
+```
+$ jq '.[0].LayerSources' < nanoserver/manifest.json
+{
+ "sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e": {
+ "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip",
+ "size": 101145811,
+ "digest": "sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053",
+ "urls": [
+ "https://mcr.microsoft.com/v2/windows/nanoserver/blobs/sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053"
+ ]
+ }
+}
+
+$ jq < nanoserver/sha256\:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6 | jq .rootfs
+{
+ "type": "layers",
+ "diff_ids": [
+ "sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e",
+ "sha256:601cf7d78c62e4b4d32a7bbf96a17606a9cea5bd9d22ffa6f34aa431d056b0e8",
+ "sha256:a1e1a3bf6529adcce4d91dce2cad86c2604a66b507ccbc4d2239f3da0ec5aab9"
+ ]
+}
+```
diff --git a/pkg/v1/tarball/doc.go b/pkg/v1/tarball/doc.go
new file mode 100644
index 0000000..4eb79bb
--- /dev/null
+++ b/pkg/v1/tarball/doc.go
@@ -0,0 +1,17 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package tarball provides facilities for reading/writing v1.Images from/to
+// a tarball on-disk.
+package tarball
diff --git a/pkg/v1/tarball/image.go b/pkg/v1/tarball/image.go
new file mode 100644
index 0000000..1f977e1
--- /dev/null
+++ b/pkg/v1/tarball/image.go
@@ -0,0 +1,429 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tarball
+
+import (
+ "archive/tar"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "path/filepath"
+ "sync"
+
+ comp "github.com/google/go-containerregistry/internal/compression"
+ "github.com/google/go-containerregistry/pkg/compression"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type image struct {
+ opener Opener
+ manifest *Manifest
+ config []byte
+ imgDescriptor *Descriptor
+
+ tag *name.Tag
+}
+
+type uncompressedImage struct {
+ *image
+}
+
+type compressedImage struct {
+ *image
+ manifestLock sync.Mutex // Protects manifest
+ manifest *v1.Manifest
+}
+
+var _ partial.UncompressedImageCore = (*uncompressedImage)(nil)
+var _ partial.CompressedImageCore = (*compressedImage)(nil)
+
+// Opener is a thunk for opening a tar file.
+type Opener func() (io.ReadCloser, error)
+
+func pathOpener(path string) Opener {
+ return func() (io.ReadCloser, error) {
+ return os.Open(path)
+ }
+}
+
+// ImageFromPath returns a v1.Image from a tarball located on path.
+func ImageFromPath(path string, tag *name.Tag) (v1.Image, error) {
+ return Image(pathOpener(path), tag)
+}
+
+// LoadManifest load manifest
+func LoadManifest(opener Opener) (Manifest, error) {
+ m, err := extractFileFromTar(opener, "manifest.json")
+ if err != nil {
+ return nil, err
+ }
+ defer m.Close()
+
+ var manifest Manifest
+
+ if err := json.NewDecoder(m).Decode(&manifest); err != nil {
+ return nil, err
+ }
+ return manifest, nil
+}
+
+// Image exposes an image from the tarball at the provided path.
+func Image(opener Opener, tag *name.Tag) (v1.Image, error) {
+ img := &image{
+ opener: opener,
+ tag: tag,
+ }
+ if err := img.loadTarDescriptorAndConfig(); err != nil {
+ return nil, err
+ }
+
+ // Peek at the first layer and see if it's compressed.
+ if len(img.imgDescriptor.Layers) > 0 {
+ compressed, err := img.areLayersCompressed()
+ if err != nil {
+ return nil, err
+ }
+ if compressed {
+ c := compressedImage{
+ image: img,
+ }
+ return partial.CompressedToImage(&c)
+ }
+ }
+
+ uc := uncompressedImage{
+ image: img,
+ }
+ return partial.UncompressedToImage(&uc)
+}
+
+func (i *image) MediaType() (types.MediaType, error) {
+ return types.DockerManifestSchema2, nil
+}
+
+// Descriptor stores the manifest data for a single image inside a `docker save` tarball.
+type Descriptor struct {
+ Config string
+ RepoTags []string
+ Layers []string
+
+ // Tracks foreign layer info. Key is DiffID.
+ LayerSources map[v1.Hash]v1.Descriptor `json:",omitempty"`
+}
+
+// Manifest represents the manifests of all images as the `manifest.json` file in a `docker save` tarball.
+type Manifest []Descriptor
+
+func (m Manifest) findDescriptor(tag *name.Tag) (*Descriptor, error) {
+ if tag == nil {
+ if len(m) != 1 {
+ return nil, errors.New("tarball must contain only a single image to be used with tarball.Image")
+ }
+ return &(m)[0], nil
+ }
+ for _, img := range m {
+ for _, tagStr := range img.RepoTags {
+ repoTag, err := name.NewTag(tagStr)
+ if err != nil {
+ return nil, err
+ }
+
+ // Compare the resolved names, since there are several ways to specify the same tag.
+ if repoTag.Name() == tag.Name() {
+ return &img, nil
+ }
+ }
+ }
+ return nil, fmt.Errorf("tag %s not found in tarball", tag)
+}
+
+func (i *image) areLayersCompressed() (bool, error) {
+ if len(i.imgDescriptor.Layers) == 0 {
+ return false, errors.New("0 layers found in image")
+ }
+ layer := i.imgDescriptor.Layers[0]
+ blob, err := extractFileFromTar(i.opener, layer)
+ if err != nil {
+ return false, err
+ }
+ defer blob.Close()
+
+ cp, _, err := comp.PeekCompression(blob)
+ if err != nil {
+ return false, err
+ }
+
+ return cp != compression.None, nil
+}
+
+func (i *image) loadTarDescriptorAndConfig() error {
+ m, err := extractFileFromTar(i.opener, "manifest.json")
+ if err != nil {
+ return err
+ }
+ defer m.Close()
+
+ if err := json.NewDecoder(m).Decode(&i.manifest); err != nil {
+ return err
+ }
+
+ if i.manifest == nil {
+ return errors.New("no valid manifest.json in tarball")
+ }
+
+ i.imgDescriptor, err = i.manifest.findDescriptor(i.tag)
+ if err != nil {
+ return err
+ }
+
+ cfg, err := extractFileFromTar(i.opener, i.imgDescriptor.Config)
+ if err != nil {
+ return err
+ }
+ defer cfg.Close()
+
+ i.config, err = io.ReadAll(cfg)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (i *image) RawConfigFile() ([]byte, error) {
+ return i.config, nil
+}
+
+// tarFile represents a single file inside a tar. Closing it closes the tar itself.
+type tarFile struct {
+ io.Reader
+ io.Closer
+}
+
+func extractFileFromTar(opener Opener, filePath string) (io.ReadCloser, error) {
+ f, err := opener()
+ if err != nil {
+ return nil, err
+ }
+ close := true
+ defer func() {
+ if close {
+ f.Close()
+ }
+ }()
+
+ tf := tar.NewReader(f)
+ for {
+ hdr, err := tf.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if hdr.Name == filePath {
+ if hdr.Typeflag == tar.TypeSymlink || hdr.Typeflag == tar.TypeLink {
+ currentDir := filepath.Dir(filePath)
+ return extractFileFromTar(opener, path.Join(currentDir, path.Clean(hdr.Linkname)))
+ }
+ close = false
+ return tarFile{
+ Reader: tf,
+ Closer: f,
+ }, nil
+ }
+ }
+ return nil, fmt.Errorf("file %s not found in tar", filePath)
+}
+
+// uncompressedLayerFromTarball implements partial.UncompressedLayer
+type uncompressedLayerFromTarball struct {
+ diffID v1.Hash
+ mediaType types.MediaType
+ opener Opener
+ filePath string
+}
+
+// foreignUncompressedLayer implements partial.UncompressedLayer but returns
+// a custom descriptor. This allows the foreign layer URLs to be included in
+// the generated image manifest for uncompressed layers.
+type foreignUncompressedLayer struct {
+ uncompressedLayerFromTarball
+ desc v1.Descriptor
+}
+
+func (fl *foreignUncompressedLayer) Descriptor() (*v1.Descriptor, error) {
+ return &fl.desc, nil
+}
+
+// DiffID implements partial.UncompressedLayer
+func (ulft *uncompressedLayerFromTarball) DiffID() (v1.Hash, error) {
+ return ulft.diffID, nil
+}
+
+// Uncompressed implements partial.UncompressedLayer
+func (ulft *uncompressedLayerFromTarball) Uncompressed() (io.ReadCloser, error) {
+ return extractFileFromTar(ulft.opener, ulft.filePath)
+}
+
+func (ulft *uncompressedLayerFromTarball) MediaType() (types.MediaType, error) {
+ return ulft.mediaType, nil
+}
+
+func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) {
+ cfg, err := partial.ConfigFile(i)
+ if err != nil {
+ return nil, err
+ }
+ for idx, diffID := range cfg.RootFS.DiffIDs {
+ if diffID == h {
+ // Technically the media type should be 'application/tar' but given that our
+ // v1.Layer doesn't force consumers to care about whether the layer is compressed
+ // we should be fine returning the DockerLayer media type
+ mt := types.DockerLayer
+ if bd, ok := i.imgDescriptor.LayerSources[h]; ok {
+ // Overwrite the mediaType for foreign layers.
+ return &foreignUncompressedLayer{
+ uncompressedLayerFromTarball: uncompressedLayerFromTarball{
+ diffID: diffID,
+ mediaType: bd.MediaType,
+ opener: i.opener,
+ filePath: i.imgDescriptor.Layers[idx],
+ },
+ desc: bd,
+ }, nil
+ }
+ return &uncompressedLayerFromTarball{
+ diffID: diffID,
+ mediaType: mt,
+ opener: i.opener,
+ filePath: i.imgDescriptor.Layers[idx],
+ }, nil
+ }
+ }
+ return nil, fmt.Errorf("diff id %q not found", h)
+}
+
+func (c *compressedImage) Manifest() (*v1.Manifest, error) {
+ c.manifestLock.Lock()
+ defer c.manifestLock.Unlock()
+ if c.manifest != nil {
+ return c.manifest, nil
+ }
+
+ b, err := c.RawConfigFile()
+ if err != nil {
+ return nil, err
+ }
+
+ cfgHash, cfgSize, err := v1.SHA256(bytes.NewReader(b))
+ if err != nil {
+ return nil, err
+ }
+
+ c.manifest = &v1.Manifest{
+ SchemaVersion: 2,
+ MediaType: types.DockerManifestSchema2,
+ Config: v1.Descriptor{
+ MediaType: types.DockerConfigJSON,
+ Size: cfgSize,
+ Digest: cfgHash,
+ },
+ }
+
+ for i, p := range c.imgDescriptor.Layers {
+ cfg, err := partial.ConfigFile(c)
+ if err != nil {
+ return nil, err
+ }
+ diffid := cfg.RootFS.DiffIDs[i]
+ if d, ok := c.imgDescriptor.LayerSources[diffid]; ok {
+ // If it's a foreign layer, just append the descriptor so we can avoid
+ // reading the entire file.
+ c.manifest.Layers = append(c.manifest.Layers, d)
+ } else {
+ l, err := extractFileFromTar(c.opener, p)
+ if err != nil {
+ return nil, err
+ }
+ defer l.Close()
+ sha, size, err := v1.SHA256(l)
+ if err != nil {
+ return nil, err
+ }
+ c.manifest.Layers = append(c.manifest.Layers, v1.Descriptor{
+ MediaType: types.DockerLayer,
+ Size: size,
+ Digest: sha,
+ })
+ }
+ }
+ return c.manifest, nil
+}
+
+func (c *compressedImage) RawManifest() ([]byte, error) {
+ return partial.RawManifest(c)
+}
+
+// compressedLayerFromTarball implements partial.CompressedLayer
+type compressedLayerFromTarball struct {
+ desc v1.Descriptor
+ opener Opener
+ filePath string
+}
+
+// Digest implements partial.CompressedLayer
+func (clft *compressedLayerFromTarball) Digest() (v1.Hash, error) {
+ return clft.desc.Digest, nil
+}
+
+// Compressed implements partial.CompressedLayer
+func (clft *compressedLayerFromTarball) Compressed() (io.ReadCloser, error) {
+ return extractFileFromTar(clft.opener, clft.filePath)
+}
+
+// MediaType implements partial.CompressedLayer
+func (clft *compressedLayerFromTarball) MediaType() (types.MediaType, error) {
+ return clft.desc.MediaType, nil
+}
+
+// Size implements partial.CompressedLayer
+func (clft *compressedLayerFromTarball) Size() (int64, error) {
+ return clft.desc.Size, nil
+}
+
+func (c *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
+ m, err := c.Manifest()
+ if err != nil {
+ return nil, err
+ }
+ for i, l := range m.Layers {
+ if l.Digest == h {
+ fp := c.imgDescriptor.Layers[i]
+ return &compressedLayerFromTarball{
+ desc: l,
+ opener: c.opener,
+ filePath: fp,
+ }, nil
+ }
+ }
+ return nil, fmt.Errorf("blob %v not found", h)
+}
diff --git a/pkg/v1/tarball/image_test.go b/pkg/v1/tarball/image_test.go
new file mode 100644
index 0000000..3a46400
--- /dev/null
+++ b/pkg/v1/tarball/image_test.go
@@ -0,0 +1,139 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tarball
+
+import (
+ "io"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestManifestAndConfig(t *testing.T) {
+ img, err := ImageFromPath("testdata/test_image_1.tar", nil)
+ if err != nil {
+ t.Fatalf("Error loading image: %v", err)
+ }
+ manifest, err := img.Manifest()
+ if err != nil {
+ t.Fatalf("Error loading manifest: %v", err)
+ }
+ if len(manifest.Layers) != 1 {
+ t.Fatalf("layers should be 1, got %d", len(manifest.Layers))
+ }
+
+ config, err := img.ConfigFile()
+ if err != nil {
+ t.Fatalf("Error loading config file: %v", err)
+ }
+ if len(config.History) != 1 {
+ t.Fatalf("history length should be 1, got %d", len(config.History))
+ }
+
+ if err := validate.Image(img); err != nil {
+ t.Errorf("Validate() = %v", err)
+ }
+}
+
+func TestNullManifest(t *testing.T) {
+ img, err := ImageFromPath("testdata/null_manifest.tar", nil)
+ if err == nil {
+ t.Fatalf("Error expected loading null image: %v", img)
+ }
+}
+
+func TestNoManifest(t *testing.T) {
+ img, err := ImageFromPath("testdata/no_manifest.tar", nil)
+ if err == nil {
+ t.Fatalf("Error expected loading image: %v", img)
+ }
+}
+
+func TestBundleSingle(t *testing.T) {
+ img, err := ImageFromPath("testdata/test_bundle.tar", nil)
+ if err == nil {
+ t.Fatalf("Error expected loading image: %v", img)
+ }
+}
+
+func TestBundleMultiple(t *testing.T) {
+ for _, imgName := range []string{
+ "test_image_1",
+ "test_image_2",
+ "test_image_1:latest",
+ "test_image_2:latest",
+ "index.docker.io/library/test_image_1:latest",
+ } {
+ t.Run(imgName, func(t *testing.T) {
+ tag, err := name.NewTag(imgName, name.WeakValidation)
+ if err != nil {
+ t.Fatalf("Error creating tag: %v", err)
+ }
+ img, err := ImageFromPath("testdata/test_bundle.tar", &tag)
+ if err != nil {
+ t.Fatalf("Error loading image: %v", err)
+ }
+ if _, err := img.Manifest(); err != nil {
+ t.Fatalf("Unexpected error loading manifest: %v", err)
+ }
+
+ if err := validate.Image(img); err != nil {
+ t.Errorf("Validate() = %v", err)
+ }
+ })
+ }
+}
+
+func TestLayerLink(t *testing.T) {
+ tag, err := name.NewTag("bazel/v1/tarball:test_image_3", name.WeakValidation)
+ if err != nil {
+ t.Fatalf("Error creating tag: %v", err)
+ }
+ img, err := ImageFromPath("testdata/test_link.tar", &tag)
+ if err != nil {
+ t.Fatalf("Error loading image: %v", img)
+ }
+ hash := v1.Hash{
+ Algorithm: "sha256",
+ Hex: "8897395fd26dc44ad0e2a834335b33198cb41ac4d98dfddf58eced3853fa7b17",
+ }
+ layer, err := img.LayerByDiffID(hash)
+ if err != nil {
+ t.Fatalf("Error getting layer by diff ID: %v, %v", hash, err)
+ }
+ rc, err := layer.Uncompressed()
+ if err != nil {
+ t.Fatal(err)
+ }
+ bs, err := io.ReadAll(rc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(bs) == 0 {
+ t.Errorf("layer.Uncompressed() returned a link file")
+ }
+}
+
+func TestLoadManifest(t *testing.T) {
+ manifest, err := LoadManifest(pathOpener("testdata/test_load_manifest.tar"))
+ if err != nil {
+ t.Fatalf("Error load manifest: %v", err)
+ }
+ if len(manifest) == 0 {
+ t.Fatalf("get nothing")
+ }
+}
diff --git a/pkg/v1/tarball/layer.go b/pkg/v1/tarball/layer.go
new file mode 100644
index 0000000..a344e92
--- /dev/null
+++ b/pkg/v1/tarball/layer.go
@@ -0,0 +1,349 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tarball
+
+import (
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "os"
+ "sync"
+
+ "github.com/containerd/stargz-snapshotter/estargz"
+ "github.com/google/go-containerregistry/internal/and"
+ comp "github.com/google/go-containerregistry/internal/compression"
+ gestargz "github.com/google/go-containerregistry/internal/estargz"
+ ggzip "github.com/google/go-containerregistry/internal/gzip"
+ "github.com/google/go-containerregistry/internal/zstd"
+ "github.com/google/go-containerregistry/pkg/compression"
+ "github.com/google/go-containerregistry/pkg/logs"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+type layer struct {
+ digest v1.Hash
+ diffID v1.Hash
+ size int64
+ compressedopener Opener
+ uncompressedopener Opener
+ compression compression.Compression
+ compressionLevel int
+ annotations map[string]string
+ estgzopts []estargz.Option
+ mediaType types.MediaType
+}
+
+// Descriptor implements partial.withDescriptor.
+func (l *layer) Descriptor() (*v1.Descriptor, error) {
+ digest, err := l.Digest()
+ if err != nil {
+ return nil, err
+ }
+ return &v1.Descriptor{
+ Size: l.size,
+ Digest: digest,
+ Annotations: l.annotations,
+ MediaType: l.mediaType,
+ }, nil
+}
+
+// Digest implements v1.Layer
+func (l *layer) Digest() (v1.Hash, error) {
+ return l.digest, nil
+}
+
+// DiffID implements v1.Layer
+func (l *layer) DiffID() (v1.Hash, error) {
+ return l.diffID, nil
+}
+
+// Compressed implements v1.Layer
+func (l *layer) Compressed() (io.ReadCloser, error) {
+ return l.compressedopener()
+}
+
+// Uncompressed implements v1.Layer
+func (l *layer) Uncompressed() (io.ReadCloser, error) {
+ return l.uncompressedopener()
+}
+
+// Size implements v1.Layer
+func (l *layer) Size() (int64, error) {
+ return l.size, nil
+}
+
+// MediaType implements v1.Layer
+func (l *layer) MediaType() (types.MediaType, error) {
+ return l.mediaType, nil
+}
+
+// LayerOption applies options to layer
+type LayerOption func(*layer)
+
+// WithCompression is a functional option for overriding the default
+// compression algorithm used for compressing uncompressed tarballs.
+// Please note that WithCompression(compression.ZStd) should be used
+// in conjunction with WithMediaType(types.OCILayerZStd)
+func WithCompression(comp compression.Compression) LayerOption {
+ return func(l *layer) {
+ switch comp {
+ case compression.ZStd:
+ l.compression = compression.ZStd
+ case compression.GZip:
+ l.compression = compression.GZip
+ case compression.None:
+ logs.Warn.Printf("Compression type 'none' is not supported for tarball layers; using gzip compression.")
+ l.compression = compression.GZip
+ default:
+ logs.Warn.Printf("Unexpected compression type for WithCompression(): %s; using gzip compression instead.", comp)
+ l.compression = compression.GZip
+ }
+ }
+}
+
+// WithCompressionLevel is a functional option for overriding the default
+// compression level used for compressing uncompressed tarballs.
+func WithCompressionLevel(level int) LayerOption {
+ return func(l *layer) {
+ l.compressionLevel = level
+ }
+}
+
+// WithMediaType is a functional option for overriding the layer's media type.
+func WithMediaType(mt types.MediaType) LayerOption {
+ return func(l *layer) {
+ l.mediaType = mt
+ }
+}
+
+// WithCompressedCaching is a functional option that overrides the
+// logic for accessing the compressed bytes to memoize the result
+// and avoid expensive repeated gzips.
+func WithCompressedCaching(l *layer) {
+ var once sync.Once
+ var err error
+
+ buf := bytes.NewBuffer(nil)
+ og := l.compressedopener
+
+ l.compressedopener = func() (io.ReadCloser, error) {
+ once.Do(func() {
+ var rc io.ReadCloser
+ rc, err = og()
+ if err == nil {
+ defer rc.Close()
+ _, err = io.Copy(buf, rc)
+ }
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return io.NopCloser(bytes.NewBuffer(buf.Bytes())), nil
+ }
+}
+
+// WithEstargzOptions is a functional option that allow the caller to pass
+// through estargz.Options to the underlying compression layer. This is
+// only meaningful when estargz is enabled.
+func WithEstargzOptions(opts ...estargz.Option) LayerOption {
+ return func(l *layer) {
+ l.estgzopts = opts
+ }
+}
+
+// WithEstargz is a functional option that explicitly enables estargz support.
+func WithEstargz(l *layer) {
+ oguncompressed := l.uncompressedopener
+ estargz := func() (io.ReadCloser, error) {
+ crc, err := oguncompressed()
+ if err != nil {
+ return nil, err
+ }
+ eopts := append(l.estgzopts, estargz.WithCompressionLevel(l.compressionLevel))
+ rc, h, err := gestargz.ReadCloser(crc, eopts...)
+ if err != nil {
+ return nil, err
+ }
+ l.annotations[estargz.TOCJSONDigestAnnotation] = h.String()
+ return &and.ReadCloser{
+ Reader: rc,
+ CloseFunc: func() error {
+ err := rc.Close()
+ if err != nil {
+ return err
+ }
+ // As an optimization, leverage the DiffID exposed by the estargz ReadCloser
+ l.diffID, err = v1.NewHash(rc.DiffID().String())
+ return err
+ },
+ }, nil
+ }
+ uncompressed := func() (io.ReadCloser, error) {
+ urc, err := estargz()
+ if err != nil {
+ return nil, err
+ }
+ return ggzip.UnzipReadCloser(urc)
+ }
+
+ l.compressedopener = estargz
+ l.uncompressedopener = uncompressed
+}
+
+// LayerFromFile returns a v1.Layer given a tarball
+func LayerFromFile(path string, opts ...LayerOption) (v1.Layer, error) {
+ opener := func() (io.ReadCloser, error) {
+ return os.Open(path)
+ }
+ return LayerFromOpener(opener, opts...)
+}
+
+// LayerFromOpener returns a v1.Layer given an Opener function.
+// The Opener may return either an uncompressed tarball (common),
+// or a compressed tarball (uncommon).
+//
+// When using this in conjunction with something like remote.Write
+// the uncompressed path may end up gzipping things multiple times:
+// 1. Compute the layer SHA256
+// 2. Upload the compressed layer.
+//
+// Since gzip can be expensive, we support an option to memoize the
+// compression that can be passed here: tarball.WithCompressedCaching
+func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
+ comp, err := comp.GetCompression(opener)
+ if err != nil {
+ return nil, err
+ }
+
+ layer := &layer{
+ compression: compression.GZip,
+ compressionLevel: gzip.BestSpeed,
+ annotations: make(map[string]string, 1),
+ mediaType: types.DockerLayer,
+ }
+
+ if estgz := os.Getenv("GGCR_EXPERIMENT_ESTARGZ"); estgz == "1" {
+ opts = append([]LayerOption{WithEstargz}, opts...)
+ }
+
+ switch comp {
+ case compression.GZip:
+ layer.compressedopener = opener
+ layer.uncompressedopener = func() (io.ReadCloser, error) {
+ urc, err := opener()
+ if err != nil {
+ return nil, err
+ }
+ return ggzip.UnzipReadCloser(urc)
+ }
+ case compression.ZStd:
+ layer.compressedopener = opener
+ layer.uncompressedopener = func() (io.ReadCloser, error) {
+ urc, err := opener()
+ if err != nil {
+ return nil, err
+ }
+ return zstd.UnzipReadCloser(urc)
+ }
+ default:
+ layer.uncompressedopener = opener
+ layer.compressedopener = func() (io.ReadCloser, error) {
+ crc, err := opener()
+ if err != nil {
+ return nil, err
+ }
+
+ if layer.compression == compression.ZStd {
+ return zstd.ReadCloserLevel(crc, layer.compressionLevel), nil
+ }
+
+ return ggzip.ReadCloserLevel(crc, layer.compressionLevel), nil
+ }
+ }
+
+ for _, opt := range opts {
+ opt(layer)
+ }
+
+ // Warn if media type does not match compression
+ var mediaTypeMismatch = false
+ switch layer.compression {
+ case compression.GZip:
+ mediaTypeMismatch =
+ layer.mediaType != types.OCILayer &&
+ layer.mediaType != types.OCIRestrictedLayer &&
+ layer.mediaType != types.DockerLayer
+
+ case compression.ZStd:
+ mediaTypeMismatch = layer.mediaType != types.OCILayerZStd
+ }
+
+ if mediaTypeMismatch {
+ logs.Warn.Printf("Unexpected mediaType (%s) for selected compression in %s in LayerFromOpener().", layer.mediaType, layer.compression)
+ }
+
+ if layer.digest, layer.size, err = computeDigest(layer.compressedopener); err != nil {
+ return nil, err
+ }
+
+ empty := v1.Hash{}
+ if layer.diffID == empty {
+ if layer.diffID, err = computeDiffID(layer.uncompressedopener); err != nil {
+ return nil, err
+ }
+ }
+
+ return layer, nil
+}
+
+// LayerFromReader returns a v1.Layer given a io.Reader.
+//
+// The reader's contents are read and buffered to a temp file in the process.
+//
+// Deprecated: Use LayerFromOpener or stream.NewLayer instead, if possible.
+func LayerFromReader(reader io.Reader, opts ...LayerOption) (v1.Layer, error) {
+ tmp, err := os.CreateTemp("", "")
+ if err != nil {
+ return nil, fmt.Errorf("creating temp file to buffer reader: %w", err)
+ }
+ if _, err := io.Copy(tmp, reader); err != nil {
+ return nil, fmt.Errorf("writing temp file to buffer reader: %w", err)
+ }
+ return LayerFromFile(tmp.Name(), opts...)
+}
+
+func computeDigest(opener Opener) (v1.Hash, int64, error) {
+ rc, err := opener()
+ if err != nil {
+ return v1.Hash{}, 0, err
+ }
+ defer rc.Close()
+
+ return v1.SHA256(rc)
+}
+
+func computeDiffID(opener Opener) (v1.Hash, error) {
+ rc, err := opener()
+ if err != nil {
+ return v1.Hash{}, err
+ }
+ defer rc.Close()
+
+ digest, _, err := v1.SHA256(rc)
+ return digest, err
+}
diff --git a/pkg/v1/tarball/layer_test.go b/pkg/v1/tarball/layer_test.go
new file mode 100644
index 0000000..5d93360
--- /dev/null
+++ b/pkg/v1/tarball/layer_test.go
@@ -0,0 +1,381 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tarball
+
+import (
+ "bytes"
+ "compress/gzip"
+ "io"
+ "os"
+ "testing"
+
+ "github.com/containerd/stargz-snapshotter/estargz"
+ "github.com/google/go-containerregistry/internal/compare"
+ "github.com/google/go-containerregistry/pkg/compression"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestLayerFromFile(t *testing.T) {
+ setupFixtures(t)
+ defer teardownFixtures(t)
+
+ tarLayer, err := LayerFromFile("testdata/content.tar")
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+
+ tarGzLayer, err := LayerFromFile("gzip_content.tgz")
+ if err != nil {
+ t.Fatalf("Unable to create layer from compressed tar file: %v", err)
+ }
+
+ tarZstdLayer, err := LayerFromFile("zstd_content.tar.zst")
+ if err != nil {
+ t.Fatalf("Unable to create layer from compressed tar file: %v", err)
+ }
+
+ if err := compare.Layers(tarLayer, tarGzLayer); err != nil {
+ t.Errorf("compare.Layers: %v", err)
+ }
+
+ if err := compare.Layers(tarLayer, tarZstdLayer); err != nil {
+ t.Errorf("compare.Layers: %v", err)
+ }
+
+ if err := validate.Layer(tarLayer); err != nil {
+ t.Errorf("validate.Layer(tarLayer): %v", err)
+ }
+
+ if err := validate.Layer(tarGzLayer); err != nil {
+ t.Errorf("validate.Layer(tarGzLayer): %v", err)
+ }
+
+ if err := validate.Layer(tarZstdLayer); err != nil {
+ t.Errorf("validate.Layer(tarZstdLayer): %v", err)
+ }
+
+ getTestDigest := func(testName string, opts ...LayerOption) v1.Hash {
+ layer, err := LayerFromFile("testdata/content.tar", opts...)
+ if err != nil {
+ t.Fatalf("Unable to create layer with '%s' compression from tar file: %v", testName, err)
+ }
+
+ digest, err := layer.Digest()
+ if err != nil {
+ t.Fatalf("Unable to generate digest with '%s' compression: %v", testName, err)
+ }
+
+ return digest
+ }
+
+ defaultDigest := getTestDigest("Gzip Default", WithCompressionLevel(gzip.DefaultCompression))
+ speedDigest := getTestDigest("Gzip BestSpeed", WithCompressionLevel(gzip.BestSpeed))
+ zstdDigest := getTestDigest("Zstd Default", WithCompression(compression.ZStd))
+ zstdDigest1 := getTestDigest("Zstd BestSpeed", WithCompression(compression.ZStd), WithCompressionLevel(1))
+
+ if defaultDigest.String() == speedDigest.String() {
+ t.Errorf("expected digests to differ: %s", defaultDigest.String())
+ }
+
+ if defaultDigest.String() == zstdDigest.String() {
+ t.Errorf("expected digests to differ: %s", defaultDigest.String())
+ }
+
+ if defaultDigest.String() == zstdDigest1.String() {
+ t.Errorf("expected digests to differ: %s", defaultDigest.String())
+ }
+}
+
+func TestLayerFromFileEstargz(t *testing.T) {
+ setupFixtures(t)
+ defer teardownFixtures(t)
+
+ tarLayer, err := LayerFromFile("testdata/content.tar", WithEstargz)
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+
+ if err := validate.Layer(tarLayer); err != nil {
+ t.Errorf("validate.Layer(tarLayer): %v", err)
+ }
+
+ tarLayerDefaultCompression, err := LayerFromFile("testdata/content.tar", WithEstargz, WithCompressionLevel(gzip.DefaultCompression))
+ if err != nil {
+ t.Fatalf("Unable to create layer with 'Default' compression from tar file: %v", err)
+ }
+ descriptorDefaultCompression, err := tarLayerDefaultCompression.(*layer).Descriptor()
+ if err != nil {
+ t.Fatalf("Descriptor() = %v", err)
+ } else if len(descriptorDefaultCompression.Annotations) != 1 {
+ t.Errorf("Annotations = %#v, wanted 1 annotation", descriptorDefaultCompression.Annotations)
+ }
+
+ defaultDigest, err := tarLayerDefaultCompression.Digest()
+ if err != nil {
+ t.Fatal("Unable to generate digest with 'Default' compression", err)
+ }
+
+ tarLayerSpeedCompression, err := LayerFromFile("testdata/content.tar", WithEstargz, WithCompressionLevel(gzip.BestSpeed))
+ if err != nil {
+ t.Fatalf("Unable to create layer with 'BestSpeed' compression from tar file: %v", err)
+ }
+ descriptorSpeedCompression, err := tarLayerSpeedCompression.(*layer).Descriptor()
+ if err != nil {
+ t.Fatalf("Descriptor() = %v", err)
+ } else if len(descriptorSpeedCompression.Annotations) != 1 {
+ t.Errorf("Annotations = %#v, wanted 1 annotation", descriptorSpeedCompression.Annotations)
+ }
+
+ speedDigest, err := tarLayerSpeedCompression.Digest()
+ if err != nil {
+ t.Fatal("Unable to generate digest with 'BestSpeed' compression", err)
+ }
+
+ if defaultDigest.String() == speedDigest.String() {
+ t.Errorf("expected digests to differ: %s", defaultDigest.String())
+ }
+
+ if descriptorDefaultCompression.Annotations[estargz.TOCJSONDigestAnnotation] == descriptorSpeedCompression.Annotations[estargz.TOCJSONDigestAnnotation] {
+ t.Errorf("wanted different toc digests got default: %s, speed: %s",
+ descriptorDefaultCompression.Annotations[estargz.TOCJSONDigestAnnotation],
+ descriptorSpeedCompression.Annotations[estargz.TOCJSONDigestAnnotation])
+ }
+
+ tarLayerPrioritizedFiles, err := LayerFromFile("testdata/content.tar",
+ WithEstargz,
+ // We compare with default, so pass for apples-to-apples comparison.
+ WithCompressionLevel(gzip.DefaultCompression),
+ // By passing a list of priority files, we expect the layer to be different.
+ WithEstargzOptions(estargz.WithPrioritizedFiles([]string{
+ "./bat",
+ })))
+ if err != nil {
+ t.Fatalf("Unable to create layer with prioritized files from tar file: %v", err)
+ }
+ descriptorPrioritizedFiles, err := tarLayerPrioritizedFiles.(*layer).Descriptor()
+ if err != nil {
+ t.Fatalf("Descriptor() = %v", err)
+ } else if len(descriptorPrioritizedFiles.Annotations) != 1 {
+ t.Errorf("Annotations = %#v, wanted 1 annotation", descriptorPrioritizedFiles.Annotations)
+ }
+
+ prioritizedDigest, err := tarLayerPrioritizedFiles.Digest()
+ if err != nil {
+ t.Fatal("Unable to generate digest with prioritized files", err)
+ }
+
+ if defaultDigest.String() == prioritizedDigest.String() {
+ t.Errorf("expected digests to differ: %s", defaultDigest.String())
+ }
+
+ if descriptorDefaultCompression.Annotations[estargz.TOCJSONDigestAnnotation] == descriptorPrioritizedFiles.Annotations[estargz.TOCJSONDigestAnnotation] {
+ t.Errorf("wanted different toc digests got default: %s, prioritized: %s",
+ descriptorDefaultCompression.Annotations[estargz.TOCJSONDigestAnnotation],
+ descriptorPrioritizedFiles.Annotations[estargz.TOCJSONDigestAnnotation])
+ }
+}
+
+func TestLayerFromOpenerReader(t *testing.T) {
+ setupFixtures(t)
+ defer teardownFixtures(t)
+
+ ucBytes, err := os.ReadFile("testdata/content.tar")
+ if err != nil {
+ t.Fatalf("Unable to read tar file: %v", err)
+ }
+ count := 0
+ ucOpener := func() (io.ReadCloser, error) {
+ count++
+ return io.NopCloser(bytes.NewReader(ucBytes)), nil
+ }
+ tarLayer, err := LayerFromOpener(ucOpener, WithCompressedCaching)
+ if err != nil {
+ t.Fatal("Unable to create layer from tar file:", err)
+ }
+ for i := 0; i < 10; i++ {
+ tarLayer.Compressed()
+ }
+
+ // Store the count and reset the counter.
+ cachedCount := count
+ count = 0
+
+ tarLayer, err = LayerFromOpener(ucOpener)
+ if err != nil {
+ t.Fatal("Unable to create layer from tar file:", err)
+ }
+ for i := 0; i < 10; i++ {
+ tarLayer.Compressed()
+ }
+
+ // We expect three calls: gzip sniff, diffid computation, cached compression
+ if cachedCount != 3 {
+ t.Errorf("cached count = %d, wanted %d", cachedCount, 3)
+ }
+ if cachedCount+10 != count {
+ t.Errorf("count = %d, wanted %d", count, cachedCount+10)
+ }
+
+ gzBytes, err := os.ReadFile("gzip_content.tgz")
+ if err != nil {
+ t.Fatalf("Unable to read tar file: %v", err)
+ }
+ gzOpener := func() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(gzBytes)), nil
+ }
+ tarGzLayer, err := LayerFromOpener(gzOpener)
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+
+ if err := compare.Layers(tarLayer, tarGzLayer); err != nil {
+ t.Errorf("compare.Layers: %v", err)
+ }
+
+ zstdBytes, err := os.ReadFile("zstd_content.tar.zst")
+ if err != nil {
+ t.Fatalf("Unable to read tar file: %v", err)
+ }
+ zstdOpener := func() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(zstdBytes)), nil
+ }
+ tarZstdLayer, err := LayerFromOpener(zstdOpener)
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+
+ if err := compare.Layers(tarLayer, tarZstdLayer); err != nil {
+ t.Errorf("compare.Layers: %v", err)
+ }
+}
+
+func TestWithMediaType(t *testing.T) {
+ setupFixtures(t)
+ defer teardownFixtures(t)
+
+ l, err := LayerFromFile("testdata/content.tar")
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+ got, err := l.MediaType()
+ if err != nil {
+ t.Fatalf("MediaType: %v", err)
+ }
+ if want := types.DockerLayer; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ l, err = LayerFromFile("testdata/content.tar", WithMediaType(types.OCILayer))
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+ got, err = l.MediaType()
+ if err != nil {
+ t.Fatalf("MediaType: %v", err)
+ }
+ if want := types.OCILayer; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+}
+
+func TestLayerFromReader(t *testing.T) {
+ setupFixtures(t)
+ defer teardownFixtures(t)
+
+ ucBytes, err := os.ReadFile("testdata/content.tar")
+ if err != nil {
+ t.Fatalf("Unable to read tar file: %v", err)
+ }
+ tarLayer, err := LayerFromReader(bytes.NewReader(ucBytes))
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+
+ gzBytes, err := os.ReadFile("gzip_content.tgz")
+ if err != nil {
+ t.Fatalf("Unable to read tar file: %v", err)
+ }
+ tarGzLayer, err := LayerFromReader(bytes.NewReader(gzBytes))
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+
+ if err := compare.Layers(tarLayer, tarGzLayer); err != nil {
+ t.Errorf("compare.Layers: %v", err)
+ }
+
+ zstdBytes, err := os.ReadFile("zstd_content.tar.zst")
+ if err != nil {
+ t.Fatalf("Unable to read tar file: %v", err)
+ }
+ tarZstdLayer, err := LayerFromReader(bytes.NewReader(zstdBytes))
+ if err != nil {
+ t.Fatalf("Unable to create layer from tar file: %v", err)
+ }
+
+ if err := compare.Layers(tarLayer, tarZstdLayer); err != nil {
+ t.Errorf("compare.Layers: %v", err)
+ }
+}
+
+// Compression settings matter in order for the digest, size,
+// compressed assertions to pass
+//
+// Since our gzip.GzipReadCloser uses gzip.BestSpeed
+// we need our fixture to use the same - bazel's pkg_tar doesn't
+// seem to let you control compression settings
+func setupFixtures(t *testing.T) {
+ t.Helper()
+
+ setupCompressedTar(t, "gzip_content.tgz")
+ setupCompressedTar(t, "zstd_content.tar.zst")
+}
+
+func setupCompressedTar(t *testing.T, fileName string) {
+ t.Helper()
+ in, err := os.Open("testdata/content.tar")
+ if err != nil {
+ t.Errorf("Error setting up fixtures: %v", err)
+ }
+
+ defer in.Close()
+
+ out, err := os.Create(fileName)
+ if err != nil {
+ t.Errorf("Error setting up fixtures: %v", err)
+ }
+
+ defer out.Close()
+
+ gw, _ := gzip.NewWriterLevel(out, gzip.BestSpeed)
+ defer gw.Close()
+
+ _, err = io.Copy(gw, in)
+ if err != nil {
+ t.Errorf("Error setting up fixtures: %v", err)
+ }
+}
+
+func teardownFixtures(t *testing.T) {
+ t.Helper()
+ if err := os.Remove("gzip_content.tgz"); err != nil {
+ t.Errorf("Error tearing down fixtures: %v", err)
+ }
+ if err := os.Remove("zstd_content.tar.zst"); err != nil {
+ t.Errorf("Error tearing down fixtures: %v", err)
+ }
+}
diff --git a/pkg/v1/tarball/progress_test.go b/pkg/v1/tarball/progress_test.go
new file mode 100644
index 0000000..c722749
--- /dev/null
+++ b/pkg/v1/tarball/progress_test.go
@@ -0,0 +1,57 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tarball_test
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+)
+
+func ExampleWithProgress() {
+ // buffered channel to make the example test easier
+ c := make(chan v1.Update, 200)
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ fmt.Printf("error creating temp file: %v\n", err)
+ return
+ }
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ img, err := tarball.ImageFromPath("testdata/test_image_1.tar", nil)
+ go func() {
+ _ = tarball.WriteToFile(fp.Name(), nil, img, tarball.WithProgress(c))
+ }()
+ for update := range c {
+ switch {
+ case update.Error != nil && errors.Is(update.Error, io.EOF):
+ fmt.Fprintf(os.Stderr, "receive error message: %v\n", err)
+ fmt.Printf("%d/%d", update.Complete, update.Total)
+ // Output: 4096/4096
+ return
+ case update.Error != nil:
+ fmt.Printf("error writing tarball: %v\n", update.Error)
+ return
+ default:
+ fmt.Fprintf(os.Stderr, "receive update: %#v\n", update)
+ }
+ }
+}
diff --git a/pkg/v1/tarball/testdata/bar b/pkg/v1/tarball/testdata/bar
new file mode 100644
index 0000000..5716ca5
--- /dev/null
+++ b/pkg/v1/tarball/testdata/bar
@@ -0,0 +1 @@
+bar
diff --git a/pkg/v1/tarball/testdata/bat/bat b/pkg/v1/tarball/testdata/bat/bat
new file mode 100644
index 0000000..1054901
--- /dev/null
+++ b/pkg/v1/tarball/testdata/bat/bat
@@ -0,0 +1 @@
+bat
diff --git a/pkg/v1/tarball/testdata/baz b/pkg/v1/tarball/testdata/baz
new file mode 100644
index 0000000..7601807
--- /dev/null
+++ b/pkg/v1/tarball/testdata/baz
@@ -0,0 +1 @@
+baz
diff --git a/pkg/v1/tarball/testdata/content.tar b/pkg/v1/tarball/testdata/content.tar
new file mode 100755
index 0000000..55f4d1d
--- /dev/null
+++ b/pkg/v1/tarball/testdata/content.tar
Binary files differ
diff --git a/pkg/v1/tarball/testdata/foo b/pkg/v1/tarball/testdata/foo
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/pkg/v1/tarball/testdata/foo
@@ -0,0 +1 @@
+foo
diff --git a/pkg/v1/tarball/testdata/no_manifest.tar b/pkg/v1/tarball/testdata/no_manifest.tar
new file mode 100644
index 0000000..319db1d
--- /dev/null
+++ b/pkg/v1/tarball/testdata/no_manifest.tar
Binary files differ
diff --git a/pkg/v1/tarball/testdata/null_manifest.tar b/pkg/v1/tarball/testdata/null_manifest.tar
new file mode 100644
index 0000000..2a65fcd
--- /dev/null
+++ b/pkg/v1/tarball/testdata/null_manifest.tar
Binary files differ
diff --git a/pkg/v1/tarball/testdata/test_bundle.tar b/pkg/v1/tarball/testdata/test_bundle.tar
new file mode 100755
index 0000000..1ad0f79
--- /dev/null
+++ b/pkg/v1/tarball/testdata/test_bundle.tar
Binary files differ
diff --git a/pkg/v1/tarball/testdata/test_image_1.tar b/pkg/v1/tarball/testdata/test_image_1.tar
new file mode 100755
index 0000000..0fe1a21
--- /dev/null
+++ b/pkg/v1/tarball/testdata/test_image_1.tar
Binary files differ
diff --git a/pkg/v1/tarball/testdata/test_image_2.tar b/pkg/v1/tarball/testdata/test_image_2.tar
new file mode 100755
index 0000000..bdfe0ef
--- /dev/null
+++ b/pkg/v1/tarball/testdata/test_image_2.tar
Binary files differ
diff --git a/pkg/v1/tarball/testdata/test_link.tar b/pkg/v1/tarball/testdata/test_link.tar
new file mode 100644
index 0000000..e83064f
--- /dev/null
+++ b/pkg/v1/tarball/testdata/test_link.tar
Binary files differ
diff --git a/pkg/v1/tarball/testdata/test_load_manifest.tar b/pkg/v1/tarball/testdata/test_load_manifest.tar
new file mode 100644
index 0000000..0fe1a21
--- /dev/null
+++ b/pkg/v1/tarball/testdata/test_load_manifest.tar
Binary files differ
diff --git a/pkg/v1/tarball/write.go b/pkg/v1/tarball/write.go
new file mode 100644
index 0000000..e607df1
--- /dev/null
+++ b/pkg/v1/tarball/write.go
@@ -0,0 +1,457 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tarball
+
+import (
+ "archive/tar"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "sort"
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+)
+
+// WriteToFile writes in the compressed format to a tarball, on disk.
+// This is just syntactic sugar wrapping tarball.Write with a new file.
+func WriteToFile(p string, ref name.Reference, img v1.Image, opts ...WriteOption) error {
+ w, err := os.Create(p)
+ if err != nil {
+ return err
+ }
+ defer w.Close()
+
+ return Write(ref, img, w, opts...)
+}
+
+// MultiWriteToFile writes in the compressed format to a tarball, on disk.
+// This is just syntactic sugar wrapping tarball.MultiWrite with a new file.
+func MultiWriteToFile(p string, tagToImage map[name.Tag]v1.Image, opts ...WriteOption) error {
+ refToImage := make(map[name.Reference]v1.Image, len(tagToImage))
+ for i, d := range tagToImage {
+ refToImage[i] = d
+ }
+ return MultiRefWriteToFile(p, refToImage, opts...)
+}
+
+// MultiRefWriteToFile writes in the compressed format to a tarball, on disk.
+// This is just syntactic sugar wrapping tarball.MultiRefWrite with a new file.
+func MultiRefWriteToFile(p string, refToImage map[name.Reference]v1.Image, opts ...WriteOption) error {
+ w, err := os.Create(p)
+ if err != nil {
+ return err
+ }
+ defer w.Close()
+
+ return MultiRefWrite(refToImage, w, opts...)
+}
+
+// Write is a wrapper to write a single image and tag to a tarball.
+func Write(ref name.Reference, img v1.Image, w io.Writer, opts ...WriteOption) error {
+ return MultiRefWrite(map[name.Reference]v1.Image{ref: img}, w, opts...)
+}
+
+// MultiWrite writes the contents of each image to the provided writer, in the compressed format.
+// The contents are written in the following format:
+// One manifest.json file at the top level containing information about several images.
+// One file for each layer, named after the layer's SHA.
+// One file for the config blob, named after its SHA.
+func MultiWrite(tagToImage map[name.Tag]v1.Image, w io.Writer, opts ...WriteOption) error {
+ refToImage := make(map[name.Reference]v1.Image, len(tagToImage))
+ for i, d := range tagToImage {
+ refToImage[i] = d
+ }
+ return MultiRefWrite(refToImage, w, opts...)
+}
+
+// MultiRefWrite writes the contents of each image to the provided writer, in the compressed format.
+// The contents are written in the following format:
+// One manifest.json file at the top level containing information about several images.
+// One file for each layer, named after the layer's SHA.
+// One file for the config blob, named after its SHA.
+func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer, opts ...WriteOption) error {
+ // process options
+ o := &writeOptions{
+ updates: nil,
+ }
+ for _, option := range opts {
+ if err := option(o); err != nil {
+ return err
+ }
+ }
+
+ imageToTags := dedupRefToImage(refToImage)
+ size, mBytes, err := getSizeAndManifest(imageToTags)
+ if err != nil {
+ return sendUpdateReturn(o, err)
+ }
+
+ return writeImagesToTar(imageToTags, mBytes, size, w, o)
+}
+
+// sendUpdateReturn return the passed in error message, also sending on update channel, if it exists
+func sendUpdateReturn(o *writeOptions, err error) error {
+ if o != nil && o.updates != nil {
+ o.updates <- v1.Update{
+ Error: err,
+ }
+ }
+ return err
+}
+
+// sendProgressWriterReturn return the passed in error message, also sending on update channel, if it exists, along with downloaded information
+func sendProgressWriterReturn(pw *progressWriter, err error) error {
+ if pw != nil {
+ return pw.Error(err)
+ }
+ return err
+}
+
+// writeImagesToTar writes the images to the tarball
+func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w io.Writer, o *writeOptions) (err error) {
+ if w == nil {
+ return sendUpdateReturn(o, errors.New("must pass valid writer"))
+ }
+
+ tw := w
+ var pw *progressWriter
+
+ // we only calculate the sizes and use a progressWriter if we were provided
+ // an option with a progress channel
+ if o != nil && o.updates != nil {
+ pw = &progressWriter{
+ w: w,
+ updates: o.updates,
+ size: size,
+ }
+ tw = pw
+ }
+
+ tf := tar.NewWriter(tw)
+ defer tf.Close()
+
+ seenLayerDigests := make(map[string]struct{})
+
+ for img := range imageToTags {
+ // Write the config.
+ cfgName, err := img.ConfigName()
+ if err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+ cfgBlob, err := img.RawConfigFile()
+ if err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+ if err := writeTarEntry(tf, cfgName.String(), bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+
+ // Write the layers.
+ layers, err := img.Layers()
+ if err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+ layerFiles := make([]string, len(layers))
+ for i, l := range layers {
+ d, err := l.Digest()
+ if err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+ // Munge the file name to appease ancient technology.
+ //
+ // tar assumes anything with a colon is a remote tape drive:
+ // https://www.gnu.org/software/tar/manual/html_section/tar_45.html
+ // Drop the algorithm prefix, e.g. "sha256:"
+ hex := d.Hex
+
+ // gunzip expects certain file extensions:
+ // https://www.gnu.org/software/gzip/manual/html_node/Overview.html
+ layerFiles[i] = fmt.Sprintf("%s.tar.gz", hex)
+
+ if _, ok := seenLayerDigests[hex]; ok {
+ continue
+ }
+ seenLayerDigests[hex] = struct{}{}
+
+ r, err := l.Compressed()
+ if err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+ blobSize, err := l.Size()
+ if err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+
+ if err := writeTarEntry(tf, layerFiles[i], r, blobSize); err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+ }
+ }
+ if err := writeTarEntry(tf, "manifest.json", bytes.NewReader(m), int64(len(m))); err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+
+ // be sure to close the tar writer so everything is flushed out before we send our EOF
+ if err := tf.Close(); err != nil {
+ return sendProgressWriterReturn(pw, err)
+ }
+ // send an EOF to indicate finished on the channel, but nil as our return error
+ _ = sendProgressWriterReturn(pw, io.EOF)
+ return nil
+}
+
+// calculateManifest calculates the manifest and optionally the size of the tar file
+func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error) {
+ if len(imageToTags) == 0 {
+ return nil, errors.New("set of images is empty")
+ }
+
+ for img, tags := range imageToTags {
+ cfgName, err := img.ConfigName()
+ if err != nil {
+ return nil, err
+ }
+
+ // Store foreign layer info.
+ layerSources := make(map[v1.Hash]v1.Descriptor)
+
+ // Write the layers.
+ layers, err := img.Layers()
+ if err != nil {
+ return nil, err
+ }
+ layerFiles := make([]string, len(layers))
+ for i, l := range layers {
+ d, err := l.Digest()
+ if err != nil {
+ return nil, err
+ }
+ // Munge the file name to appease ancient technology.
+ //
+ // tar assumes anything with a colon is a remote tape drive:
+ // https://www.gnu.org/software/tar/manual/html_section/tar_45.html
+ // Drop the algorithm prefix, e.g. "sha256:"
+ hex := d.Hex
+
+ // gunzip expects certain file extensions:
+ // https://www.gnu.org/software/gzip/manual/html_node/Overview.html
+ layerFiles[i] = fmt.Sprintf("%s.tar.gz", hex)
+
+ // Add to LayerSources if it's a foreign layer.
+ desc, err := partial.BlobDescriptor(img, d)
+ if err != nil {
+ return nil, err
+ }
+ if !desc.MediaType.IsDistributable() {
+ diffid, err := partial.BlobToDiffID(img, d)
+ if err != nil {
+ return nil, err
+ }
+ layerSources[diffid] = *desc
+ }
+ }
+
+ // Generate the tar descriptor and write it.
+ m = append(m, Descriptor{
+ Config: cfgName.String(),
+ RepoTags: tags,
+ Layers: layerFiles,
+ LayerSources: layerSources,
+ })
+ }
+ // sort by name of the repotags so it is consistent. Alternatively, we could sort by hash of the
+ // descriptor, but that would make it hard for humans to process
+ sort.Slice(m, func(i, j int) bool {
+ return strings.Join(m[i].RepoTags, ",") < strings.Join(m[j].RepoTags, ",")
+ })
+
+ return m, nil
+}
+
+// CalculateSize calculates the expected complete size of the output tar file
+func CalculateSize(refToImage map[name.Reference]v1.Image) (size int64, err error) {
+ imageToTags := dedupRefToImage(refToImage)
+ size, _, err = getSizeAndManifest(imageToTags)
+ return size, err
+}
+
+func getSizeAndManifest(imageToTags map[v1.Image][]string) (int64, []byte, error) {
+ m, err := calculateManifest(imageToTags)
+ if err != nil {
+ return 0, nil, fmt.Errorf("unable to calculate manifest: %w", err)
+ }
+ mBytes, err := json.Marshal(m)
+ if err != nil {
+ return 0, nil, fmt.Errorf("could not marshall manifest to bytes: %w", err)
+ }
+
+ size, err := calculateTarballSize(imageToTags, mBytes)
+ if err != nil {
+ return 0, nil, fmt.Errorf("error calculating tarball size: %w", err)
+ }
+ return size, mBytes, nil
+}
+
+// calculateTarballSize calculates the size of the tar file
+func calculateTarballSize(imageToTags map[v1.Image][]string, mBytes []byte) (size int64, err error) {
+ seenLayerDigests := make(map[string]struct{})
+ for img, name := range imageToTags {
+ manifest, err := img.Manifest()
+ if err != nil {
+ return size, fmt.Errorf("unable to get manifest for img %s: %w", name, err)
+ }
+ size += calculateSingleFileInTarSize(manifest.Config.Size)
+ for _, l := range manifest.Layers {
+ hex := l.Digest.Hex
+ if _, ok := seenLayerDigests[hex]; ok {
+ continue
+ }
+ seenLayerDigests[hex] = struct{}{}
+ size += calculateSingleFileInTarSize(l.Size)
+ }
+ }
+ // add the manifest
+ size += calculateSingleFileInTarSize(int64(len(mBytes)))
+
+ // add the two padding blocks that indicate end of a tar file
+ size += 1024
+ return size, nil
+}
+
+func dedupRefToImage(refToImage map[name.Reference]v1.Image) map[v1.Image][]string {
+ imageToTags := make(map[v1.Image][]string)
+
+ for ref, img := range refToImage {
+ if tag, ok := ref.(name.Tag); ok {
+ if tags, ok := imageToTags[img]; !ok || tags == nil {
+ imageToTags[img] = []string{}
+ }
+ // Docker cannot load tarballs without an explicit tag:
+ // https://github.com/google/go-containerregistry/issues/890
+ //
+ // We can't use the fully qualified tag.Name() because of rules_docker:
+ // https://github.com/google/go-containerregistry/issues/527
+ //
+ // If the tag is "latest", but tag.String() doesn't end in ":latest",
+ // just append it. Kind of gross, but should work for now.
+ ts := tag.String()
+ if tag.Identifier() == name.DefaultTag && !strings.HasSuffix(ts, ":"+name.DefaultTag) {
+ ts = fmt.Sprintf("%s:%s", ts, name.DefaultTag)
+ }
+ imageToTags[img] = append(imageToTags[img], ts)
+ } else if _, ok := imageToTags[img]; !ok {
+ imageToTags[img] = nil
+ }
+ }
+
+ return imageToTags
+}
+
+// writeTarEntry writes a file to the provided writer with a corresponding tar header
+func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error {
+ hdr := &tar.Header{
+ Mode: 0644,
+ Typeflag: tar.TypeReg,
+ Size: size,
+ Name: path,
+ }
+ if err := tf.WriteHeader(hdr); err != nil {
+ return err
+ }
+ _, err := io.Copy(tf, r)
+ return err
+}
+
+// ComputeManifest get the manifest.json that will be written to the tarball
+// for multiple references
+func ComputeManifest(refToImage map[name.Reference]v1.Image) (Manifest, error) {
+ imageToTags := dedupRefToImage(refToImage)
+ return calculateManifest(imageToTags)
+}
+
+// WriteOption a function option to pass to Write()
+type WriteOption func(*writeOptions) error
+type writeOptions struct {
+ updates chan<- v1.Update
+}
+
+// WithProgress create a WriteOption for passing to Write() that enables
+// a channel to receive updates as they are downloaded and written to disk.
+func WithProgress(updates chan<- v1.Update) WriteOption {
+ return func(o *writeOptions) error {
+ o.updates = updates
+ return nil
+ }
+}
+
+// progressWriter is a writer which will send the download progress
+type progressWriter struct {
+ w io.Writer
+ updates chan<- v1.Update
+ size, complete int64
+}
+
+func (pw *progressWriter) Write(p []byte) (int, error) {
+ n, err := pw.w.Write(p)
+ if err != nil {
+ return n, err
+ }
+
+ pw.complete += int64(n)
+
+ pw.updates <- v1.Update{
+ Total: pw.size,
+ Complete: pw.complete,
+ }
+
+ return n, err
+}
+
+func (pw *progressWriter) Error(err error) error {
+ pw.updates <- v1.Update{
+ Total: pw.size,
+ Complete: pw.complete,
+ Error: err,
+ }
+ return err
+}
+
+func (pw *progressWriter) Close() error {
+ pw.updates <- v1.Update{
+ Total: pw.size,
+ Complete: pw.complete,
+ Error: io.EOF,
+ }
+ return io.EOF
+}
+
+// calculateSingleFileInTarSize calculate the size a file will take up in a tar archive,
+// given the input data. Provided by rounding up to nearest whole block (512)
+// and adding header 512
+func calculateSingleFileInTarSize(in int64) (out int64) {
+ // doing this manually, because math.Round() works with float64
+ out += in
+ if remainder := out % 512; remainder != 0 {
+ out += (512 - remainder)
+ }
+ out += 512
+ return out
+}
diff --git a/pkg/v1/tarball/write_test.go b/pkg/v1/tarball/write_test.go
new file mode 100644
index 0000000..fdfe499
--- /dev/null
+++ b/pkg/v1/tarball/write_test.go
@@ -0,0 +1,502 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tarball_test
+
+import (
+ "archive/tar"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/internal/compare"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/random"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestWrite(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file.")
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image
+ randImage, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image.")
+ }
+ tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag.")
+ }
+ if err := tarball.WriteToFile(fp.Name(), tag, randImage); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+
+ // Make sure the image is valid and can be loaded.
+ // Load it both by nil and by its name.
+ for _, it := range []*name.Tag{nil, &tag} {
+ tarImage, err := tarball.ImageFromPath(fp.Name(), it)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+
+ if err := validate.Image(tarImage); err != nil {
+ t.Errorf("validate.Image: %v", err)
+ }
+
+ if err := compare.Images(randImage, tarImage); err != nil {
+ t.Errorf("compare.Images: %v", err)
+ }
+ }
+
+ // Try loading a different tag, it should error.
+ fakeTag, err := name.NewTag("gcr.io/notthistag:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error generating tag: %v", err)
+ }
+ if _, err := tarball.ImageFromPath(fp.Name(), &fakeTag); err == nil {
+ t.Errorf("Expected error loading tag %v from image", fakeTag)
+ }
+}
+
+func TestMultiWriteSameImage(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file.")
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image
+ randImage, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image.")
+ }
+
+ // Make two tags that point to the random image above.
+ tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag1.")
+ }
+ tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag2.")
+ }
+ dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test dig3.")
+ }
+ refToImage := make(map[name.Reference]v1.Image)
+ refToImage[tag1] = randImage
+ refToImage[tag2] = randImage
+ refToImage[dig3] = randImage
+
+ // Write the images with both tags to the tarball
+ if err := tarball.MultiRefWriteToFile(fp.Name(), refToImage); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+ for ref := range refToImage {
+ tag, ok := ref.(name.Tag)
+ if !ok {
+ continue
+ }
+
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+
+ if err := validate.Image(tarImage); err != nil {
+ t.Errorf("validate.Image: %v", err)
+ }
+
+ if err := compare.Images(randImage, tarImage); err != nil {
+ t.Errorf("compare.Images: %v", err)
+ }
+ }
+}
+
+func TestMultiWriteDifferentImages(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file.")
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image
+ randImage1, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image 1.")
+ }
+
+ // Make another random image
+ randImage2, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image 2.")
+ }
+
+ // Make another random image
+ randImage3, err := random.Image(256, 8)
+ if err != nil {
+ t.Fatalf("Error creating random image 3.")
+ }
+
+ // Create two tags, one pointing to each image created.
+ tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag1.")
+ }
+ tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag2.")
+ }
+ dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test dig3.")
+ }
+ refToImage := make(map[name.Reference]v1.Image)
+ refToImage[tag1] = randImage1
+ refToImage[tag2] = randImage2
+ refToImage[dig3] = randImage3
+
+ // Write both images to the tarball.
+ if err := tarball.MultiRefWriteToFile(fp.Name(), refToImage); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+ for ref, img := range refToImage {
+ tag, ok := ref.(name.Tag)
+ if !ok {
+ continue
+ }
+
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+
+ if err := validate.Image(tarImage); err != nil {
+ t.Errorf("validate.Image: %v", err)
+ }
+
+ if err := compare.Images(img, tarImage); err != nil {
+ t.Errorf("compare.Images: %v", err)
+ }
+ }
+}
+
+func TestWriteForeignLayers(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file.")
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image
+ randImage, err := random.Image(256, 1)
+ if err != nil {
+ t.Fatalf("Error creating random image.")
+ }
+ tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag.")
+ }
+ randLayer, err := random.Layer(512, types.DockerForeignLayer)
+ if err != nil {
+ t.Fatalf("random.Layer: %v", err)
+ }
+ img, err := mutate.Append(randImage, mutate.Addendum{
+ Layer: randLayer,
+ URLs: []string{
+ "example.com",
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := tarball.WriteToFile(fp.Name(), tag, img); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+
+ if err := validate.Image(tarImage); err != nil {
+ t.Fatalf("validate.Image(): %v", err)
+ }
+
+ m, err := tarImage.Manifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if got, want := m.Layers[1].MediaType, types.DockerForeignLayer; got != want {
+ t.Errorf("Wrong MediaType: %s != %s", got, want)
+ }
+ if got, want := m.Layers[1].URLs[0], "example.com"; got != want {
+ t.Errorf("Wrong URLs: %s != %s", got, want)
+ }
+}
+
+func TestWriteSharedLayers(t *testing.T) {
+ // Make a tempfile for tarball writes.
+ fp, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Error creating temp file.")
+ }
+ t.Log(fp.Name())
+ defer fp.Close()
+ defer os.Remove(fp.Name())
+
+ // Make a random image
+ randImage, err := random.Image(256, 1)
+ if err != nil {
+ t.Fatalf("Error creating random image.")
+ }
+ tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag1.")
+ }
+ tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag2.")
+ }
+ randLayer, err := random.Layer(512, types.DockerLayer)
+ if err != nil {
+ t.Fatalf("random.Layer: %v", err)
+ }
+ mutatedImage, err := mutate.Append(randImage, mutate.Addendum{
+ Layer: randLayer,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ refToImage := make(map[name.Reference]v1.Image)
+ refToImage[tag1] = randImage
+ refToImage[tag2] = mutatedImage
+
+ // Write the images with both tags to the tarball
+ if err := tarball.MultiRefWriteToFile(fp.Name(), refToImage); err != nil {
+ t.Fatalf("Unexpected error writing tarball: %v", err)
+ }
+ for ref := range refToImage {
+ tag, ok := ref.(name.Tag)
+ if !ok {
+ continue
+ }
+
+ tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
+ if err != nil {
+ t.Fatalf("Unexpected error reading tarball: %v", err)
+ }
+
+ if err := validate.Image(tarImage); err != nil {
+ t.Errorf("validate.Image: %v", err)
+ }
+
+ if err := compare.Images(refToImage[tag], tarImage); err != nil {
+ t.Errorf("compare.Images: %v", err)
+ }
+ }
+ _, err = fp.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatalf("Seek to start of file: %v", err)
+ }
+ layers, err := randImage.Layers()
+ if err != nil {
+ t.Fatalf("Get image layers: %v", err)
+ }
+ layers = append(layers, randLayer)
+ wantDigests := make(map[string]struct{})
+ for _, layer := range layers {
+ d, err := layer.Digest()
+ if err != nil {
+ t.Fatalf("Get layer digest: %v", err)
+ }
+ wantDigests[d.Hex] = struct{}{}
+ }
+
+ const layerFileSuffix = ".tar.gz"
+ r := tar.NewReader(fp)
+ for {
+ hdr, err := r.Next()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ t.Fatalf("Get tar header: %v", err)
+ }
+ if strings.HasSuffix(hdr.Name, layerFileSuffix) {
+ hex := hdr.Name[:len(hdr.Name)-len(layerFileSuffix)]
+ if _, ok := wantDigests[hex]; ok {
+ delete(wantDigests, hex)
+ } else {
+ t.Errorf("Found unwanted layer with digest %q", hex)
+ }
+ }
+ }
+ if len(wantDigests) != 0 {
+ for hex := range wantDigests {
+ t.Errorf("Expected to find layer with digest %q but it didn't exist", hex)
+ }
+ }
+}
+
+func TestComputeManifest(t *testing.T) {
+ var randomTag, mutatedTag = "ubuntu", "gcr.io/baz/bat:latest"
+
+ // https://github.com/google/go-containerregistry/issues/890
+ randomTagWritten := "ubuntu:latest"
+
+ // Make a random image
+ randImage, err := random.Image(256, 1)
+ if err != nil {
+ t.Fatalf("Error creating random image.")
+ }
+ randConfig, err := randImage.ConfigName()
+ if err != nil {
+ t.Fatalf("error getting random image config: %v", err)
+ }
+ tag1, err := name.NewTag(randomTag)
+ if err != nil {
+ t.Fatalf("Error creating test tag1.")
+ }
+ tag2, err := name.NewTag(mutatedTag, name.StrictValidation)
+ if err != nil {
+ t.Fatalf("Error creating test tag2.")
+ }
+ randLayer, err := random.Layer(512, types.DockerLayer)
+ if err != nil {
+ t.Fatalf("random.Layer: %v", err)
+ }
+ mutatedImage, err := mutate.Append(randImage, mutate.Addendum{
+ Layer: randLayer,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ mutatedConfig, err := mutatedImage.ConfigName()
+ if err != nil {
+ t.Fatalf("error getting mutated image config: %v", err)
+ }
+ randomLayersHashes, err := getLayersHashes(randImage)
+ if err != nil {
+ t.Fatalf("error getting random image hashes: %v", err)
+ }
+ randomLayersFilenames := getLayersFilenames(randomLayersHashes)
+
+ mutatedLayersHashes, err := getLayersHashes(mutatedImage)
+ if err != nil {
+ t.Fatalf("error getting mutated image hashes: %v", err)
+ }
+ mutatedLayersFilenames := getLayersFilenames(mutatedLayersHashes)
+
+ refToImage := make(map[name.Reference]v1.Image)
+ refToImage[tag1] = randImage
+ refToImage[tag2] = mutatedImage
+
+ // calculate the manifest
+ m, err := tarball.ComputeManifest(refToImage)
+ if err != nil {
+ t.Fatalf("Unexpected error calculating manifest: %v", err)
+ }
+ // the order of these two is based on the repo tags
+ // so mutated "gcr.io/baz/bat:latest" is before random "gcr.io/foo/bar:latest"
+ expected := []tarball.Descriptor{
+ {
+ Config: mutatedConfig.String(),
+ RepoTags: []string{mutatedTag},
+ Layers: mutatedLayersFilenames,
+ },
+ {
+ Config: randConfig.String(),
+ RepoTags: []string{randomTagWritten},
+ Layers: randomLayersFilenames,
+ },
+ }
+ if len(m) != len(expected) {
+ t.Fatalf("mismatched manifest lengths: actual %d, expected %d", len(m), len(expected))
+ }
+ mBytes, err := json.Marshal(m)
+ if err != nil {
+ t.Fatalf("unable to marshall actual manifest to json: %v", err)
+ }
+ eBytes, err := json.Marshal(expected)
+ if err != nil {
+ t.Fatalf("unable to marshall expected manifest to json: %v", err)
+ }
+ if !bytes.Equal(mBytes, eBytes) {
+ t.Errorf("mismatched manifests.\nActual: %s\nExpected: %s", string(mBytes), string(eBytes))
+ }
+}
+
+func TestComputeManifest_FailsOnNoRefs(t *testing.T) {
+ _, err := tarball.ComputeManifest(nil)
+ if err == nil || !strings.Contains(err.Error(), "set of images is empty") {
+ t.Error("expected calculateManifest to fail with nil input")
+ }
+
+ _, err = tarball.ComputeManifest(map[name.Reference]v1.Image{})
+ if err == nil || !strings.Contains(err.Error(), "set of images is empty") {
+ t.Error("expected calculateManifest to fail with empty input")
+ }
+}
+
+func getLayersHashes(img v1.Image) ([]string, error) {
+ hashes := []string{}
+ layers, err := img.Layers()
+ if err != nil {
+ return nil, fmt.Errorf("error getting image layers: %w", err)
+ }
+ for i, l := range layers {
+ hash, err := l.Digest()
+ if err != nil {
+ return nil, fmt.Errorf("error getting digest for layer %d: %w", i, err)
+ }
+ hashes = append(hashes, hash.Hex)
+ }
+ return hashes, nil
+}
+
+func getLayersFilenames(hashes []string) []string {
+ filenames := []string{}
+ for _, h := range hashes {
+ filenames = append(filenames, fmt.Sprintf("%s.tar.gz", h))
+ }
+ return filenames
+}
diff --git a/pkg/v1/types/types.go b/pkg/v1/types/types.go
new file mode 100644
index 0000000..efc6bd6
--- /dev/null
+++ b/pkg/v1/types/types.go
@@ -0,0 +1,82 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package types holds common OCI media types.
+package types
+
+// MediaType is an enumeration of the supported mime types that an element of an image might have.
+type MediaType string
+
+// The collection of known MediaType values.
+const (
+ OCIContentDescriptor MediaType = "application/vnd.oci.descriptor.v1+json"
+ OCIImageIndex MediaType = "application/vnd.oci.image.index.v1+json"
+ OCIManifestSchema1 MediaType = "application/vnd.oci.image.manifest.v1+json"
+ OCIConfigJSON MediaType = "application/vnd.oci.image.config.v1+json"
+ OCILayer MediaType = "application/vnd.oci.image.layer.v1.tar+gzip"
+ OCILayerZStd MediaType = "application/vnd.oci.image.layer.v1.tar+zstd"
+ OCIRestrictedLayer MediaType = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"
+ OCIUncompressedLayer MediaType = "application/vnd.oci.image.layer.v1.tar"
+ OCIUncompressedRestrictedLayer MediaType = "application/vnd.oci.image.layer.nondistributable.v1.tar"
+
+ DockerManifestSchema1 MediaType = "application/vnd.docker.distribution.manifest.v1+json"
+ DockerManifestSchema1Signed MediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws"
+ DockerManifestSchema2 MediaType = "application/vnd.docker.distribution.manifest.v2+json"
+ DockerManifestList MediaType = "application/vnd.docker.distribution.manifest.list.v2+json"
+ DockerLayer MediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip"
+ DockerConfigJSON MediaType = "application/vnd.docker.container.image.v1+json"
+ DockerPluginConfig MediaType = "application/vnd.docker.plugin.v1+json"
+ DockerForeignLayer MediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
+ DockerUncompressedLayer MediaType = "application/vnd.docker.image.rootfs.diff.tar"
+
+ OCIVendorPrefix = "vnd.oci"
+ DockerVendorPrefix = "vnd.docker"
+)
+
+// IsDistributable returns true if a layer is distributable, see:
+// https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers
+func (m MediaType) IsDistributable() bool {
+ switch m {
+ case DockerForeignLayer, OCIRestrictedLayer, OCIUncompressedRestrictedLayer:
+ return false
+ }
+ return true
+}
+
+// IsImage returns true if the mediaType represents an image manifest, as opposed to something else, like an index.
+func (m MediaType) IsImage() bool {
+ switch m {
+ case OCIManifestSchema1, DockerManifestSchema2:
+ return true
+ }
+ return false
+}
+
+// IsIndex returns true if the mediaType represents an index, as opposed to something else, like an image.
+func (m MediaType) IsIndex() bool {
+ switch m {
+ case OCIImageIndex, DockerManifestList:
+ return true
+ }
+ return false
+}
+
+// IsConfig returns true if the mediaType represents a config, as opposed to something else, like an image.
+func (m MediaType) IsConfig() bool {
+ switch m {
+ case OCIConfigJSON, DockerConfigJSON:
+ return true
+ }
+ return false
+}
diff --git a/pkg/v1/types/types_test.go b/pkg/v1/types/types_test.go
new file mode 100644
index 0000000..7a8d356
--- /dev/null
+++ b/pkg/v1/types/types_test.go
@@ -0,0 +1,112 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package types
+
+import "testing"
+
+func TestIsDistributable(t *testing.T) {
+ for _, mt := range []MediaType{
+ OCIRestrictedLayer,
+ OCIUncompressedRestrictedLayer,
+ DockerForeignLayer,
+ } {
+ if mt.IsDistributable() {
+ t.Errorf("%s: should not be distributable", mt)
+ }
+ }
+
+ for _, mt := range []MediaType{
+ OCIContentDescriptor,
+ OCIImageIndex,
+ OCIManifestSchema1,
+ OCIConfigJSON,
+ OCILayer,
+ OCIUncompressedLayer,
+ DockerManifestSchema1,
+ DockerManifestSchema1Signed,
+ DockerManifestSchema2,
+ DockerManifestList,
+ DockerLayer,
+ DockerConfigJSON,
+ DockerPluginConfig,
+ DockerUncompressedLayer,
+ } {
+ if !mt.IsDistributable() {
+ t.Errorf("%s: should be distributable", mt)
+ }
+ }
+}
+
+func TestIsImage(t *testing.T) {
+ for _, mt := range []MediaType{
+ OCIManifestSchema1, DockerManifestSchema2,
+ } {
+ if !mt.IsImage() {
+ t.Errorf("%s: should be image", mt)
+ }
+ }
+
+ for _, mt := range []MediaType{
+ OCIContentDescriptor,
+ OCIImageIndex,
+ OCIConfigJSON,
+ OCILayer,
+ OCIRestrictedLayer,
+ OCIUncompressedLayer,
+ OCIUncompressedRestrictedLayer,
+
+ DockerManifestList,
+ DockerLayer,
+ DockerConfigJSON,
+ DockerPluginConfig,
+ DockerForeignLayer,
+ DockerUncompressedLayer,
+ } {
+ if mt.IsImage() {
+ t.Errorf("%s: should not be image", mt)
+ }
+ }
+}
+
+func TestIsIndex(t *testing.T) {
+ for _, mt := range []MediaType{
+ OCIImageIndex, DockerManifestList,
+ } {
+ if !mt.IsIndex() {
+ t.Errorf("%s: should be index", mt)
+ }
+ }
+
+ for _, mt := range []MediaType{
+ OCIContentDescriptor,
+ OCIConfigJSON,
+ OCILayer,
+ OCIRestrictedLayer,
+ OCIUncompressedLayer,
+ OCIUncompressedRestrictedLayer,
+ OCIManifestSchema1,
+
+ DockerManifestSchema2,
+ DockerLayer,
+ DockerConfigJSON,
+ DockerPluginConfig,
+ DockerForeignLayer,
+ DockerUncompressedLayer,
+ } {
+ if mt.IsIndex() {
+ t.Errorf("%s: should not be index", mt)
+ }
+ }
+}
diff --git a/pkg/v1/validate/doc.go b/pkg/v1/validate/doc.go
new file mode 100644
index 0000000..91ca87a
--- /dev/null
+++ b/pkg/v1/validate/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package validate provides methods for validating image correctness.
+package validate
diff --git a/pkg/v1/validate/image.go b/pkg/v1/validate/image.go
new file mode 100644
index 0000000..94fb767
--- /dev/null
+++ b/pkg/v1/validate/image.go
@@ -0,0 +1,288 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package validate
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/google/go-cmp/cmp"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+)
+
+// Image validates that img does not violate any invariants of the image format.
+func Image(img v1.Image, opt ...Option) error {
+ errs := []string{}
+ if err := validateLayers(img, opt...); err != nil {
+ errs = append(errs, fmt.Sprintf("validating layers: %v", err))
+ }
+
+ if err := validateConfig(img); err != nil {
+ errs = append(errs, fmt.Sprintf("validating config: %v", err))
+ }
+
+ if err := validateManifest(img); err != nil {
+ errs = append(errs, fmt.Sprintf("validating manifest: %v", err))
+ }
+
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n\n"))
+ }
+ return nil
+}
+
+func validateConfig(img v1.Image) error {
+ cn, err := img.ConfigName()
+ if err != nil {
+ return err
+ }
+
+ rc, err := img.RawConfigFile()
+ if err != nil {
+ return err
+ }
+
+ hash, size, err := v1.SHA256(bytes.NewReader(rc))
+ if err != nil {
+ return err
+ }
+
+ m, err := img.Manifest()
+ if err != nil {
+ return err
+ }
+
+ cf, err := img.ConfigFile()
+ if err != nil {
+ return err
+ }
+
+ pcf, err := v1.ParseConfigFile(bytes.NewReader(rc))
+ if err != nil {
+ return err
+ }
+
+ errs := []string{}
+ if cn != hash {
+ errs = append(errs, fmt.Sprintf("mismatched config digest: ConfigName()=%s, SHA256(RawConfigFile())=%s", cn, hash))
+ }
+
+ if want, got := m.Config.Size, size; want != got {
+ errs = append(errs, fmt.Sprintf("mismatched config size: Manifest.Config.Size()=%d, len(RawConfigFile())=%d", want, got))
+ }
+
+ if diff := cmp.Diff(pcf, cf); diff != "" {
+ errs = append(errs, fmt.Sprintf("mismatched config content: (-ParseConfigFile(RawConfigFile()) +ConfigFile()) %s", diff))
+ }
+
+ if cf.RootFS.Type != "layers" {
+ errs = append(errs, fmt.Sprintf("invalid ConfigFile.RootFS.Type: %q != %q", cf.RootFS.Type, "layers"))
+ }
+
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
+
+func validateLayers(img v1.Image, opt ...Option) error {
+ o := makeOptions(opt...)
+
+ layers, err := img.Layers()
+ if err != nil {
+ return err
+ }
+
+ if o.fast {
+ return layersExist(layers)
+ }
+
+ digests := []v1.Hash{}
+ diffids := []v1.Hash{}
+ udiffids := []v1.Hash{}
+ sizes := []int64{}
+ for i, layer := range layers {
+ cl, err := computeLayer(layer)
+ if errors.Is(err, io.ErrUnexpectedEOF) {
+ // Errored while reading tar content of layer because a header or
+ // content section was not the correct length. This is most likely
+ // due to an incomplete download or otherwise interrupted process.
+ m, err := img.Manifest()
+ if err != nil {
+ return fmt.Errorf("undersized layer[%d] content", i)
+ }
+ return fmt.Errorf("undersized layer[%d] content: Manifest.Layers[%d].Size=%d", i, i, m.Layers[i].Size)
+ }
+ if err != nil {
+ return err
+ }
+ // Compute all of these first before we call Config() and Manifest() to allow
+ // for lazy access e.g. for stream.Layer.
+ digests = append(digests, cl.digest)
+ diffids = append(diffids, cl.diffid)
+ udiffids = append(udiffids, cl.uncompressedDiffid)
+ sizes = append(sizes, cl.size)
+ }
+
+ cf, err := img.ConfigFile()
+ if err != nil {
+ return err
+ }
+
+ m, err := img.Manifest()
+ if err != nil {
+ return err
+ }
+
+ errs := []string{}
+ for i, layer := range layers {
+ digest, err := layer.Digest()
+ if err != nil {
+ return err
+ }
+ diffid, err := layer.DiffID()
+ if err != nil {
+ return err
+ }
+ size, err := layer.Size()
+ if err != nil {
+ return err
+ }
+ mediaType, err := layer.MediaType()
+ if err != nil {
+ return err
+ }
+
+ if _, err := img.LayerByDigest(digest); err != nil {
+ return err
+ }
+
+ if _, err := img.LayerByDiffID(diffid); err != nil {
+ return err
+ }
+
+ if digest != digests[i] {
+ errs = append(errs, fmt.Sprintf("mismatched layer[%d] digest: Digest()=%s, SHA256(Compressed())=%s", i, digest, digests[i]))
+ }
+
+ if m.Layers[i].Digest != digests[i] {
+ errs = append(errs, fmt.Sprintf("mismatched layer[%d] digest: Manifest.Layers[%d].Digest=%s, SHA256(Compressed())=%s", i, i, m.Layers[i].Digest, digests[i]))
+ }
+
+ if diffid != diffids[i] {
+ errs = append(errs, fmt.Sprintf("mismatched layer[%d] diffid: DiffID()=%s, SHA256(Gunzip(Compressed()))=%s", i, diffid, diffids[i]))
+ }
+
+ if diffid != udiffids[i] {
+ errs = append(errs, fmt.Sprintf("mismatched layer[%d] diffid: DiffID()=%s, SHA256(Uncompressed())=%s", i, diffid, udiffids[i]))
+ }
+
+ if cf.RootFS.DiffIDs[i] != diffids[i] {
+ errs = append(errs, fmt.Sprintf("mismatched layer[%d] diffid: ConfigFile.RootFS.DiffIDs[%d]=%s, SHA256(Gunzip(Compressed()))=%s", i, i, cf.RootFS.DiffIDs[i], diffids[i]))
+ }
+
+ if size != sizes[i] {
+ errs = append(errs, fmt.Sprintf("mismatched layer[%d] size: Size()=%d, len(Compressed())=%d", i, size, sizes[i]))
+ }
+
+ if m.Layers[i].Size != sizes[i] {
+ errs = append(errs, fmt.Sprintf("mismatched layer[%d] size: Manifest.Layers[%d].Size=%d, len(Compressed())=%d", i, i, m.Layers[i].Size, sizes[i]))
+ }
+
+ if m.Layers[i].MediaType != mediaType {
+ errs = append(errs, fmt.Sprintf("mismatched layer[%d] mediaType: Manifest.Layers[%d].MediaType=%s, layer.MediaType()=%s", i, i, m.Layers[i].MediaType, mediaType))
+ }
+ }
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
+
+func validateManifest(img v1.Image) error {
+ digest, err := img.Digest()
+ if err != nil {
+ return err
+ }
+
+ size, err := img.Size()
+ if err != nil {
+ return err
+ }
+
+ rm, err := img.RawManifest()
+ if err != nil {
+ return err
+ }
+
+ hash, _, err := v1.SHA256(bytes.NewReader(rm))
+ if err != nil {
+ return err
+ }
+
+ m, err := img.Manifest()
+ if err != nil {
+ return err
+ }
+
+ pm, err := v1.ParseManifest(bytes.NewReader(rm))
+ if err != nil {
+ return err
+ }
+
+ errs := []string{}
+ if digest != hash {
+ errs = append(errs, fmt.Sprintf("mismatched manifest digest: Digest()=%s, SHA256(RawManifest())=%s", digest, hash))
+ }
+
+ if diff := cmp.Diff(pm, m); diff != "" {
+ errs = append(errs, fmt.Sprintf("mismatched manifest content: (-ParseManifest(RawManifest()) +Manifest()) %s", diff))
+ }
+
+ if size != int64(len(rm)) {
+ errs = append(errs, fmt.Sprintf("mismatched manifest size: Size()=%d, len(RawManifest())=%d", size, len(rm)))
+ }
+
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
+
+func layersExist(layers []v1.Layer) error {
+ errs := []string{}
+ for _, layer := range layers {
+ ok, err := partial.Exists(layer)
+ if err != nil {
+ errs = append(errs, err.Error())
+ }
+ if !ok {
+ errs = append(errs, "layer does not exist")
+ }
+ }
+
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
diff --git a/pkg/v1/validate/index.go b/pkg/v1/validate/index.go
new file mode 100644
index 0000000..7514dc4
--- /dev/null
+++ b/pkg/v1/validate/index.go
@@ -0,0 +1,175 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package validate
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-containerregistry/pkg/logs"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+// Index validates that idx does not violate any invariants of the index format.
+func Index(idx v1.ImageIndex, opt ...Option) error {
+ errs := []string{}
+
+ if err := validateChildren(idx, opt...); err != nil {
+ errs = append(errs, fmt.Sprintf("validating children: %v", err))
+ }
+
+ if err := validateIndexManifest(idx); err != nil {
+ errs = append(errs, fmt.Sprintf("validating index manifest: %v", err))
+ }
+
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n\n"))
+ }
+ return nil
+}
+
+type withLayer interface {
+ Layer(v1.Hash) (v1.Layer, error)
+}
+
+func validateChildren(idx v1.ImageIndex, opt ...Option) error {
+ manifest, err := idx.IndexManifest()
+ if err != nil {
+ return err
+ }
+
+ errs := []string{}
+ for i, desc := range manifest.Manifests {
+ switch desc.MediaType {
+ case types.OCIImageIndex, types.DockerManifestList:
+ idx, err := idx.ImageIndex(desc.Digest)
+ if err != nil {
+ return err
+ }
+ if err := Index(idx, opt...); err != nil {
+ errs = append(errs, fmt.Sprintf("failed to validate index Manifests[%d](%s): %v", i, desc.Digest, err))
+ }
+ if err := validateMediaType(idx, desc.MediaType); err != nil {
+ errs = append(errs, fmt.Sprintf("failed to validate index MediaType[%d](%s): %v", i, desc.Digest, err))
+ }
+ case types.OCIManifestSchema1, types.DockerManifestSchema2:
+ img, err := idx.Image(desc.Digest)
+ if err != nil {
+ return err
+ }
+ if err := Image(img, opt...); err != nil {
+ errs = append(errs, fmt.Sprintf("failed to validate image Manifests[%d](%s): %v", i, desc.Digest, err))
+ }
+ if err := validateMediaType(img, desc.MediaType); err != nil {
+ errs = append(errs, fmt.Sprintf("failed to validate image MediaType[%d](%s): %v", i, desc.Digest, err))
+ }
+ default:
+ // Workaround for #819.
+ if wl, ok := idx.(withLayer); ok {
+ layer, err := wl.Layer(desc.Digest)
+ if err != nil {
+ return fmt.Errorf("failed to get layer Manifests[%d]: %w", i, err)
+ }
+ if err := Layer(layer, opt...); err != nil {
+ lerr := fmt.Sprintf("failed to validate layer Manifests[%d](%s): %v", i, desc.Digest, err)
+ if desc.MediaType.IsDistributable() {
+ errs = append(errs, lerr)
+ } else {
+ logs.Warn.Printf("nondistributable layer failure: %v", lerr)
+ }
+ }
+ } else {
+ logs.Warn.Printf("Unexpected manifest: %s", desc.MediaType)
+ }
+ }
+ }
+
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
+
+type withMediaType interface {
+ MediaType() (types.MediaType, error)
+}
+
+func validateMediaType(i withMediaType, want types.MediaType) error {
+ got, err := i.MediaType()
+ if err != nil {
+ return err
+ }
+ if want != got {
+ return fmt.Errorf("mismatched mediaType: MediaType() = %v != %v", got, want)
+ }
+
+ return nil
+}
+
+func validateIndexManifest(idx v1.ImageIndex) error {
+ digest, err := idx.Digest()
+ if err != nil {
+ return err
+ }
+
+ size, err := idx.Size()
+ if err != nil {
+ return err
+ }
+
+ rm, err := idx.RawManifest()
+ if err != nil {
+ return err
+ }
+
+ hash, _, err := v1.SHA256(bytes.NewReader(rm))
+ if err != nil {
+ return err
+ }
+
+ m, err := idx.IndexManifest()
+ if err != nil {
+ return err
+ }
+
+ pm, err := v1.ParseIndexManifest(bytes.NewReader(rm))
+ if err != nil {
+ return err
+ }
+
+ errs := []string{}
+ if digest != hash {
+ errs = append(errs, fmt.Sprintf("mismatched manifest digest: Digest()=%s, SHA256(RawManifest())=%s", digest, hash))
+ }
+
+ if diff := cmp.Diff(pm, m); diff != "" {
+ errs = append(errs, fmt.Sprintf("mismatched manifest content: (-ParseIndexManifest(RawManifest()) +Manifest()) %s", diff))
+ }
+
+ if size != int64(len(rm)) {
+ errs = append(errs, fmt.Sprintf("mismatched manifest size: Size()=%d, len(RawManifest())=%d", size, len(rm)))
+ }
+
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
diff --git a/pkg/v1/validate/layer.go b/pkg/v1/validate/layer.go
new file mode 100644
index 0000000..fdd8f38
--- /dev/null
+++ b/pkg/v1/validate/layer.go
@@ -0,0 +1,191 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package validate
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "crypto"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+)
+
+// Layer validates that the values return by its methods are consistent with the
+// contents returned by Compressed and Uncompressed.
+func Layer(layer v1.Layer, opt ...Option) error {
+ o := makeOptions(opt...)
+ if o.fast {
+ ok, err := partial.Exists(layer)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return fmt.Errorf("layer does not exist")
+ }
+ return nil
+ }
+
+ cl, err := computeLayer(layer)
+ if err != nil {
+ return err
+ }
+
+ errs := []string{}
+
+ digest, err := layer.Digest()
+ if err != nil {
+ return err
+ }
+ diffid, err := layer.DiffID()
+ if err != nil {
+ return err
+ }
+ size, err := layer.Size()
+ if err != nil {
+ return err
+ }
+
+ if digest != cl.digest {
+ errs = append(errs, fmt.Sprintf("mismatched digest: Digest()=%s, SHA256(Compressed())=%s", digest, cl.digest))
+ }
+
+ if diffid != cl.diffid {
+ errs = append(errs, fmt.Sprintf("mismatched diffid: DiffID()=%s, SHA256(Gunzip(Compressed()))=%s", diffid, cl.diffid))
+ }
+
+ if diffid != cl.uncompressedDiffid {
+ errs = append(errs, fmt.Sprintf("mismatched diffid: DiffID()=%s, SHA256(Uncompressed())=%s", diffid, cl.uncompressedDiffid))
+ }
+
+ if size != cl.size {
+ errs = append(errs, fmt.Sprintf("mismatched size: Size()=%d, len(Compressed())=%d", size, cl.size))
+ }
+
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
+
+type computedLayer struct {
+ // Calculated from Compressed stream.
+ digest v1.Hash
+ size int64
+ diffid v1.Hash
+
+ // Calculated from Uncompressed stream.
+ uncompressedDiffid v1.Hash
+ uncompressedSize int64
+}
+
+func computeLayer(layer v1.Layer) (*computedLayer, error) {
+ compressed, err := layer.Compressed()
+ if err != nil {
+ return nil, err
+ }
+
+ // Keep track of compressed digest.
+ digester := crypto.SHA256.New()
+ // Everything read from compressed is written to digester to compute digest.
+ hashCompressed := io.TeeReader(compressed, digester)
+
+ // Call io.Copy to write from the layer Reader through to the tarReader on
+ // the other side of the pipe.
+ pr, pw := io.Pipe()
+ var size int64
+ go func() {
+ n, err := io.Copy(pw, hashCompressed)
+ if err != nil {
+ pw.CloseWithError(err)
+ return
+ }
+ size = n
+
+ // Now close the compressed reader, to flush the gzip stream
+ // and calculate digest/diffID/size. This will cause pr to
+ // return EOF which will cause readers of the Compressed stream
+ // to finish reading.
+ pw.CloseWithError(compressed.Close())
+ }()
+
+ // Read the bytes through gzip.Reader to compute the DiffID.
+ uncompressed, err := gzip.NewReader(pr)
+ if err != nil {
+ return nil, err
+ }
+ diffider := crypto.SHA256.New()
+ hashUncompressed := io.TeeReader(uncompressed, diffider)
+
+ // Ensure there aren't duplicate file paths.
+ tarReader := tar.NewReader(hashUncompressed)
+ files := make(map[string]struct{})
+ for {
+ hdr, err := tarReader.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := files[hdr.Name]; ok {
+ return nil, fmt.Errorf("duplicate file path: %s", hdr.Name)
+ }
+ files[hdr.Name] = struct{}{}
+ }
+
+ // Discard any trailing padding that the tar.Reader doesn't consume.
+ if _, err := io.Copy(io.Discard, hashUncompressed); err != nil {
+ return nil, err
+ }
+
+ if err := uncompressed.Close(); err != nil {
+ return nil, err
+ }
+
+ digest := v1.Hash{
+ Algorithm: "sha256",
+ Hex: hex.EncodeToString(digester.Sum(make([]byte, 0, digester.Size()))),
+ }
+
+ diffid := v1.Hash{
+ Algorithm: "sha256",
+ Hex: hex.EncodeToString(diffider.Sum(make([]byte, 0, diffider.Size()))),
+ }
+
+ ur, err := layer.Uncompressed()
+ if err != nil {
+ return nil, err
+ }
+ defer ur.Close()
+ udiffid, usize, err := v1.SHA256(ur)
+ if err != nil {
+ return nil, err
+ }
+
+ return &computedLayer{
+ digest: digest,
+ diffid: diffid,
+ size: size,
+ uncompressedDiffid: udiffid,
+ uncompressedSize: usize,
+ }, nil
+}
diff --git a/pkg/v1/validate/options.go b/pkg/v1/validate/options.go
new file mode 100644
index 0000000..a6bf2dc
--- /dev/null
+++ b/pkg/v1/validate/options.go
@@ -0,0 +1,37 @@
+// Copyright 2021 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package validate
+
+// Option is a functional option for validate.
+type Option func(*options)
+
+type options struct {
+ fast bool
+}
+
+func makeOptions(opts ...Option) options {
+ opt := options{
+ fast: false,
+ }
+ for _, o := range opts {
+ o(&opt)
+ }
+ return opt
+}
+
+// Fast causes validate to skip reading and digesting layer bytes.
+func Fast(o *options) {
+ o.fast = true
+}
diff --git a/pkg/v1/zz_deepcopy_generated.go b/pkg/v1/zz_deepcopy_generated.go
new file mode 100644
index 0000000..a47b747
--- /dev/null
+++ b/pkg/v1/zz_deepcopy_generated.go
@@ -0,0 +1,339 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Config) DeepCopyInto(out *Config) {
+ *out = *in
+ if in.Cmd != nil {
+ in, out := &in.Cmd, &out.Cmd
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Healthcheck != nil {
+ in, out := &in.Healthcheck, &out.Healthcheck
+ *out = new(HealthConfig)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Entrypoint != nil {
+ in, out := &in.Entrypoint, &out.Entrypoint
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Env != nil {
+ in, out := &in.Env, &out.Env
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Labels != nil {
+ in, out := &in.Labels, &out.Labels
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.OnBuild != nil {
+ in, out := &in.OnBuild, &out.OnBuild
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Volumes != nil {
+ in, out := &in.Volumes, &out.Volumes
+ *out = make(map[string]struct{}, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.ExposedPorts != nil {
+ in, out := &in.ExposedPorts, &out.ExposedPorts
+ *out = make(map[string]struct{}, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Shell != nil {
+ in, out := &in.Shell, &out.Shell
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config.
+func (in *Config) DeepCopy() *Config {
+ if in == nil {
+ return nil
+ }
+ out := new(Config)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ConfigFile) DeepCopyInto(out *ConfigFile) {
+ *out = *in
+ in.Created.DeepCopyInto(&out.Created)
+ if in.History != nil {
+ in, out := &in.History, &out.History
+ *out = make([]History, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ in.RootFS.DeepCopyInto(&out.RootFS)
+ in.Config.DeepCopyInto(&out.Config)
+ if in.OSFeatures != nil {
+ in, out := &in.OSFeatures, &out.OSFeatures
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigFile.
+func (in *ConfigFile) DeepCopy() *ConfigFile {
+ if in == nil {
+ return nil
+ }
+ out := new(ConfigFile)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Descriptor) DeepCopyInto(out *Descriptor) {
+ *out = *in
+ out.Digest = in.Digest
+ if in.Data != nil {
+ in, out := &in.Data, &out.Data
+ *out = make([]byte, len(*in))
+ copy(*out, *in)
+ }
+ if in.URLs != nil {
+ in, out := &in.URLs, &out.URLs
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Annotations != nil {
+ in, out := &in.Annotations, &out.Annotations
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Platform != nil {
+ in, out := &in.Platform, &out.Platform
+ *out = new(Platform)
+ (*in).DeepCopyInto(*out)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Descriptor.
+func (in *Descriptor) DeepCopy() *Descriptor {
+ if in == nil {
+ return nil
+ }
+ out := new(Descriptor)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Hash) DeepCopyInto(out *Hash) {
+ *out = *in
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Hash.
+func (in *Hash) DeepCopy() *Hash {
+ if in == nil {
+ return nil
+ }
+ out := new(Hash)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HealthConfig) DeepCopyInto(out *HealthConfig) {
+ *out = *in
+ if in.Test != nil {
+ in, out := &in.Test, &out.Test
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthConfig.
+func (in *HealthConfig) DeepCopy() *HealthConfig {
+ if in == nil {
+ return nil
+ }
+ out := new(HealthConfig)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *History) DeepCopyInto(out *History) {
+ *out = *in
+ in.Created.DeepCopyInto(&out.Created)
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new History.
+func (in *History) DeepCopy() *History {
+ if in == nil {
+ return nil
+ }
+ out := new(History)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IndexManifest) DeepCopyInto(out *IndexManifest) {
+ *out = *in
+ if in.Manifests != nil {
+ in, out := &in.Manifests, &out.Manifests
+ *out = make([]Descriptor, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Annotations != nil {
+ in, out := &in.Annotations, &out.Annotations
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Subject != nil {
+ in, out := &in.Subject, &out.Subject
+ *out = new(Descriptor)
+ (*in).DeepCopyInto(*out)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexManifest.
+func (in *IndexManifest) DeepCopy() *IndexManifest {
+ if in == nil {
+ return nil
+ }
+ out := new(IndexManifest)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Manifest) DeepCopyInto(out *Manifest) {
+ *out = *in
+ in.Config.DeepCopyInto(&out.Config)
+ if in.Layers != nil {
+ in, out := &in.Layers, &out.Layers
+ *out = make([]Descriptor, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Annotations != nil {
+ in, out := &in.Annotations, &out.Annotations
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Subject != nil {
+ in, out := &in.Subject, &out.Subject
+ *out = new(Descriptor)
+ (*in).DeepCopyInto(*out)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Manifest.
+func (in *Manifest) DeepCopy() *Manifest {
+ if in == nil {
+ return nil
+ }
+ out := new(Manifest)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Platform) DeepCopyInto(out *Platform) {
+ *out = *in
+ if in.OSFeatures != nil {
+ in, out := &in.OSFeatures, &out.OSFeatures
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Features != nil {
+ in, out := &in.Features, &out.Features
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Platform.
+func (in *Platform) DeepCopy() *Platform {
+ if in == nil {
+ return nil
+ }
+ out := new(Platform)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RootFS) DeepCopyInto(out *RootFS) {
+ *out = *in
+ if in.DiffIDs != nil {
+ in, out := &in.DiffIDs, &out.DiffIDs
+ *out = make([]Hash, len(*in))
+ copy(*out, *in)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RootFS.
+func (in *RootFS) DeepCopy() *RootFS {
+ if in == nil {
+ return nil
+ }
+ out := new(RootFS)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Time.
+func (in *Time) DeepCopy() *Time {
+ if in == nil {
+ return nil
+ }
+ out := new(Time)
+ in.DeepCopyInto(out)
+ return out
+}