mirror of
https://github.com/containers/skopeo.git
synced 2025-10-22 11:44:05 +00:00
285 lines
7.1 KiB
Go
285 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/projectatomic/skopeo/types"
|
|
)
|
|
|
|
var (
|
|
validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
|
)
|
|
|
|
type dockerImage struct {
|
|
src *dockerImageSource
|
|
digest string
|
|
rawManifest []byte
|
|
}
|
|
|
|
func parseDockerImage(img, certPath string, tlsVerify bool) (types.Image, error) {
|
|
s, err := newDockerImageSource(img, certPath, tlsVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &dockerImage{src: s}, nil
|
|
}
|
|
|
|
func (i *dockerImage) RawManifest(version string) ([]byte, error) {
|
|
// TODO(runcom): unused version param for now, default to docker v2-1
|
|
if err := i.retrieveRawManifest(); err != nil {
|
|
return nil, err
|
|
}
|
|
return i.rawManifest, nil
|
|
}
|
|
|
|
func (i *dockerImage) Manifest() (types.ImageManifest, error) {
|
|
// TODO(runcom): unused version param for now, default to docker v2-1
|
|
m, err := i.getSchema1Manifest()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ms1, ok := m.(*manifestSchema1)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error retrivieng manifest schema1")
|
|
}
|
|
tags, err := i.getTags()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
imgManifest, err := makeImageManifest(i.src.ref.FullName(), ms1, i.digest, tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return imgManifest, nil
|
|
}
|
|
|
|
func (i *dockerImage) getTags() ([]string, error) {
|
|
// FIXME? Breaking the abstraction.
|
|
url := fmt.Sprintf(tagsURL, i.src.ref.RemoteName())
|
|
res, err := i.src.c.makeRequest("GET", url, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
// print url also
|
|
return nil, fmt.Errorf("Invalid status code returned when fetching tags list %d", res.StatusCode)
|
|
}
|
|
type tagsRes struct {
|
|
Tags []string
|
|
}
|
|
tags := &tagsRes{}
|
|
if err := json.NewDecoder(res.Body).Decode(tags); err != nil {
|
|
return nil, err
|
|
}
|
|
return tags.Tags, nil
|
|
}
|
|
|
|
type config struct {
|
|
Labels map[string]string
|
|
}
|
|
|
|
type v1Image struct {
|
|
// Config is the configuration of the container received from the client
|
|
Config *config `json:"config,omitempty"`
|
|
// DockerVersion specifies version on which image is built
|
|
DockerVersion string `json:"docker_version,omitempty"`
|
|
// Created timestamp when image was created
|
|
Created time.Time `json:"created"`
|
|
// Architecture is the hardware that the image is build and runs on
|
|
Architecture string `json:"architecture,omitempty"`
|
|
// OS is the operating system used to build and run the image
|
|
OS string `json:"os,omitempty"`
|
|
}
|
|
|
|
func makeImageManifest(name string, m *manifestSchema1, dgst string, tagList []string) (types.ImageManifest, error) {
|
|
v1 := &v1Image{}
|
|
if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), v1); err != nil {
|
|
return nil, err
|
|
}
|
|
return &types.DockerImageManifest{
|
|
Name: name,
|
|
Tag: m.Tag,
|
|
Digest: dgst,
|
|
RepoTags: tagList,
|
|
DockerVersion: v1.DockerVersion,
|
|
Created: v1.Created,
|
|
Labels: v1.Config.Labels,
|
|
Architecture: v1.Architecture,
|
|
Os: v1.OS,
|
|
Layers: m.GetLayers(),
|
|
}, nil
|
|
}
|
|
|
|
// TODO(runcom)
|
|
func (i *dockerImage) DockerTar() ([]byte, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// will support v1 one day...
|
|
type manifest interface {
|
|
String() string
|
|
GetLayers() []string
|
|
}
|
|
|
|
type manifestSchema1 struct {
|
|
Name string
|
|
Tag string
|
|
FSLayers []struct {
|
|
BlobSum string `json:"blobSum"`
|
|
} `json:"fsLayers"`
|
|
History []struct {
|
|
V1Compatibility string `json:"v1Compatibility"`
|
|
} `json:"history"`
|
|
// TODO(runcom) verify the downloaded manifest
|
|
//Signature []byte `json:"signature"`
|
|
}
|
|
|
|
func (m *manifestSchema1) GetLayers() []string {
|
|
layers := make([]string, len(m.FSLayers))
|
|
for i, layer := range m.FSLayers {
|
|
layers[i] = layer.BlobSum
|
|
}
|
|
return layers
|
|
}
|
|
|
|
func (m *manifestSchema1) String() string {
|
|
return fmt.Sprintf("%s-%s", sanitize(m.Name), sanitize(m.Tag))
|
|
}
|
|
|
|
func sanitize(s string) string {
|
|
return strings.Replace(s, "/", "-", -1)
|
|
}
|
|
|
|
func (i *dockerImage) retrieveRawManifest() error {
|
|
if i.rawManifest != nil {
|
|
return nil
|
|
}
|
|
manblob, unverifiedCanonicalDigest, err := i.src.GetManifest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
i.rawManifest = manblob
|
|
i.digest = unverifiedCanonicalDigest
|
|
return nil
|
|
}
|
|
|
|
func (i *dockerImage) getSchema1Manifest() (manifest, error) {
|
|
if err := i.retrieveRawManifest(); err != nil {
|
|
return nil, err
|
|
}
|
|
mschema1 := &manifestSchema1{}
|
|
if err := json.Unmarshal(i.rawManifest, mschema1); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := fixManifestLayers(mschema1); err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO(runcom): verify manifest schema 1, 2 etc
|
|
//if len(m.FSLayers) != len(m.History) {
|
|
//return nil, fmt.Errorf("length of history not equal to number of layers for %q", ref.String())
|
|
//}
|
|
//if len(m.FSLayers) == 0 {
|
|
//return nil, fmt.Errorf("no FSLayers in manifest for %q", ref.String())
|
|
//}
|
|
return mschema1, nil
|
|
}
|
|
|
|
func (i *dockerImage) Layers(layers ...string) error {
|
|
m, err := i.getSchema1Manifest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpDir, err := ioutil.TempDir(".", "layers-"+m.String()+"-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dest := NewDirImageDestination(tmpDir)
|
|
data, err := json.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := dest.PutManifest(data); err != nil {
|
|
return err
|
|
}
|
|
if len(layers) == 0 {
|
|
layers = m.GetLayers()
|
|
}
|
|
for _, l := range layers {
|
|
if !strings.HasPrefix(l, "sha256:") {
|
|
l = "sha256:" + l
|
|
}
|
|
if err := i.getLayer(dest, l); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *dockerImage) getLayer(dest types.ImageDestination, digest string) error {
|
|
stream, err := i.src.GetLayer(digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stream.Close()
|
|
return dest.PutLayer(digest, stream)
|
|
}
|
|
|
|
func fixManifestLayers(manifest *manifestSchema1) error {
|
|
type imageV1 struct {
|
|
ID string
|
|
Parent string
|
|
}
|
|
imgs := make([]*imageV1, len(manifest.FSLayers))
|
|
for i := range manifest.FSLayers {
|
|
img := &imageV1{}
|
|
|
|
if err := json.Unmarshal([]byte(manifest.History[i].V1Compatibility), img); err != nil {
|
|
return err
|
|
}
|
|
|
|
imgs[i] = img
|
|
if err := validateV1ID(img.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if imgs[len(imgs)-1].Parent != "" {
|
|
return errors.New("Invalid parent ID in the base layer of the image.")
|
|
}
|
|
// check general duplicates to error instead of a deadlock
|
|
idmap := make(map[string]struct{})
|
|
var lastID string
|
|
for _, img := range imgs {
|
|
// skip IDs that appear after each other, we handle those later
|
|
if _, exists := idmap[img.ID]; img.ID != lastID && exists {
|
|
return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID)
|
|
}
|
|
lastID = img.ID
|
|
idmap[lastID] = struct{}{}
|
|
}
|
|
// backwards loop so that we keep the remaining indexes after removing items
|
|
for i := len(imgs) - 2; i >= 0; i-- {
|
|
if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue
|
|
manifest.FSLayers = append(manifest.FSLayers[:i], manifest.FSLayers[i+1:]...)
|
|
manifest.History = append(manifest.History[:i], manifest.History[i+1:]...)
|
|
} else if imgs[i].Parent != imgs[i+1].ID {
|
|
return fmt.Errorf("Invalid parent ID. Expected %v, got %v.", imgs[i+1].ID, imgs[i].Parent)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateV1ID(id string) error {
|
|
if ok := validHex.MatchString(id); !ok {
|
|
return fmt.Errorf("image ID %q is invalid", id)
|
|
}
|
|
return nil
|
|
}
|