2017-06-15 14:41:54 -04:00
package manifest
import (
2018-05-03 21:02:44 -04:00
"context"
2017-06-15 14:41:54 -04:00
"encoding/json"
"fmt"
"io"
2023-08-30 18:36:58 -04:00
"github.com/distribution/reference"
2017-06-15 14:41:54 -04:00
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/types"
registryclient "github.com/docker/cli/cli/registry/client"
2018-06-28 20:41:47 -04:00
"github.com/docker/distribution"
2017-06-15 14:41:54 -04:00
"github.com/docker/distribution/manifest/manifestlist"
2023-01-25 11:26:43 -05:00
"github.com/docker/distribution/manifest/ocischema"
2017-06-15 14:41:54 -04:00
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type pushOpts struct {
insecure bool
purge bool
target string
}
type mountRequest struct {
ref reference . Named
manifest types . ImageManifest
}
type manifestBlob struct {
canonical reference . Canonical
os string
}
type pushRequest struct {
targetRef reference . Named
list * manifestlist . DeserializedManifestList
mountRequests [ ] mountRequest
manifestBlobs [ ] manifestBlob
insecure bool
}
func newPushListCommand ( dockerCli command . Cli ) * cobra . Command {
opts := pushOpts { }
cmd := & cobra . Command {
Use : "push [OPTIONS] MANIFEST_LIST" ,
Short : "Push a manifest list to a repository" ,
Args : cli . ExactArgs ( 1 ) ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
opts . target = args [ 0 ]
return runPush ( dockerCli , opts )
} ,
}
flags := cmd . Flags ( )
flags . BoolVarP ( & opts . purge , "purge" , "p" , false , "Remove the local manifest list after push" )
flags . BoolVar ( & opts . insecure , "insecure" , false , "Allow push to an insecure registry" )
return cmd
}
func runPush ( dockerCli command . Cli , opts pushOpts ) error {
targetRef , err := normalizeReference ( opts . target )
if err != nil {
return err
}
manifests , err := dockerCli . ManifestStore ( ) . GetList ( targetRef )
if err != nil {
return err
}
if len ( manifests ) == 0 {
return errors . Errorf ( "%s not found" , targetRef )
}
2023-11-20 07:04:09 -05:00
req , err := buildPushRequest ( manifests , targetRef , opts . insecure )
2017-06-15 14:41:54 -04:00
if err != nil {
return err
}
ctx := context . Background ( )
2023-11-20 07:04:09 -05:00
if err := pushList ( ctx , dockerCli , req ) ; err != nil {
2017-06-15 14:41:54 -04:00
return err
}
if opts . purge {
return dockerCli . ManifestStore ( ) . Remove ( targetRef )
}
return nil
}
func buildPushRequest ( manifests [ ] types . ImageManifest , targetRef reference . Named , insecure bool ) ( pushRequest , error ) {
req := pushRequest { targetRef : targetRef , insecure : insecure }
var err error
req . list , err = buildManifestList ( manifests , targetRef )
if err != nil {
return req , err
}
targetRepo , err := registry . ParseRepositoryInfo ( targetRef )
if err != nil {
return req , err
}
targetRepoName , err := registryclient . RepoNameForReference ( targetRepo . Name )
if err != nil {
return req , err
}
for _ , imageManifest := range manifests {
manifestRepoName , err := registryclient . RepoNameForReference ( imageManifest . Ref )
if err != nil {
return req , err
}
repoName , _ := reference . WithName ( manifestRepoName )
if repoName . Name ( ) != targetRepoName {
blobs , err := buildBlobRequestList ( imageManifest , repoName )
if err != nil {
return req , err
}
req . manifestBlobs = append ( req . manifestBlobs , blobs ... )
manifestPush , err := buildPutManifestRequest ( imageManifest , targetRef )
if err != nil {
return req , err
}
req . mountRequests = append ( req . mountRequests , manifestPush )
}
}
return req , nil
}
func buildManifestList ( manifests [ ] types . ImageManifest , targetRef reference . Named ) ( * manifestlist . DeserializedManifestList , error ) {
targetRepoInfo , err := registry . ParseRepositoryInfo ( targetRef )
if err != nil {
return nil , err
}
descriptors := [ ] manifestlist . ManifestDescriptor { }
for _ , imageManifest := range manifests {
2018-06-28 20:41:47 -04:00
if imageManifest . Descriptor . Platform == nil ||
imageManifest . Descriptor . Platform . Architecture == "" ||
imageManifest . Descriptor . Platform . OS == "" {
2017-06-15 14:41:54 -04:00
return nil , errors . Errorf (
"manifest %s must have an OS and Architecture to be pushed to a registry" , imageManifest . Ref )
}
descriptor , err := buildManifestDescriptor ( targetRepoInfo , imageManifest )
if err != nil {
return nil , err
}
descriptors = append ( descriptors , descriptor )
}
return manifestlist . FromDescriptors ( descriptors )
}
func buildManifestDescriptor ( targetRepo * registry . RepositoryInfo , imageManifest types . ImageManifest ) ( manifestlist . ManifestDescriptor , error ) {
repoInfo , err := registry . ParseRepositoryInfo ( imageManifest . Ref )
if err != nil {
return manifestlist . ManifestDescriptor { } , err
}
manifestRepoHostname := reference . Domain ( repoInfo . Name )
targetRepoHostname := reference . Domain ( targetRepo . Name )
if manifestRepoHostname != targetRepoHostname {
return manifestlist . ManifestDescriptor { } , errors . Errorf ( "cannot use source images from a different registry than the target image: %s != %s" , manifestRepoHostname , targetRepoHostname )
}
2018-06-28 20:41:47 -04:00
manifest := manifestlist . ManifestDescriptor {
Descriptor : distribution . Descriptor {
Digest : imageManifest . Descriptor . Digest ,
Size : imageManifest . Descriptor . Size ,
MediaType : imageManifest . Descriptor . MediaType ,
} ,
2017-06-15 14:41:54 -04:00
}
2018-06-28 20:41:47 -04:00
platform := types . PlatformSpecFromOCI ( imageManifest . Descriptor . Platform )
if platform != nil {
manifest . Platform = * platform
2017-06-15 14:41:54 -04:00
}
if err = manifest . Descriptor . Digest . Validate ( ) ; err != nil {
return manifestlist . ManifestDescriptor { } , errors . Wrapf ( err ,
2017-09-26 18:15:04 -04:00
"digest parse of image %q failed" , imageManifest . Ref )
2017-06-15 14:41:54 -04:00
}
return manifest , nil
}
func buildBlobRequestList ( imageManifest types . ImageManifest , repoName reference . Named ) ( [ ] manifestBlob , error ) {
2022-09-03 14:07:29 -04:00
blobs := imageManifest . Blobs ( )
blobReqs := make ( [ ] manifestBlob , 0 , len ( blobs ) )
for _ , blobDigest := range blobs {
2017-06-15 14:41:54 -04:00
canonical , err := reference . WithDigest ( repoName , blobDigest )
if err != nil {
return nil , err
}
2018-06-28 20:41:47 -04:00
var os string
if imageManifest . Descriptor . Platform != nil {
os = imageManifest . Descriptor . Platform . OS
}
blobReqs = append ( blobReqs , manifestBlob { canonical : canonical , os : os } )
2017-06-15 14:41:54 -04:00
}
return blobReqs , nil
}
func buildPutManifestRequest ( imageManifest types . ImageManifest , targetRef reference . Named ) ( mountRequest , error ) {
refWithoutTag , err := reference . WithName ( targetRef . Name ( ) )
if err != nil {
return mountRequest { } , err
}
2018-06-28 20:41:47 -04:00
mountRef , err := reference . WithDigest ( refWithoutTag , imageManifest . Descriptor . Digest )
2017-06-15 14:41:54 -04:00
if err != nil {
return mountRequest { } , err
}
2023-01-27 05:31:07 -05:00
// Attempt to reconstruct indentation of the manifest to ensure sha parity
2023-01-27 05:48:54 -05:00
// with the registry - if we haven't preserved the raw content.
2023-01-27 05:31:07 -05:00
//
// This is necessary because our previous internal storage format did not
// preserve whitespace. If we don't have the newer format present, we can
// attempt the reconstruction like before, but explicitly error if the
// reconstruction failed!
2023-01-25 11:26:43 -05:00
switch {
case imageManifest . SchemaV2Manifest != nil :
2023-01-27 05:48:54 -05:00
dt := imageManifest . Raw
if len ( dt ) == 0 {
dt , err = json . MarshalIndent ( imageManifest . SchemaV2Manifest , "" , " " )
if err != nil {
return mountRequest { } , err
}
2023-01-25 11:26:43 -05:00
}
2023-01-27 05:31:07 -05:00
dig := imageManifest . Descriptor . Digest
if dig2 := dig . Algorithm ( ) . FromBytes ( dt ) ; dig != dig2 {
return mountRequest { } , errors . Errorf ( "internal digest mismatch for %s: expected %s, got %s" , imageManifest . Ref , dig , dig2 )
}
2023-01-25 11:26:43 -05:00
var manifest schema2 . DeserializedManifest
if err = manifest . UnmarshalJSON ( dt ) ; err != nil {
return mountRequest { } , err
}
imageManifest . SchemaV2Manifest = & manifest
case imageManifest . OCIManifest != nil :
2023-01-27 05:48:54 -05:00
dt := imageManifest . Raw
if len ( dt ) == 0 {
dt , err = json . MarshalIndent ( imageManifest . OCIManifest , "" , " " )
if err != nil {
return mountRequest { } , err
}
2023-01-25 11:26:43 -05:00
}
2023-01-27 05:31:07 -05:00
dig := imageManifest . Descriptor . Digest
if dig2 := dig . Algorithm ( ) . FromBytes ( dt ) ; dig != dig2 {
return mountRequest { } , errors . Errorf ( "internal digest mismatch for %s: expected %s, got %s" , imageManifest . Ref , dig , dig2 )
}
2023-01-25 11:26:43 -05:00
var manifest ocischema . DeserializedManifest
if err = manifest . UnmarshalJSON ( dt ) ; err != nil {
return mountRequest { } , err
}
imageManifest . OCIManifest = & manifest
2017-06-15 14:41:54 -04:00
}
return mountRequest { ref : mountRef , manifest : imageManifest } , err
}
func pushList ( ctx context . Context , dockerCli command . Cli , req pushRequest ) error {
rclient := dockerCli . RegistryClient ( req . insecure )
if err := mountBlobs ( ctx , rclient , req . targetRef , req . manifestBlobs ) ; err != nil {
return err
}
if err := pushReferences ( ctx , dockerCli . Out ( ) , rclient , req . mountRequests ) ; err != nil {
return err
}
dgst , err := rclient . PutManifest ( ctx , req . targetRef , req . list )
if err != nil {
return err
}
fmt . Fprintln ( dockerCli . Out ( ) , dgst . String ( ) )
return nil
}
func pushReferences ( ctx context . Context , out io . Writer , client registryclient . RegistryClient , mounts [ ] mountRequest ) error {
for _ , mount := range mounts {
newDigest , err := client . PutManifest ( ctx , mount . ref , mount . manifest )
if err != nil {
return err
}
fmt . Fprintf ( out , "Pushed ref %s with digest: %s\n" , mount . ref , newDigest )
}
return nil
}
func mountBlobs ( ctx context . Context , client registryclient . RegistryClient , ref reference . Named , blobs [ ] manifestBlob ) error {
for _ , blob := range blobs {
err := client . MountBlob ( ctx , blob . canonical , ref )
switch err . ( type ) {
case nil :
case registryclient . ErrBlobCreated :
if blob . os != "windows" {
return fmt . Errorf ( "error mounting %s to %s" , blob . canonical , ref )
}
default :
return err
}
}
return nil
}