VMware vSphere Integrated Containers provider (#206)
* Add Virtual Kubelet provider for VIC Initial virtual kubelet provider for VMware VIC. This provider currently handles creating and starting of a pod VM via the VIC portlayer and persona server. Image store handling via the VIC persona server. This provider currently requires the feature/wolfpack branch of VIC. * Added pod stop and delete. Also added node capacity. Added the ability to stop and delete pod VMs via VIC. Also retrieve node capacity information from the VCH. * Cleanup and readme file Some file clean up and added a Readme.md markdown file for the VIC provider. * Cleaned up errors, added function comments, moved operation code 1. Cleaned up error handling. Set standard for creating errors. 2. Added method prototype comments for all interface functions. 3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder. * Add mocking code and unit tests for podcache, podcreator, and podstarter Used the unit test framework used in VIC to handle assertions in the provider's unit test. Mocking code generated using OSS project mockery, which is compatible with the testify assertion framework. * Vendored packages for the VIC provider Requires feature/wolfpack branch of VIC and a few specific commit sha of projects used within VIC. * Implementation of POD Stopper and Deleter unit tests (#4) * Updated files for initial PR
This commit is contained in:
485
vendor/github.com/vmware/vic/lib/imagec/docker.go
generated
vendored
Normal file
485
vendor/github.com/vmware/vic/lib/imagec/docker.go
generated
vendored
Normal file
@@ -0,0 +1,485 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package imagec
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
ddigest "github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
dlayer "github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/docker/libtrust"
|
||||
|
||||
urlfetcher "github.com/vmware/vic/pkg/fetcher"
|
||||
registryutils "github.com/vmware/vic/pkg/registry"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
// DigestSHA256EmptyTar is the canonical sha256 digest of empty tar file -
|
||||
// (1024 NULL bytes)
|
||||
DigestSHA256EmptyTar = string(dlayer.DigestSHA256EmptyTar)
|
||||
)
|
||||
|
||||
// FSLayer is a container struct for BlobSums defined in an image manifest
|
||||
type FSLayer struct {
|
||||
// BlobSum is the tarsum of the referenced filesystem image layer
|
||||
BlobSum string `json:"blobSum"`
|
||||
}
|
||||
|
||||
// History is a container struct for V1Compatibility defined in an image manifest
|
||||
type History struct {
|
||||
V1Compatibility string `json:"v1Compatibility"`
|
||||
}
|
||||
|
||||
// Manifest represents the Docker Manifest file
|
||||
type Manifest struct {
|
||||
Name string `json:"name"`
|
||||
Tag string `json:"tag"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
FSLayers []FSLayer `json:"fsLayers"`
|
||||
History []History `json:"history"`
|
||||
// ignoring signatures
|
||||
}
|
||||
|
||||
// LearnRegistryURL returns the registry URL after making sure that it responds to queries
|
||||
func LearnRegistryURL(options *Options) (string, error) {
|
||||
defer trace.End(trace.Begin(options.Registry))
|
||||
|
||||
log.Debugf("Trying https scheme for %#v", options)
|
||||
|
||||
registry, err := registryutils.Reachable(options.Registry, "https", options.Username, options.Password, options.RegistryCAs, options.Timeout, options.InsecureSkipVerify)
|
||||
|
||||
if err != nil && options.InsecureAllowHTTP {
|
||||
// try https without verification
|
||||
log.Debugf("Trying https without verification, last error: %+v", err)
|
||||
registry, err = registryutils.Reachable(options.Registry, "https", options.Username, options.Password, options.RegistryCAs, options.Timeout, true)
|
||||
if err == nil {
|
||||
// Success, set InsecureSkipVerify to true
|
||||
options.InsecureSkipVerify = true
|
||||
} else {
|
||||
// try http
|
||||
log.Debugf("Falling back to http")
|
||||
registry, err = registryutils.Reachable(options.Registry, "http", options.Username, options.Password, options.RegistryCAs, options.Timeout, options.InsecureSkipVerify)
|
||||
}
|
||||
}
|
||||
|
||||
return registry, err
|
||||
}
|
||||
|
||||
// LearnAuthURL returns the URL of the OAuth endpoint
|
||||
func LearnAuthURL(options Options) (*url.URL, error) {
|
||||
defer trace.End(trace.Begin(options.Reference.String()))
|
||||
|
||||
url, err := url.Parse(options.Registry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tagOrDigest := tagOrDigest(options.Reference, options.Tag)
|
||||
manifestURL, err := url.Parse(path.Join(url.Path, options.Image, "manifests", tagOrDigest))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fetcher := urlfetcher.NewURLFetcher(urlfetcher.Options{
|
||||
Timeout: options.Timeout,
|
||||
Username: options.Username,
|
||||
Password: options.Password,
|
||||
InsecureSkipVerify: options.InsecureSkipVerify,
|
||||
RootCAs: options.RegistryCAs,
|
||||
})
|
||||
|
||||
// We expect docker registry to return a 401 to us - with a WWW-Authenticate header
|
||||
// We parse that header and learn the OAuth endpoint to fetch OAuth token.
|
||||
log.Debugf("Pinging %s", manifestURL.String())
|
||||
hdr, err := fetcher.Ping(manifestURL)
|
||||
if err == nil && fetcher.IsStatusUnauthorized() {
|
||||
return fetcher.ExtractOAuthURL(hdr.Get("www-authenticate"), nil)
|
||||
}
|
||||
|
||||
if !fetcher.IsStatusOK() {
|
||||
// Try with just the registry url. This works better with some registries (e.g.
|
||||
// Artifactory)
|
||||
log.Debugf("Pinging %s", url.String())
|
||||
hdr, err = fetcher.Ping(url)
|
||||
if err == nil && fetcher.IsStatusUnauthorized() {
|
||||
return fetcher.ExtractOAuthURL(hdr.Get("www-authenticate"), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Private registry returned the manifest directly as auth option is optional.
|
||||
// https://github.com/docker/distribution/blob/master/docs/configuration.md#auth
|
||||
if err == nil && options.Registry != DefaultDockerURL && fetcher.IsStatusOK() {
|
||||
log.Debugf("%s does not support OAuth", url)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Do we even have the image on that registry
|
||||
if err != nil && fetcher.IsStatusNotFound() {
|
||||
err = fmt.Errorf("image not found")
|
||||
return nil, urlfetcher.ImageNotFoundError{Err: err}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s returned an unexpected response: %s", url, err)
|
||||
}
|
||||
|
||||
// FetchToken fetches the OAuth token from OAuth endpoint
|
||||
func FetchToken(ctx context.Context, options Options, url *url.URL, progressOutput progress.Output) (*urlfetcher.Token, error) {
|
||||
defer trace.End(trace.Begin(url.String()))
|
||||
|
||||
log.Debugf("URL: %s", url)
|
||||
|
||||
fetcher := urlfetcher.NewURLFetcher(urlfetcher.Options{
|
||||
Timeout: options.Timeout,
|
||||
Username: options.Username,
|
||||
Password: options.Password,
|
||||
InsecureSkipVerify: options.InsecureSkipVerify,
|
||||
RootCAs: options.RegistryCAs,
|
||||
})
|
||||
|
||||
token, err := fetcher.FetchAuthToken(url)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("FetchToken (%s) failed: %s", url, err)
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// FetchImageBlob fetches the image blob
|
||||
func FetchImageBlob(ctx context.Context, options Options, image *ImageWithMeta, progressOutput progress.Output) (string, error) {
|
||||
defer trace.End(trace.Begin(options.Image + "/" + image.Layer.BlobSum))
|
||||
|
||||
id := image.ID
|
||||
layer := image.Layer.BlobSum
|
||||
meta := image.Meta
|
||||
diffID := ""
|
||||
|
||||
url, err := url.Parse(options.Registry)
|
||||
if err != nil {
|
||||
return diffID, err
|
||||
}
|
||||
url.Path = path.Join(url.Path, options.Image, "blobs", layer)
|
||||
|
||||
log.Debugf("URL: %s\n ", url)
|
||||
|
||||
fetcher := urlfetcher.NewURLFetcher(urlfetcher.Options{
|
||||
Timeout: options.Timeout,
|
||||
Username: options.Username,
|
||||
Password: options.Password,
|
||||
Token: options.Token,
|
||||
InsecureSkipVerify: options.InsecureSkipVerify,
|
||||
RootCAs: options.RegistryCAs,
|
||||
})
|
||||
|
||||
// ctx
|
||||
ctx, cancel := context.WithTimeout(ctx, options.Timeout)
|
||||
defer cancel()
|
||||
|
||||
imageFileName, err := fetcher.Fetch(ctx, url, nil, true, progressOutput, image.String())
|
||||
if err != nil {
|
||||
return diffID, err
|
||||
}
|
||||
|
||||
// Cleanup function for the error case
|
||||
defer func() {
|
||||
if err != nil {
|
||||
os.Remove(imageFileName)
|
||||
}
|
||||
}()
|
||||
|
||||
// Open the file so that we can use it as a io.Reader for sha256 calculation
|
||||
imageFile, err := os.Open(string(imageFileName))
|
||||
if err != nil {
|
||||
return diffID, err
|
||||
}
|
||||
defer imageFile.Close()
|
||||
|
||||
// blobSum is the sha of the compressed layer
|
||||
blobSum := sha256.New()
|
||||
|
||||
// diffIDSum is the sha of the uncompressed layer
|
||||
diffIDSum := sha256.New()
|
||||
|
||||
// blobTr is an io.TeeReader that writes bytes to blobSum that it reads from imageFile
|
||||
// see https://golang.org/pkg/io/#TeeReader
|
||||
blobTr := io.TeeReader(imageFile, blobSum)
|
||||
|
||||
progress.Update(progressOutput, image.String(), "Verifying Checksum")
|
||||
decompressedTar, err := archive.DecompressStream(blobTr)
|
||||
if err != nil {
|
||||
return diffID, err
|
||||
}
|
||||
|
||||
// Copy bytes from decompressed layer into diffIDSum to calculate diffID
|
||||
_, cerr := io.Copy(diffIDSum, decompressedTar)
|
||||
if cerr != nil {
|
||||
return diffID, cerr
|
||||
}
|
||||
|
||||
bs := fmt.Sprintf("sha256:%x", blobSum.Sum(nil))
|
||||
if bs != layer {
|
||||
return diffID, fmt.Errorf("Failed to validate layer checksum. Expected %s got %s", layer, bs)
|
||||
}
|
||||
|
||||
diffID = fmt.Sprintf("sha256:%x", diffIDSum.Sum(nil))
|
||||
|
||||
// this isn't an empty layer, so we need to calculate the size
|
||||
if diffID != string(DigestSHA256EmptyTar) {
|
||||
var layerSize int64
|
||||
|
||||
// seek to the beginning of the file
|
||||
imageFile.Seek(0, 0)
|
||||
|
||||
// recreate the decompressed tar Reader
|
||||
decompressedTar, err := archive.DecompressStream(imageFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// get a tar reader for access to the files in the archive
|
||||
tr := tar.NewReader(decompressedTar)
|
||||
|
||||
// iterate through tar headers to get file sizes
|
||||
for {
|
||||
tarHeader, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
layerSize += tarHeader.Size
|
||||
}
|
||||
|
||||
image.Size = layerSize
|
||||
}
|
||||
|
||||
log.Infof("diffID for layer %s: %s", id, diffID)
|
||||
|
||||
// Ensure the parent directory exists
|
||||
destination := path.Join(DestinationDirectory(options), id)
|
||||
err = os.MkdirAll(destination, 0755) /* #nosec */
|
||||
if err != nil {
|
||||
return diffID, err
|
||||
}
|
||||
|
||||
// Move(rename) the temporary file to its final destination
|
||||
err = os.Rename(string(imageFileName), path.Join(destination, id+".tar"))
|
||||
if err != nil {
|
||||
return diffID, err
|
||||
}
|
||||
|
||||
// Dump the history next to it
|
||||
err = ioutil.WriteFile(path.Join(destination, id+".json"), []byte(meta), 0644)
|
||||
if err != nil {
|
||||
return diffID, err
|
||||
}
|
||||
|
||||
progress.Update(progressOutput, image.String(), "Download complete")
|
||||
|
||||
return diffID, nil
|
||||
}
|
||||
|
||||
// tagOrDigest returns an image's digest if it's pulled by digest, or its tag
|
||||
// otherwise.
|
||||
func tagOrDigest(r reference.Named, tag string) string {
|
||||
if digested, ok := r.(reference.Canonical); ok {
|
||||
return digested.Digest().String()
|
||||
}
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
// FetchImageManifest fetches the image manifest file
|
||||
func FetchImageManifest(ctx context.Context, options Options, schemaVersion int, progressOutput progress.Output) (interface{}, string, error) {
|
||||
defer trace.End(trace.Begin(options.Reference.String()))
|
||||
|
||||
if schemaVersion != 1 && schemaVersion != 2 {
|
||||
return nil, "", fmt.Errorf("Unknown schema version %d requested!", schemaVersion)
|
||||
}
|
||||
|
||||
url, err := url.Parse(options.Registry)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
tagOrDigest := tagOrDigest(options.Reference, options.Tag)
|
||||
url.Path = path.Join(url.Path, options.Image, "manifests", tagOrDigest)
|
||||
log.Debugf("URL: %s", url)
|
||||
|
||||
fetcher := urlfetcher.NewURLFetcher(urlfetcher.Options{
|
||||
Timeout: options.Timeout,
|
||||
Username: options.Username,
|
||||
Password: options.Password,
|
||||
Token: options.Token,
|
||||
InsecureSkipVerify: options.InsecureSkipVerify,
|
||||
RootCAs: options.RegistryCAs,
|
||||
})
|
||||
|
||||
reqHeaders := make(http.Header)
|
||||
if schemaVersion == 2 {
|
||||
reqHeaders.Add("Accept", schema2.MediaTypeManifest)
|
||||
reqHeaders.Add("Accept", schema1.MediaTypeManifest)
|
||||
}
|
||||
|
||||
manifestFileName, err := fetcher.Fetch(ctx, url, &reqHeaders, true, progressOutput)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Cleanup function for the error case
|
||||
defer func() {
|
||||
if err != nil {
|
||||
os.Remove(manifestFileName)
|
||||
}
|
||||
}()
|
||||
|
||||
switch schemaVersion {
|
||||
case 1: //schema 1, signed manifest
|
||||
return decodeManifestSchema1(manifestFileName, options, url.Hostname())
|
||||
case 2: //schema 2
|
||||
return decodeManifestSchema2(manifestFileName, options)
|
||||
}
|
||||
|
||||
//We shouldn't really get here
|
||||
return nil, "", fmt.Errorf("Unknown schema version %d requested!", schemaVersion)
|
||||
}
|
||||
|
||||
// decodeManifestSchema1() reads a manifest schema 1 and creates an imageC
|
||||
// defined Manifest structure and returns the digest of the manifest as a string.
|
||||
// For historical reason, we did not use the Docker's defined schema1.Manifest
|
||||
// instead of our own and probably should do so in the future.
|
||||
func decodeManifestSchema1(filename string, options Options, registry string) (interface{}, string, error) {
|
||||
// Read the entire file into []byte for json.Unmarshal
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
manifest := &Manifest{}
|
||||
err = json.Unmarshal(content, manifest)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
digest, err := getManifestDigest(content, options.Reference)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
manifest.Digest = digest
|
||||
|
||||
// Verify schema 1 manifest's fields per docker/docker/distribution/pull_v2.go
|
||||
numFSLayers := len(manifest.FSLayers)
|
||||
if numFSLayers == 0 {
|
||||
return nil, "", fmt.Errorf("no FSLayers in manifest")
|
||||
}
|
||||
if numFSLayers != len(manifest.History) {
|
||||
return nil, "", fmt.Errorf("length of history not equal to number of layers")
|
||||
}
|
||||
|
||||
return manifest, digest, nil
|
||||
}
|
||||
|
||||
// verifyManifestDigest checks the manifest digest against the received payload.
|
||||
func verifyManifestDigest(digested reference.Canonical, bytes []byte) error {
|
||||
verifier, err := ddigest.NewDigestVerifier(digested.Digest())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = verifier.Write(bytes); err != nil {
|
||||
return err
|
||||
}
|
||||
if !verifier.Verified() {
|
||||
return fmt.Errorf("image manifest verification failed for digest %s", digested.Digest())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodeManifestSchema2() reads a manifest schema 2 and creates a Docker
|
||||
// defined Manifest structure and returns the digest of the manifest as a string.
|
||||
func decodeManifestSchema2(filename string, options Options) (interface{}, string, error) {
|
||||
// Read the entire file into []byte for json.Unmarshal
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
manifest := &schema2.DeserializedManifest{}
|
||||
|
||||
err = json.Unmarshal(content, manifest)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
_, canonical, err := manifest.Payload()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
digest := ddigest.FromBytes(canonical)
|
||||
|
||||
return manifest, string(digest), nil
|
||||
}
|
||||
|
||||
func getManifestDigest(content []byte, ref reference.Named) (string, error) {
|
||||
jsonSig, err := libtrust.ParsePrettySignature(content, "signatures")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Resolve the payload in the manifest.
|
||||
bytes, err := jsonSig.Payload()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debugf("Canonical Bytes: %d", len(bytes))
|
||||
|
||||
// Verify the manifest digest if the image is pulled by digest. If the image
|
||||
// is not pulled by digest, we proceed without this check because we don't
|
||||
// have a digest to verify the received content with.
|
||||
// https://docs.docker.com/registry/spec/api/#content-digests
|
||||
if digested, ok := ref.(reference.Canonical); ok {
|
||||
if err := verifyManifestDigest(digested, bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
digest := ddigest.FromBytes(bytes)
|
||||
// Correct Manifest Digest
|
||||
log.Debugf("Manifest Digest: %v", digest)
|
||||
return string(digest), nil
|
||||
}
|
||||
105
vendor/github.com/vmware/vic/lib/imagec/docker_test.go
generated
vendored
Normal file
105
vendor/github.com/vmware/vic/lib/imagec/docker_test.go
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package imagec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
UbuntuTaggedRef = "library/ubuntu:latest"
|
||||
UbuntuDigest = "ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"
|
||||
UbuntuDigestSHA = "sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"
|
||||
UbuntuDigestManifest = `{
|
||||
"schemaVersion": 1,
|
||||
"name": "library/ubuntu",
|
||||
"tag": "14.04",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:28a2f68d1120598986362662445c47dce7ec13c2662479e7aab9f0ecad4a7416"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:fd2731e4c50ce221d785d4ce26a8430bca9a95bfe4162fafc997a1cc65682cce"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5a132a7e7af11f304041e93efb9cb2a0a7839bccaec5a03cfbdc9a3f5d0eb481"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"56063ad57855f2632f641a622efa81a0feda1731bfadda497b1288d11feef4da\",\"parent\":\"4e1f7c524148bf80fcc4ce9991e88d708048d38440e3e3a14d56e72c17ddccc7\",\"created\":\"2016-03-03T21:38:53.80360049Z\",\"container\":\"b6361ab0a2a82f71c5bd3becbb9c854331f8259b9c3fe466bf6e7e073c735a2c\",\"container_config\":{\"Hostname\":\"c24ffee5b2b8\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/bin/bash\\\"]\"],\"Image\":\"4e1f7c524148bf80fcc4ce9991e88d708048d38440e3e3a14d56e72c17ddccc7\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"c24ffee5b2b8\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":[\"/bin/bash\"],\"Image\":\"4e1f7c524148bf80fcc4ce9991e88d708048d38440e3e3a14d56e72c17ddccc7\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\"}"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"4e1f7c524148bf80fcc4ce9991e88d708048d38440e3e3a14d56e72c17ddccc7\",\"parent\":\"38112156678df7d8001ae944f118d283009565540dc0cd88fb39fccc88c3c7f2\",\"created\":\"2016-03-03T21:38:53.085760873Z\",\"container\":\"ccc6ec8b31df981344b4107bd890394be35564adb8d13df34d1cb1849c7f0c3e\",\"container_config\":{\"Hostname\":\"c24ffee5b2b8\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":[\"/bin/sh\",\"-c\",\"sed -i 's/^#\\\\s*\\\\(deb.*universe\\\\)$/\\\\1/g' /etc/apt/sources.list\"],\"Image\":\"38112156678df7d8001ae944f118d283009565540dc0cd88fb39fccc88c3c7f2\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"c24ffee5b2b8\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":null,\"Image\":\"38112156678df7d8001ae944f118d283009565540dc0cd88fb39fccc88c3c7f2\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":1895}"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"38112156678df7d8001ae944f118d283009565540dc0cd88fb39fccc88c3c7f2\",\"parent\":\"454970bd163ba95435b50e963edd63b2b2fff4c1845e5d3cd03d5ba8afb8a08d\",\"created\":\"2016-03-03T21:38:51.45368726Z\",\"container\":\"3c8556d1a209f22cfbc87f3cbd9bcb6674c5f9645a14aa488756d129f6987f40\",\"container_config\":{\"Hostname\":\"c24ffee5b2b8\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":[\"/bin/sh\",\"-c\",\"echo '#!/bin/sh' \\u003e /usr/sbin/policy-rc.d \\t\\u0026\\u0026 echo 'exit 101' \\u003e\\u003e /usr/sbin/policy-rc.d \\t\\u0026\\u0026 chmod +x /usr/sbin/policy-rc.d \\t\\t\\u0026\\u0026 dpkg-divert --local --rename --add /sbin/initctl \\t\\u0026\\u0026 cp -a /usr/sbin/policy-rc.d /sbin/initctl \\t\\u0026\\u0026 sed -i 's/^exit.*/exit 0/' /sbin/initctl \\t\\t\\u0026\\u0026 echo 'force-unsafe-io' \\u003e /etc/dpkg/dpkg.cfg.d/docker-apt-speedup \\t\\t\\u0026\\u0026 echo 'DPkg::Post-Invoke { \\\"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true\\\"; };' \\u003e /etc/apt/apt.conf.d/docker-clean \\t\\u0026\\u0026 echo 'APT::Update::Post-Invoke { \\\"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true\\\"; };' \\u003e\\u003e /etc/apt/apt.conf.d/docker-clean \\t\\u0026\\u0026 echo 'Dir::Cache::pkgcache \\\"\\\"; Dir::Cache::srcpkgcache \\\"\\\";' \\u003e\\u003e /etc/apt/apt.conf.d/docker-clean \\t\\t\\u0026\\u0026 echo 'Acquire::Languages \\\"none\\\";' \\u003e /etc/apt/apt.conf.d/docker-no-languages \\t\\t\\u0026\\u0026 echo 'Acquire::GzipIndexes \\\"true\\\"; Acquire::CompressionTypes::Order:: \\\"gz\\\";' \\u003e /etc/apt/apt.conf.d/docker-gzip-indexes\"],\"Image\":\"454970bd163ba95435b50e963edd63b2b2fff4c1845e5d3cd03d5ba8afb8a08d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"c24ffee5b2b8\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":null,\"Image\":\"454970bd163ba95435b50e963edd63b2b2fff4c1845e5d3cd03d5ba8afb8a08d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":194533}"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"454970bd163ba95435b50e963edd63b2b2fff4c1845e5d3cd03d5ba8afb8a08d\",\"created\":\"2016-03-03T21:38:46.169812943Z\",\"container\":\"c24ffee5b2b808674d335e2c42c9c47aa9aff1b368eb5920018cde7dd26f2046\",\"container_config\":{\"Hostname\":\"c24ffee5b2b8\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:b9504126dc55908988977286e89c43c7ea73a506d43fae82c29ef132e21b6ece in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"c24ffee5b2b8\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":187763841}"
|
||||
}
|
||||
],
|
||||
"signatures": [
|
||||
{
|
||||
"header": {
|
||||
"jwk": {
|
||||
"crv": "P-256",
|
||||
"kid": "G2JA:NPRD:EWRM:EFEJ:4PHQ:TRZR:6W6O:D5LC:UJ36:RHOE:ZN7D:N55I",
|
||||
"kty": "EC",
|
||||
"x": "NrETepARqTLeOBcTdBCE8K8jbQJgiTH1p7XJ78zBxjk",
|
||||
"y": "ay0SmJatkJs-JdnW80807CcNPWHElsh6MW_JTh7NdbU"
|
||||
},
|
||||
"alg": "ES256"
|
||||
},
|
||||
"signature": "kHbWdD1NQw2RIAQ8uYnKmolU3Z_WeUW8DfRtJRprVDzK7AZaF-ChI4V9Lh74HnjSNwoNZ_QRhUQDl_Nezb0Hgw",
|
||||
"protected": "eyJmb3JtYXRMZW5ndGgiOjY3NzksImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wNS0xN1QxODowMjozMFoifQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
func TestGetManifestDigest(t *testing.T) {
|
||||
// Get the manifest content when the image is not pulled by digest
|
||||
ref, err := reference.ParseNamed(UbuntuTaggedRef)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
digest, err := getManifestDigest([]byte(UbuntuDigestManifest), ref)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, digest, UbuntuDigestSHA)
|
||||
|
||||
// Get and verify the manifest content with the correct digest
|
||||
ref, err = reference.ParseNamed(UbuntuDigest)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
_, ok := ref.(reference.Canonical)
|
||||
assert.True(t, ok)
|
||||
digest, err = getManifestDigest([]byte(UbuntuDigestManifest), ref)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, digest, UbuntuDigestSHA)
|
||||
|
||||
// Attempt to get and verify an incorrect manifest content with the digest
|
||||
digest, err = getManifestDigest([]byte(DefaultManifest), ref)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
388
vendor/github.com/vmware/vic/lib/imagec/download.go
generated
vendored
Normal file
388
vendor/github.com/vmware/vic/lib/imagec/download.go
generated
vendored
Normal file
@@ -0,0 +1,388 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissi[ons and
|
||||
// limitations under the License.
|
||||
|
||||
package imagec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
|
||||
"github.com/docker/docker/distribution/xfer"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
)
|
||||
|
||||
const (
|
||||
maxDownloadAttempts = 5
|
||||
maxConcurrentDownloads = 3
|
||||
)
|
||||
|
||||
type downloadTransfer struct {
|
||||
xfer.Transfer
|
||||
|
||||
layer *ImageWithMeta
|
||||
err error
|
||||
}
|
||||
|
||||
// result returns the layer resulting from the download, if the download
|
||||
// and registration were successful.
|
||||
func (d *downloadTransfer) result() error {
|
||||
return d.err
|
||||
}
|
||||
|
||||
// LayerDownloader keeps track of what layers are currently being downloaded
|
||||
type LayerDownloader struct {
|
||||
m sync.Mutex
|
||||
downloadsByID map[string]*downloadTransfer
|
||||
|
||||
tm xfer.TransferManager
|
||||
}
|
||||
|
||||
// NewLayerDownloader creates and returns a new LayerDownloadManager
|
||||
func NewLayerDownloader() *LayerDownloader {
|
||||
return &LayerDownloader{
|
||||
tm: xfer.NewTransferManager(maxConcurrentDownloads),
|
||||
downloadsByID: make(map[string]*downloadTransfer),
|
||||
}
|
||||
}
|
||||
|
||||
func (ldm *LayerDownloader) registerDownload(download *downloadTransfer) {
|
||||
ldm.downloadsByID[download.layer.ID] = download
|
||||
}
|
||||
|
||||
func (ldm *LayerDownloader) unregisterDownload(layer *ImageWithMeta) {
|
||||
// stop tracking the download transfer
|
||||
delete(ldm.downloadsByID, layer.ID)
|
||||
}
|
||||
|
||||
type prog struct {
|
||||
err chan error
|
||||
p progress.Progress
|
||||
}
|
||||
|
||||
type serialProgressOutput struct {
|
||||
c chan prog
|
||||
out progress.Output
|
||||
}
|
||||
|
||||
func (s *serialProgressOutput) WriteProgress(p progress.Progress) error {
|
||||
pr := prog{err: make(chan error, 1), p: p}
|
||||
s.c <- pr
|
||||
err := <-pr.err
|
||||
close(pr.err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *serialProgressOutput) run() {
|
||||
for pr := range s.c {
|
||||
err := s.out.WriteProgress(pr.p)
|
||||
pr.err <- err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serialProgressOutput) stop() {
|
||||
close(s.c)
|
||||
}
|
||||
|
||||
// DownloadLayers ensures layers end up in the portlayer's image store
|
||||
// It handles existing and simultaneous layer download de-duplication
|
||||
// This code is utilizes Docker's xfer package: https://github.com/docker/docker/tree/v1.11.2/distribution/xfer
|
||||
func (ldm *LayerDownloader) DownloadLayers(ctx context.Context, ic *ImageC) error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
var (
|
||||
topDownload *downloadTransfer
|
||||
watcher *xfer.Watcher
|
||||
d xfer.Transfer
|
||||
layerCount = 0
|
||||
sf = streamformatter.NewJSONStreamFormatter()
|
||||
progressOutput = &serialProgressOutput{
|
||||
c: make(chan prog, 100),
|
||||
out: sf.NewProgressOutput(ic.Outstream, false),
|
||||
}
|
||||
)
|
||||
|
||||
go progressOutput.run()
|
||||
defer progressOutput.stop()
|
||||
|
||||
// lock here so that we get all layers in flight before another client comes along
|
||||
ldm.m.Lock()
|
||||
|
||||
// Grab the imageLayers
|
||||
layers := ic.ImageLayers
|
||||
|
||||
// iterate backwards through layers to download
|
||||
for i := len(layers) - 1; i >= 0; i-- {
|
||||
|
||||
layer := layers[i]
|
||||
id := layer.ID
|
||||
|
||||
layerConfig, err := LayerCache().Get(id)
|
||||
if err != nil {
|
||||
|
||||
switch err := err.(type) {
|
||||
case LayerNotFoundError:
|
||||
|
||||
layerCount++
|
||||
|
||||
// layer does not already exist in store and is not currently in flight, so download it
|
||||
progress.Update(progressOutput, layer.String(), "Pulling fs layer")
|
||||
|
||||
xferFunc := ldm.makeDownloadFunc(layer, ic, topDownload, layers)
|
||||
d, watcher = ldm.tm.Transfer(id, xferFunc, progressOutput)
|
||||
topDownload = d.(*downloadTransfer)
|
||||
|
||||
defer topDownload.Transfer.Release(watcher)
|
||||
|
||||
ldm.registerDownload(topDownload)
|
||||
layer.Downloading = true
|
||||
LayerCache().Add(layer)
|
||||
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if layerConfig.Downloading {
|
||||
|
||||
layerCount++
|
||||
|
||||
if existingDownload, ok := ldm.downloadsByID[id]; ok {
|
||||
|
||||
xferFunc := ldm.makeDownloadFuncFromDownload(layer, existingDownload, topDownload, layers)
|
||||
d, watcher = ldm.tm.Transfer(id, xferFunc, progressOutput)
|
||||
topDownload = d.(*downloadTransfer)
|
||||
|
||||
defer topDownload.Transfer.Release(watcher)
|
||||
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
progress.Update(progressOutput, layer.String(), "Already exists")
|
||||
}
|
||||
|
||||
ldm.m.Unlock()
|
||||
|
||||
// each layer download will block until the parent download finishes,
|
||||
// so this will block until the child-most layer, and thus all layers, have finished downloading
|
||||
if layerCount > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-topDownload.Done():
|
||||
default:
|
||||
<-topDownload.Done()
|
||||
}
|
||||
err := topDownload.result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := UpdateRepoCache(ic); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
progress.Message(progressOutput, "", "Digest: "+ic.ManifestDigest)
|
||||
|
||||
tagOrDigest := tagOrDigest(ic.Reference, ic.Tag)
|
||||
if layerCount > 0 {
|
||||
progress.Message(progressOutput, "", "Status: Downloaded newer image for "+ic.Image+":"+tagOrDigest)
|
||||
} else {
|
||||
progress.Message(progressOutput, "", "Status: Image is up to date for "+ic.Image+":"+tagOrDigest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeDownloadFunc returns a func used by xfer.TransferManager to download a layer
|
||||
func (ldm *LayerDownloader) makeDownloadFunc(layer *ImageWithMeta, ic *ImageC, parentDownload *downloadTransfer, layers []*ImageWithMeta) xfer.DoFunc {
|
||||
return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) xfer.Transfer {
|
||||
|
||||
d := &downloadTransfer{
|
||||
Transfer: xfer.NewTransfer(),
|
||||
layer: layer,
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
||||
defer func() {
|
||||
close(progressChan)
|
||||
|
||||
// remove layer from cache if there was an error attempting to download
|
||||
if d.err != nil {
|
||||
LayerCache().Remove(layer.ID)
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
progressOutput := progress.ChanOutput(progressChan)
|
||||
|
||||
// wait for TransferManager to give the go-ahead
|
||||
select {
|
||||
case <-start:
|
||||
default:
|
||||
progress.Update(progressOutput, layer.String(), "Waiting")
|
||||
<-start
|
||||
}
|
||||
|
||||
if parentDownload != nil {
|
||||
// bail if parent download failed or was cancelled
|
||||
select {
|
||||
case <-parentDownload.Done():
|
||||
if err := parentDownload.result(); err != nil {
|
||||
d.err = err
|
||||
return
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// fetch blob
|
||||
diffID, err := FetchImageBlob(d.Transfer.Context(), ic.Options, layer, progressOutput)
|
||||
if err != nil {
|
||||
d.err = fmt.Errorf("%s/%s returned %s", ic.Image, layer.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
layer.DiffID = diffID
|
||||
|
||||
close(inactive)
|
||||
|
||||
if parentDownload != nil {
|
||||
select {
|
||||
case <-d.Transfer.Context().Done():
|
||||
d.err = errors.New("layer download cancelled")
|
||||
return
|
||||
default:
|
||||
<-parentDownload.Done() // block until parent download completes
|
||||
}
|
||||
|
||||
if err := parentDownload.result(); err != nil {
|
||||
d.err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// is this the leaf layer?
|
||||
imageLayer := layer.ID == layers[0].ID
|
||||
|
||||
if !ic.Standalone {
|
||||
// if this is the leaf layer, we are done and can now create the image config
|
||||
if imageLayer {
|
||||
imageConfig, err := ic.CreateImageConfig(layers)
|
||||
if err != nil {
|
||||
d.err = err
|
||||
return
|
||||
}
|
||||
// cache and persist the image
|
||||
cache.ImageCache().Add(&imageConfig)
|
||||
if err := cache.ImageCache().Save(); err != nil {
|
||||
d.err = fmt.Errorf("error saving image cache: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// place calculated ImageID in struct
|
||||
ic.ImageID = imageConfig.ImageID
|
||||
|
||||
if err = UpdateRepoCache(ic); err != nil {
|
||||
d.err = err
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ldm.m.Lock()
|
||||
defer ldm.m.Unlock()
|
||||
|
||||
// Write blob to the storage layer
|
||||
if err := ic.WriteImageBlob(layer, progressOutput, imageLayer); err != nil {
|
||||
d.err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !ic.Standalone {
|
||||
// mark the layer as finished downloading
|
||||
LayerCache().Commit(layer)
|
||||
}
|
||||
|
||||
ldm.unregisterDownload(layer)
|
||||
|
||||
}()
|
||||
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
// makeDownloadFuncFromDownload returns a func used by the TransferManager to
|
||||
// handle a layer that was already seen in this image pull, or is currently being downloaded
|
||||
func (ldm *LayerDownloader) makeDownloadFuncFromDownload(layer *ImageWithMeta, sourceDownload, parentDownload *downloadTransfer, layers []*ImageWithMeta) xfer.DoFunc {
|
||||
|
||||
return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) xfer.Transfer {
|
||||
|
||||
d := &downloadTransfer{
|
||||
Transfer: xfer.NewTransfer(),
|
||||
layer: layer,
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(progressChan)
|
||||
|
||||
<-start
|
||||
|
||||
close(inactive)
|
||||
|
||||
if parentDownload != nil {
|
||||
select {
|
||||
case <-d.Transfer.Context().Done():
|
||||
d.err = errors.New("layer download cancelled")
|
||||
return
|
||||
case <-parentDownload.Done(): // wait for parent layer download to complete
|
||||
}
|
||||
|
||||
if err := parentDownload.result(); err != nil {
|
||||
d.err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// sourceDownload should have already finished if
|
||||
// parentDownload finished, but wait for it explicitly
|
||||
// to be sure.
|
||||
select {
|
||||
case <-d.Transfer.Context().Done():
|
||||
d.err = errors.New("layer download cancelled")
|
||||
return
|
||||
case <-sourceDownload.Done():
|
||||
}
|
||||
|
||||
if err := sourceDownload.result(); err != nil {
|
||||
d.err = err
|
||||
return
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
}
|
||||
648
vendor/github.com/vmware/vic/lib/imagec/imagec.go
generated
vendored
Normal file
648
vendor/github.com/vmware/vic/lib/imagec/imagec.go
generated
vendored
Normal file
@@ -0,0 +1,648 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package imagec
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
docker "github.com/docker/docker/image"
|
||||
dockerLayer "github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/reference"
|
||||
|
||||
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
|
||||
"github.com/vmware/vic/lib/apiservers/portlayer/models"
|
||||
"github.com/vmware/vic/lib/constants"
|
||||
"github.com/vmware/vic/lib/metadata"
|
||||
urlfetcher "github.com/vmware/vic/pkg/fetcher"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/sys"
|
||||
)
|
||||
|
||||
// ImageC is responsible for pulling docker images from a repository
|
||||
type ImageC struct {
|
||||
Options
|
||||
|
||||
// https://raw.githubusercontent.com/docker/docker/master/distribution/pull_v2.go
|
||||
sf *streamformatter.StreamFormatter
|
||||
progressOutput progress.Output
|
||||
|
||||
// ImageLayers are sourced from the manifest file
|
||||
ImageLayers []*ImageWithMeta
|
||||
|
||||
// ImageID is the docker ImageID calculated during download
|
||||
ImageID string
|
||||
}
|
||||
|
||||
// NewImageC returns a new instance of ImageC
|
||||
func NewImageC(options Options, strfmtr *streamformatter.StreamFormatter) *ImageC {
|
||||
return &ImageC{
|
||||
Options: options,
|
||||
sf: strfmtr,
|
||||
progressOutput: strfmtr.NewProgressOutput(options.Outstream, false),
|
||||
}
|
||||
}
|
||||
|
||||
// Options contain all options for a single instance of imagec
|
||||
type Options struct {
|
||||
Reference reference.Named
|
||||
|
||||
Registry string
|
||||
Image string
|
||||
Tag string
|
||||
|
||||
Destination string
|
||||
|
||||
Host string
|
||||
Storename string
|
||||
|
||||
Username string
|
||||
Password string
|
||||
|
||||
Token *urlfetcher.Token
|
||||
|
||||
Timeout time.Duration
|
||||
|
||||
Outstream io.Writer
|
||||
|
||||
InsecureSkipVerify bool
|
||||
InsecureAllowHTTP bool
|
||||
|
||||
// Get both schema 1 and schema 2 manifests. Schema 1 is used to get history
|
||||
// and was imageC implementation predated schema 2. Schema 2 is used to
|
||||
// calculate digest.
|
||||
ImageManifestSchema1 *Manifest
|
||||
ImageManifestSchema2 *schema2.DeserializedManifest
|
||||
|
||||
//Digest of manifest schema 2 or schema 1; therefore, it may not match hash
|
||||
//of the above manifest.
|
||||
ManifestDigest string
|
||||
|
||||
// RegistryCAs will not be modified by imagec
|
||||
RegistryCAs *x509.CertPool
|
||||
|
||||
// If true, do not bother portlayer or persona
|
||||
Standalone bool
|
||||
|
||||
// image store name or url
|
||||
ImageStore string
|
||||
}
|
||||
|
||||
// ImageWithMeta wraps the models.Image with some additional metadata
|
||||
type ImageWithMeta struct {
|
||||
*models.Image
|
||||
|
||||
DiffID string
|
||||
Layer FSLayer
|
||||
Meta string
|
||||
Size int64
|
||||
|
||||
Downloading bool
|
||||
}
|
||||
|
||||
func (i *ImageWithMeta) String() string {
|
||||
return stringid.TruncateID(i.Layer.BlobSum)
|
||||
}
|
||||
|
||||
var (
|
||||
ldm *LayerDownloader
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultDockerURL holds the URL of Docker registry
|
||||
DefaultDockerURL = "registry.hub.docker.com"
|
||||
|
||||
// DefaultDestination specifies the default directory to use
|
||||
DefaultDestination = "images"
|
||||
|
||||
// DefaultPortLayerHost specifies the default port layer server
|
||||
DefaultPortLayerHost = "localhost:2377"
|
||||
|
||||
// DefaultLogfile specifies the default log file name
|
||||
DefaultLogfile = "imagec.log"
|
||||
|
||||
// DefaultHTTPTimeout specifies the default HTTP timeout
|
||||
DefaultHTTPTimeout = 3600 * time.Second
|
||||
|
||||
// attribute update actions
|
||||
Add = iota + 1
|
||||
)
|
||||
|
||||
func init() {
|
||||
ldm = NewLayerDownloader()
|
||||
}
|
||||
|
||||
// ParseReference parses the -reference parameter and populate options struct
|
||||
func (ic *ImageC) ParseReference() {
|
||||
if reference.IsNameOnly(ic.Reference) {
|
||||
ic.Tag = reference.DefaultTag
|
||||
} else {
|
||||
if tagged, isTagged := ic.Reference.(reference.NamedTagged); isTagged {
|
||||
ic.Tag = tagged.Tag()
|
||||
}
|
||||
}
|
||||
|
||||
ic.Registry = ic.Reference.Hostname()
|
||||
if ic.Registry == reference.DefaultHostname {
|
||||
ic.Registry = DefaultDockerURL
|
||||
}
|
||||
|
||||
ic.Image = ic.Reference.RemoteName()
|
||||
}
|
||||
|
||||
// DestinationDirectory returns the path of the output directory
|
||||
func DestinationDirectory(options Options) string {
|
||||
// #nosec: Errors unhandled.
|
||||
u, _ := url.Parse(options.Registry)
|
||||
|
||||
// Use a hierarchy like following so that we can support multiple schemes, registries and versions
|
||||
/*
|
||||
https/
|
||||
├── 192.168.218.5:5000
|
||||
│ └── v2
|
||||
│ └── busybox
|
||||
│ └── latest
|
||||
...
|
||||
│ ├── fef924a0204a00b3ec67318e2ed337b189c99ea19e2bf10ed30a13b87c5e17ab
|
||||
│ │ ├── fef924a0204a00b3ec67318e2ed337b189c99ea19e2bf10ed30a13b87c5e17ab.json
|
||||
│ │ └── fef924a0204a00b3ec67318e2ed337b189c99ea19e2bf10ed30a13b87c5e17ab.tar
|
||||
│ └── manifest.json
|
||||
└── registry-1.docker.io
|
||||
└── v2
|
||||
└── library
|
||||
└── golang
|
||||
└── latest
|
||||
...
|
||||
├── f61ebe2817bb4e6a7f0a4cf249a5316223f7ecc886feac24b9887a490feaed57
|
||||
│ ├── f61ebe2817bb4e6a7f0a4cf249a5316223f7ecc886feac24b9887a490feaed57.json
|
||||
│ └── f61ebe2817bb4e6a7f0a4cf249a5316223f7ecc886feac24b9887a490feaed57.tar
|
||||
└── manifest.json
|
||||
|
||||
*/
|
||||
if u.Scheme == "" && u.Host == "" && u.Path == "" {
|
||||
return path.Join(
|
||||
options.Destination,
|
||||
"localhost",
|
||||
options.Image,
|
||||
options.Tag,
|
||||
)
|
||||
}
|
||||
return path.Join(
|
||||
options.Destination,
|
||||
u.Scheme,
|
||||
u.Host,
|
||||
u.Path,
|
||||
options.Image,
|
||||
options.Tag,
|
||||
)
|
||||
}
|
||||
|
||||
// LayersToDownload creates a slice of ImageWithMeta for the layers that need to be downloaded
|
||||
func (ic *ImageC) LayersToDownload() ([]*ImageWithMeta, error) {
|
||||
images := make([]*ImageWithMeta, len(ic.ImageManifestSchema1.FSLayers))
|
||||
|
||||
manifest := ic.ImageManifestSchema1
|
||||
v1 := docker.V1Image{}
|
||||
|
||||
// iterate from parent to children
|
||||
for i := len(ic.ImageManifestSchema1.History) - 1; i >= 0; i-- {
|
||||
history := manifest.History[i]
|
||||
layer := manifest.FSLayers[i]
|
||||
|
||||
// unmarshall V1Compatibility to get the image ID
|
||||
if err := json.Unmarshal([]byte(history.V1Compatibility), &v1); err != nil {
|
||||
return nil, fmt.Errorf("Failed to unmarshall image history: %s", err)
|
||||
}
|
||||
|
||||
// if parent is empty set it to scratch
|
||||
parent := constants.ScratchLayerID
|
||||
if v1.Parent != "" {
|
||||
parent = v1.Parent
|
||||
}
|
||||
|
||||
// add image to ImageWithMeta list
|
||||
images[i] = &ImageWithMeta{
|
||||
Image: &models.Image{
|
||||
ID: v1.ID,
|
||||
Parent: parent,
|
||||
Store: ic.Storename,
|
||||
},
|
||||
Meta: history.V1Compatibility,
|
||||
Layer: layer,
|
||||
}
|
||||
|
||||
// populate manifest layer with existing cached data
|
||||
if layer, err := LayerCache().Get(images[i].ID); err == nil {
|
||||
if !layer.Downloading { // possibly unnecessary but won't hurt anything
|
||||
images[i] = layer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// UpdateRepositoryCache will update the repository cache
|
||||
// that resides in the docker persona. This will add image tag,
|
||||
// digest and layer information.
|
||||
func UpdateRepoCache(ic *ImageC) error {
|
||||
// if standalone then no persona, so exit
|
||||
if ic.Standalone {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LayerID for the image layer
|
||||
imageLayerID := ic.ImageLayers[0].ID
|
||||
|
||||
// get the repoCache
|
||||
repoCache := cache.RepositoryCache()
|
||||
|
||||
// In the case that we don't have the ImageID, then we need
|
||||
// to go to the RepositoryCache to get it.
|
||||
if ic.ImageID == "" {
|
||||
// call to repository cache for the imageID for this layer
|
||||
ic.ImageID = repoCache.GetImageID(imageLayerID)
|
||||
|
||||
// if we still don't have an imageID we can't continue
|
||||
if ic.ImageID == "" {
|
||||
return fmt.Errorf("ImageID not found by LayerID(%s) in RepositoryCache", imageLayerID)
|
||||
}
|
||||
}
|
||||
// AddReference will add the repo:tag to the repositoryCache and save to the portLayer
|
||||
err := repoCache.AddReference(ic.Reference, ic.ImageID, true, imageLayerID, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to Add Image Reference(%s): %s", ic.Reference.String(), err.Error())
|
||||
}
|
||||
|
||||
dig, err := reference.ParseNamed(fmt.Sprintf("%s@%s", ic.Reference.Name(), ic.ManifestDigest))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse digest: %s", err.Error())
|
||||
}
|
||||
|
||||
// AddReference will add the digest and persist to the portLayer
|
||||
err = repoCache.AddReference(dig, ic.ImageID, true, imageLayerID, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to Add Image Digest(%s): %s", dig.String(), err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteImageBlob writes the image blob to the storage layer
|
||||
func (ic *ImageC) WriteImageBlob(image *ImageWithMeta, progressOutput progress.Output, cleanup bool) error {
|
||||
defer trace.End(trace.Begin(image.Image.ID))
|
||||
|
||||
destination := DestinationDirectory(ic.Options)
|
||||
|
||||
id := image.Image.ID
|
||||
filePath := path.Join(destination, id, id+".tar")
|
||||
log.Infof("Path: %s", filePath)
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to stat file: %s", err)
|
||||
}
|
||||
|
||||
in := progress.NewProgressReader(
|
||||
ioutils.NewCancelReadCloser(context.Background(), f),
|
||||
progressOutput,
|
||||
fi.Size(),
|
||||
image.String(),
|
||||
"Extracting",
|
||||
)
|
||||
defer in.Close()
|
||||
|
||||
if !ic.Standalone {
|
||||
// Write the image
|
||||
err = WriteImage(ic.Host, image, in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to write to image store: %s", err)
|
||||
}
|
||||
} else {
|
||||
// If standalone, write to a local directory
|
||||
cleanup = false
|
||||
}
|
||||
|
||||
progress.Update(progressOutput, image.String(), "Pull complete")
|
||||
|
||||
if cleanup {
|
||||
if err := os.RemoveAll(destination); err != nil {
|
||||
return fmt.Errorf("Failed to remove download directory: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateImageConfig constructs the image metadata from layers that compose the image
|
||||
func (ic *ImageC) CreateImageConfig(images []*ImageWithMeta) (metadata.ImageConfig, error) {
|
||||
|
||||
imageLayer := images[0] // the layer that represents the actual image
|
||||
|
||||
// if we already have an imageID associated with this layerID, we don't need
|
||||
// to calculate imageID and can just grab the image config from the cache
|
||||
id := cache.RepositoryCache().GetImageID(imageLayer.ID)
|
||||
if image, err := cache.ImageCache().Get(id); err == nil {
|
||||
return *image, nil
|
||||
}
|
||||
|
||||
manifest := ic.ImageManifestSchema1
|
||||
image := docker.V1Image{}
|
||||
rootFS := docker.NewRootFS()
|
||||
history := make([]docker.History, 0, len(images))
|
||||
diffIDs := make(map[string]string)
|
||||
var size int64
|
||||
|
||||
// step through layers to get command history and diffID from oldest to newest
|
||||
for i := len(images) - 1; i >= 0; i-- {
|
||||
layer := images[i]
|
||||
if err := json.Unmarshal([]byte(layer.Meta), &image); err != nil {
|
||||
return metadata.ImageConfig{}, fmt.Errorf("Failed to unmarshall layer history: %s", err)
|
||||
}
|
||||
h := docker.History{
|
||||
Created: image.Created,
|
||||
Author: image.Author,
|
||||
CreatedBy: strings.Join(image.ContainerConfig.Cmd, " "),
|
||||
Comment: image.Comment,
|
||||
}
|
||||
|
||||
// is this an empty layer?
|
||||
if layer.DiffID == dockerLayer.DigestSHA256EmptyTar.String() {
|
||||
h.EmptyLayer = true
|
||||
} else {
|
||||
// if not empty, add diffID to rootFS
|
||||
rootFS.DiffIDs = append(rootFS.DiffIDs, dockerLayer.DiffID(layer.DiffID))
|
||||
}
|
||||
history = append(history, h)
|
||||
size += layer.Size
|
||||
}
|
||||
|
||||
// result is constructed without unused fields
|
||||
result := docker.Image{
|
||||
V1Image: docker.V1Image{
|
||||
Comment: image.Comment,
|
||||
Created: image.Created,
|
||||
Container: image.Container,
|
||||
ContainerConfig: image.ContainerConfig,
|
||||
DockerVersion: image.DockerVersion,
|
||||
Author: image.Author,
|
||||
Config: image.Config,
|
||||
Architecture: image.Architecture,
|
||||
OS: image.OS,
|
||||
},
|
||||
RootFS: rootFS,
|
||||
History: history,
|
||||
}
|
||||
|
||||
imageConfigBytes, err := result.MarshalJSON()
|
||||
if err != nil {
|
||||
return metadata.ImageConfig{}, fmt.Errorf("Failed to marshall image metadata: %s", err)
|
||||
}
|
||||
|
||||
// calculate image ID
|
||||
sum := fmt.Sprintf("%x", sha256.Sum256(imageConfigBytes))
|
||||
log.Infof("Image ID: sha256:%s", sum)
|
||||
|
||||
// prepare metadata
|
||||
result.V1Image.Parent = image.Parent
|
||||
result.Size = size
|
||||
result.V1Image.ID = imageLayer.ID
|
||||
imageConfig := metadata.ImageConfig{
|
||||
V1Image: result.V1Image,
|
||||
ImageID: sum,
|
||||
DiffIDs: diffIDs,
|
||||
History: history,
|
||||
}
|
||||
|
||||
if ic.Tag != "" {
|
||||
imageConfig.Tags = []string{ic.Tag}
|
||||
}
|
||||
if manifest != nil {
|
||||
imageConfig.Name = manifest.Name
|
||||
}
|
||||
if ic.Reference != nil {
|
||||
imageConfig.Reference = ic.Reference.String()
|
||||
}
|
||||
if _, ok := ic.Reference.(reference.Canonical); ok {
|
||||
log.Debugf("Populating digest in imageConfig for image: %s", ic.Reference.String())
|
||||
imageConfig.Digests = []string{ic.ManifestDigest}
|
||||
}
|
||||
|
||||
return imageConfig, nil
|
||||
}
|
||||
|
||||
// PullImage pulls an image from docker hub
|
||||
func (ic *ImageC) PullImage() error {
|
||||
ctx, cancel := context.WithTimeout(ctx, ic.Options.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// Authenticate, get URL, get token
|
||||
if err := ic.prepareTransfer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Output message
|
||||
tagOrDigest := tagOrDigest(ic.Reference, ic.Tag)
|
||||
progress.Message(ic.progressOutput, "", tagOrDigest+": Pulling from "+ic.Image)
|
||||
|
||||
// Pull the image manifest
|
||||
if err := ic.pullManifest(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Manifest for image = %#v", ic.ImageManifestSchema1)
|
||||
|
||||
// Get layers to download from manifest
|
||||
layers, err := ic.LayersToDownload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ic.ImageLayers = layers
|
||||
|
||||
// Download all the layers
|
||||
if err := ldm.DownloadLayers(ctx, ic); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListLayer prints out the layers for an image to progress. This is used by imagec standalone binary
|
||||
// for debug/validation.
|
||||
func (ic *ImageC) ListLayers() error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, ic.Options.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// Authenticate, get URL, get token
|
||||
if err := ic.prepareTransfer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Output message
|
||||
tagOrDigest := tagOrDigest(ic.Reference, ic.Tag)
|
||||
progress.Message(ic.progressOutput, "", tagOrDigest+": Fetching layers from "+ic.Image)
|
||||
|
||||
// Pull the image manifest
|
||||
if err := ic.pullManifest(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get layers to download from manifest
|
||||
layers, err := ic.LayersToDownload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progress.Message(ic.progressOutput, "", constants.ScratchLayerID)
|
||||
for i := len(layers) - 1; i >= 0; i-- {
|
||||
progress.Message(ic.progressOutput, "", layers[i].ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareTransfer Looks up URLs and fetch auth token
|
||||
func (ic *ImageC) prepareTransfer(ctx context.Context) error {
|
||||
|
||||
// Parse the -reference parameter
|
||||
ic.ParseReference()
|
||||
|
||||
// Host is either the host's UUID (if run on vsphere) or the hostname of
|
||||
// the system (if run standalone)
|
||||
host, err := sys.UUID()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to return host name: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if host != "" {
|
||||
log.Infof("Using UUID (%s) for imagestore name", host)
|
||||
} else if ic.Standalone {
|
||||
host, err = os.Hostname()
|
||||
log.Infof("Using host (%s) for imagestore name", host)
|
||||
}
|
||||
|
||||
ic.Storename = host
|
||||
|
||||
if !ic.Standalone {
|
||||
log.Debugf("Running with portlayer")
|
||||
|
||||
// Ping the server to ensure it's at least running
|
||||
ok, err := PingPortLayer(ic.Host)
|
||||
if err != nil || !ok {
|
||||
log.Errorf("Failed to ping portlayer: %s", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Running standalone")
|
||||
}
|
||||
|
||||
// Calculate (and overwrite) the registry URL and make sure that it responds to requests
|
||||
ic.Registry, err = LearnRegistryURL(&ic.Options)
|
||||
if err != nil {
|
||||
log.Errorf("Error while pulling image: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the URL of the OAuth endpoint
|
||||
url, err := LearnAuthURL(ic.Options)
|
||||
if err != nil {
|
||||
log.Warnf("LearnAuthURL returned %s", err.Error())
|
||||
switch err := err.(type) {
|
||||
case urlfetcher.ImageNotFoundError:
|
||||
return fmt.Errorf("Error: image %s not found", ic.Reference)
|
||||
default:
|
||||
return fmt.Errorf("Failed to obtain OAuth endpoint: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the OAuth token - if only we have a URL
|
||||
if url != nil {
|
||||
token, err := FetchToken(ctx, ic.Options, url, ic.progressOutput)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to fetch OAuth token: %s", err)
|
||||
return err
|
||||
}
|
||||
ic.Token = token
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// pullManifest attempts to pull manifest for an image. Attempts to get schema 2 but will fall back to schema 1.
|
||||
func (ic *ImageC) pullManifest(ctx context.Context) error {
|
||||
// Get the schema1 manifest
|
||||
manifest, digest, err := FetchImageManifest(ctx, ic.Options, 1, ic.progressOutput)
|
||||
if err != nil {
|
||||
log.Infof(err.Error())
|
||||
switch err := err.(type) {
|
||||
case urlfetcher.ImageNotFoundError:
|
||||
return fmt.Errorf("Error: image %s not found", ic.Image)
|
||||
case urlfetcher.TagNotFoundError:
|
||||
return fmt.Errorf("Tag %s not found in repository %s", ic.Tag, ic.Image)
|
||||
default:
|
||||
return fmt.Errorf("Error while pulling image manifest: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
schema1, ok := manifest.(*Manifest)
|
||||
if !ok {
|
||||
return fmt.Errorf("Error pulling manifest schema 1")
|
||||
}
|
||||
|
||||
ic.ImageManifestSchema1 = schema1
|
||||
ic.ManifestDigest = digest
|
||||
|
||||
// Attempt to get schema2 manifest
|
||||
manifest, digest, err = FetchImageManifest(ctx, ic.Options, 2, ic.progressOutput)
|
||||
if err == nil {
|
||||
if schema2, ok := manifest.(*schema2.DeserializedManifest); ok {
|
||||
if schema2 != nil {
|
||||
log.Infof("pullManifest - schema 2: %#v", schema2)
|
||||
}
|
||||
ic.ImageManifestSchema2 = schema2
|
||||
|
||||
// Override the manifest digest as Docker uses schema 2, unless the image
|
||||
// is pulled by digest since we only support pull-by-digest for schema 1.
|
||||
// TODO(anchal): this check should be removed once issue #5187 is implemented.
|
||||
if _, ok := ic.Reference.(reference.Canonical); !ok {
|
||||
ic.ManifestDigest = digest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
616
vendor/github.com/vmware/vic/lib/imagec/imagec_test.go
generated
vendored
Normal file
616
vendor/github.com/vmware/vic/lib/imagec/imagec_test.go
generated
vendored
Normal file
@@ -0,0 +1,616 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package imagec
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/vic/lib/apiservers/portlayer/models"
|
||||
urlfetcher "github.com/vmware/vic/pkg/fetcher"
|
||||
)
|
||||
|
||||
const (
|
||||
OAuthToken = "Top_Secret_Token"
|
||||
Image = "library/photon"
|
||||
Tag = "latest"
|
||||
Reference = Image + ":" + Tag
|
||||
|
||||
BusyboxImage = "library/busybox"
|
||||
|
||||
// fake content
|
||||
LayerContent = "Cannot_Contain_Myself"
|
||||
// fake ID
|
||||
LayerID = "f9767cae14f372c98900f15bb07cb40b8e1a6d1507912489e1342db499313d32"
|
||||
// fake history
|
||||
LayerHistory = "{\"id\":\"f9767cae14f372c98900f15bb07cb40b8e1a6d1507912489e1342db499313d32\"" + "," +
|
||||
"\"parent\":\"09a5baea69e9c781d64df5366c36492d53d507048035abd68632264dc23a1edb\"}"
|
||||
// fake store
|
||||
Storename = "PetStore"
|
||||
|
||||
// sha256 sum of LayerContent
|
||||
DigestSHA256LayerContent = "sha256:1f1f9635040c465c7f7b32a396e56e26c1396dbadfbed744b0aab9337a24ad5a"
|
||||
|
||||
//DigestSHA256EmptyData is the canonical sha256 digest of empty data
|
||||
DigestSHA256EmptyData = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
|
||||
DefaultManifest = `
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "library/photon",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"156e10b83429\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"sh\"],\"Image\":\"56ed16bd6310cca65920c653a9bb22de6b235990dcaa1742ff839867aed730e5\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"container\":\"5f8098ec29947b5bea80483cd3275008911ce87438fed628e34ec0c522665510\",\"container_config\":{\"Hostname\":\"156e10b83429\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"sh\\\"]\"],\"Image\":\"56ed16bd6310cca65920c653a9bb22de6b235990dcaa1742ff839867aed730e5\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"created\":\"2016-03-18T18:22:48.810791943Z\",\"docker_version\":\"1.9.1\",\"id\":\"437595becdebaaaf3a4fc3db02c59a980f955dee825c153308c670610bb694e1\",\"os\":\"linux\",\"parent\":\"920777304d1d5e337bc59877253e946f224df5aae64c72538672eb74637b3c9e\"}"
|
||||
}
|
||||
],
|
||||
"signatures": [
|
||||
{
|
||||
"header": {
|
||||
"jwk": {
|
||||
"crv": "P-256",
|
||||
"kid": "LUQI:WBTB:JRDU:TTD2:FUVY:EMCB:64HP:MZF6:SGFS:XAB6:JPUK:6PK4",
|
||||
"kty": "EC",
|
||||
"x": "zjkAuFGpCuWOBl-iMMzZqgl_1cid-04S04-k-A1qEeU",
|
||||
"y": "9HWcOMfVFUMXJGeNajIAlPicL4UOsCJSpqRcIxpUl0Q"
|
||||
},
|
||||
"alg": "ES256"
|
||||
},
|
||||
"signature": "dTXnnt3IkTScpZhyyqRlmZFcQV1QzD7lWDqnjlD4Cj-KsMuGd1pl5QpFL2Cadw-8KeTBlSleSecxjHU4t3yhCQ",
|
||||
"protected": "eyJmb3JtYXRMZW5ndGgiOjE5MjIsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNi0wOVQxNzoyMzo1NFoifQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
func TestParseReference(t *testing.T) {
|
||||
|
||||
options := Options{
|
||||
Outstream: os.Stdout,
|
||||
}
|
||||
|
||||
ic := NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
|
||||
ref, err := reference.ParseNamed("index.docker.io/library/busybox")
|
||||
ic.Options.Reference = ref
|
||||
ic.ParseReference()
|
||||
assert.Equal(t, ic.Tag, reference.DefaultTag)
|
||||
assert.Equal(t, ic.Image, BusyboxImage)
|
||||
assert.Equal(t, ic.Registry, DefaultDockerURL)
|
||||
|
||||
ref, err = reference.ParseNamed("vmware/photon")
|
||||
ic.Options.Reference = ref
|
||||
ic.ParseReference()
|
||||
assert.Equal(t, ic.Tag, reference.DefaultTag)
|
||||
assert.Equal(t, ic.Image, "vmware/photon")
|
||||
assert.Equal(t, ic.Registry, DefaultDockerURL)
|
||||
|
||||
ref, err = reference.ParseNamed("busybox")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
ic.Options.Reference = ref
|
||||
ic.ParseReference()
|
||||
assert.Equal(t, ic.Tag, reference.DefaultTag)
|
||||
assert.Equal(t, ic.Image, BusyboxImage)
|
||||
assert.Equal(t, ic.Registry, DefaultDockerURL)
|
||||
|
||||
ref, err = reference.ParseNamed("library/busybox")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
ic.Options.Reference = ref
|
||||
ic.ParseReference()
|
||||
assert.Equal(t, ic.Tag, reference.DefaultTag)
|
||||
assert.Equal(t, ic.Image, BusyboxImage)
|
||||
assert.Equal(t, ic.Registry, DefaultDockerURL)
|
||||
|
||||
ref, err = reference.ParseNamed("library/busybox:latest")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
ic.Options.Reference = ref
|
||||
ic.ParseReference()
|
||||
assert.Equal(t, ic.Tag, reference.DefaultTag)
|
||||
assert.Equal(t, ic.Image, BusyboxImage)
|
||||
assert.Equal(t, ic.Registry, DefaultDockerURL)
|
||||
|
||||
ic = NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
ref, err = reference.ParseNamed("busybox")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
digest, err := digest.ParseDigest("sha256:c79345819a6882c31b41bc771d9a94fc52872fa651b36771fbe0c8461d7ee558")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
ref, err = reference.WithDigest(reference.TrimNamed(ref), digest)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
ic.Options.Reference = ref
|
||||
ic.ParseReference()
|
||||
assert.Equal(t, ic.Tag, "")
|
||||
assert.Equal(t, ic.Image, BusyboxImage)
|
||||
assert.Equal(t, ic.Registry, DefaultDockerURL)
|
||||
}
|
||||
|
||||
func TestLearnRegistryURL(t *testing.T) {
|
||||
|
||||
options := Options{
|
||||
Outstream: os.Stdout,
|
||||
}
|
||||
|
||||
ic := NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
|
||||
s := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
|
||||
http.Error(w, "You shall not pass", http.StatusUnauthorized)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ic.Options.Registry = s.URL[7:]
|
||||
ic.Options.Image = Image
|
||||
ic.Options.Tag = Tag
|
||||
ic.Options.Timeout = DefaultHTTPTimeout
|
||||
|
||||
// should fail
|
||||
_, err := LearnRegistryURL(&ic.Options)
|
||||
if err == nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
// should pass
|
||||
ic.Options.InsecureAllowHTTP = true
|
||||
_, err = LearnRegistryURL(&ic.Options)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLearnAuthURL(t *testing.T) {
|
||||
var err error
|
||||
|
||||
options := Options{
|
||||
Outstream: os.Stdout,
|
||||
}
|
||||
|
||||
ic := NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
|
||||
s := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("www-authenticate",
|
||||
"Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\",scope=\"repository:library/photon:pull\"")
|
||||
http.Error(w, "You shall not pass", http.StatusUnauthorized)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ic.Options.Registry = s.URL
|
||||
ic.Options.Image = Image
|
||||
ic.Options.Tag = Tag
|
||||
ic.Options.Timeout = DefaultHTTPTimeout
|
||||
ic.Options.Reference, err = reference.ParseNamed(Reference)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
url, err := LearnAuthURL(ic.Options)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
if url.String() != "https://auth.docker.io/token?scope=repository%3Alibrary%2Fphoton%3Apull&service=registry.docker.io" {
|
||||
t.Errorf("Returned url %s is different than expected", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchToken(t *testing.T) {
|
||||
|
||||
options := Options{
|
||||
Outstream: os.Stdout,
|
||||
Timeout: DefaultHTTPTimeout,
|
||||
}
|
||||
|
||||
ic := NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
|
||||
s := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
body, err := json.Marshal(&urlfetcher.Token{Token: OAuthToken})
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
w.Write(body)
|
||||
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
url, err := url.Parse(s.URL)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
url.Path = path.Join(url.Path, "token?scope=repository%3Alibrary%2Fphoton%3Apull&service=registry.docker.io")
|
||||
|
||||
ctx := context.TODO()
|
||||
token, err := FetchToken(ctx, ic.Options, url, ic.progressOutput)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
if token.Token != OAuthToken {
|
||||
t.Errorf("Returned token %s is different than expected", token.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchImageManifest(t *testing.T) {
|
||||
var err error
|
||||
|
||||
options := Options{
|
||||
Outstream: os.Stdout,
|
||||
}
|
||||
|
||||
ic := NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
|
||||
s := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.Write([]byte(DefaultManifest))
|
||||
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ic.Options.Registry = s.URL
|
||||
ic.Options.Image = Image
|
||||
ic.Options.Tag = Tag
|
||||
ic.Options.Timeout = DefaultHTTPTimeout
|
||||
ic.Options.Token = &urlfetcher.Token{Token: OAuthToken}
|
||||
ic.Options.Reference, err = reference.ParseNamed(Reference)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
// create a temporary directory
|
||||
dir, err := ioutil.TempDir("", "imagec")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ic.Options.Destination = dir
|
||||
|
||||
ctx := context.TODO()
|
||||
manifest, _, err := FetchImageManifest(ctx, ic.Options, 1, ic.progressOutput)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
if schema1, ok := manifest.(*Manifest); ok {
|
||||
if schema1.FSLayers[0].BlobSum != DigestSHA256EmptyData {
|
||||
t.Errorf("Returned manifest %#v is different than expected", manifest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchImageBlob(t *testing.T) {
|
||||
|
||||
options := Options{
|
||||
Outstream: os.Stdout,
|
||||
}
|
||||
|
||||
ic := NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
|
||||
// create a tar archive from our dummy data
|
||||
r := strings.NewReader(LayerContent)
|
||||
buf := new(bytes.Buffer)
|
||||
tw := tar.NewWriter(buf)
|
||||
defer tw.Close()
|
||||
|
||||
// create and populate tar header
|
||||
header := new(tar.Header)
|
||||
header.Size = r.Size()
|
||||
header.Name = LayerID
|
||||
header.Mode = 0644
|
||||
|
||||
// write the header
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
// write the file into the tar archive
|
||||
if _, err := io.Copy(tw, r); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
attempt := 3
|
||||
s := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
attempt--
|
||||
if attempt == 0 {
|
||||
w.Header().Set("Content-Type", "application/x-gzip")
|
||||
// return our tar archive
|
||||
w.Write(buf.Bytes())
|
||||
} else {
|
||||
// return an error to force caller to retry
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ic.Options.Registry = s.URL
|
||||
ic.Options.Image = Image
|
||||
ic.Options.Tag = Tag
|
||||
ic.Options.Timeout = DefaultHTTPTimeout
|
||||
ic.Options.Token = &urlfetcher.Token{Token: OAuthToken}
|
||||
|
||||
// create a temporary directory
|
||||
dir, err := ioutil.TempDir("", "imagec")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ic.Options.Destination = dir
|
||||
|
||||
parent := "scratch"
|
||||
image := ImageWithMeta{
|
||||
Image: &models.Image{
|
||||
ID: LayerID,
|
||||
Parent: parent,
|
||||
Store: Storename,
|
||||
},
|
||||
Meta: LayerHistory,
|
||||
Layer: FSLayer{BlobSum: DigestSHA256LayerContent},
|
||||
}
|
||||
diffID, err := FetchImageBlob(ctx, ic.Options, &image, ic.progressOutput)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
if diffID == "" {
|
||||
t.Errorf("Expected a diffID, got nil.")
|
||||
}
|
||||
|
||||
tarFile, err := ioutil.ReadFile(path.Join(DestinationDirectory(ic.Options), LayerID, LayerID+".tar"))
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
br := bytes.NewReader(tarFile)
|
||||
tr := tar.NewReader(br)
|
||||
out := new(bytes.Buffer)
|
||||
|
||||
// extract the tar file
|
||||
for {
|
||||
_, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
if _, err := io.Copy(out, tr); err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// compare contents of tar file to dummy data
|
||||
if out.String() != LayerContent {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
hist, err := ioutil.ReadFile(path.Join(DestinationDirectory(ic.Options), LayerID, LayerID+".json"))
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
if string(hist) != LayerHistory {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPingPortLayer(t *testing.T) {
|
||||
|
||||
options := Options{
|
||||
Outstream: os.Stdout,
|
||||
}
|
||||
|
||||
ic := NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
|
||||
s := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("\"OK\""))
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ic.Options.Host = s.URL[7:]
|
||||
|
||||
b, err := PingPortLayer(ic.Host)
|
||||
if err != nil || b != true {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListImages(t *testing.T) {
|
||||
|
||||
options := Options{
|
||||
Outstream: os.Stdout,
|
||||
}
|
||||
|
||||
ic := NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
|
||||
s := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
resp := `
|
||||
[
|
||||
{
|
||||
"ID": "7bd023c8937ded982c1b98da453b1a5afec86f390ffad8fa0f4fba244a6155f1",
|
||||
"Metadata": {
|
||||
"v1Compatibility": "{\"id\":\"7bd023c8937ded982c1b98da453b1a5afec86f390ffad8fa0f4fba244a6155f1\",\"parent\":\"b873f334fa5259acb24cf0e2cd2639d3a9fb3eb9bafbca06ed4f702c289b31c0\",\"created\":\"2016-05-27T14:15:02.359284074Z\",\"container\":\"b8bd6a8e8874a87f626871ce370f4775bdf598865637082da2949ee0f4786432\",\"container_config\":{\"Hostname\":\"914cf42a3e15\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/bin/bash\\\"]\"],\"Image\":\"b873f334fa5259acb24cf0e2cd2639d3a9fb3eb9bafbca06ed4f702c289b31c0\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"914cf42a3e15\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[],\"Cmd\":[\"/bin/bash\"],\"Image\":\"b873f334fa5259acb24cf0e2cd2639d3a9fb3eb9bafbca06ed4f702c289b31c0\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\"}"
|
||||
},
|
||||
"SelfLink": "http://Photon/storage/7bd023c8937ded982c1b98da453b1a5afec86f390ffad8fa0f4fba244a6155f1",
|
||||
"Store": "http://Photon/storage/PetStore"
|
||||
}
|
||||
]
|
||||
`
|
||||
w.Write([]byte(resp))
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ic.Options.Host = s.URL[7:]
|
||||
|
||||
m, err := ListImages(ic.Host, ic.Storename, nil)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
if m["7bd023c8937ded982c1b98da453b1a5afec86f390ffad8fa0f4fba244a6155f1"].ID != "7bd023c8937ded982c1b98da453b1a5afec86f390ffad8fa0f4fba244a6155f1" {
|
||||
t.Errorf("Returned list %#v is different than expected", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchScenarios(t *testing.T) {
|
||||
var err error
|
||||
ctx := context.TODO()
|
||||
|
||||
options := Options{
|
||||
Outstream: os.Stdout,
|
||||
Timeout: DefaultHTTPTimeout,
|
||||
}
|
||||
|
||||
ic := NewImageC(options, streamformatter.NewJSONStreamFormatter())
|
||||
|
||||
wwwAuthenticate := false
|
||||
insufficientScope := false
|
||||
invalidToken := false
|
||||
|
||||
s := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
if wwwAuthenticate {
|
||||
w.Header().Set("www-authenticate", "Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\",scope=\"repository:library/busybox:pull\"")
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
} else {
|
||||
if !insufficientScope && !invalidToken {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(DefaultManifest))
|
||||
} else if insufficientScope {
|
||||
w.Header().Set("www-authenticate", "error=\"insufficient_scope\"")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
} else if invalidToken {
|
||||
w.Header().Set("www-authenticate", "error=\"invalid_token\"")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ic.Options.Registry = s.URL
|
||||
ic.Options.Image = Image
|
||||
ic.Options.Tag = Tag
|
||||
ic.Options.InsecureAllowHTTP = true
|
||||
ic.Options.Reference, err = reference.ParseNamed(Reference)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
|
||||
// create a temporary directory
|
||||
dir, err := ioutil.TempDir("", "imagec")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ic.Options.Destination = dir
|
||||
|
||||
ic.Options.Token = nil
|
||||
// try without token, response will miss www-authenticate so we will retry (and fail eventually)
|
||||
_, _, err = FetchImageManifest(ctx, ic.Options, 1, ic.progressOutput)
|
||||
if err == nil {
|
||||
t.Errorf("Condition didn't fail (testing failure)")
|
||||
}
|
||||
|
||||
// set the www-authenticate
|
||||
wwwAuthenticate = true
|
||||
// try without token, response will carry a valid www-authenticate so we won't retry
|
||||
_, _, err = FetchImageManifest(ctx, ic.Options, 1, ic.progressOutput)
|
||||
if err != nil {
|
||||
// we should get a DNR error
|
||||
if _, isDNR := err.(urlfetcher.DoNotRetry); !isDNR {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
wwwAuthenticate = false
|
||||
|
||||
// set a valid token
|
||||
ic.Options.Token = &urlfetcher.Token{Token: OAuthToken}
|
||||
|
||||
// enable invalid token test
|
||||
invalidToken = true
|
||||
// valid token but faulty hub, we should retry but eventually fail
|
||||
_, _, err = FetchImageManifest(ctx, ic.Options, 1, ic.progressOutput)
|
||||
if err == nil {
|
||||
t.Errorf("Condition didn't fail (testing failure)")
|
||||
}
|
||||
invalidToken = false
|
||||
|
||||
// enable insufficient_scope test
|
||||
insufficientScope = true
|
||||
// valid token but image is missing we shouldn't retry
|
||||
_, _, err = FetchImageManifest(ctx, ic.Options, 1, ic.progressOutput)
|
||||
if err != nil {
|
||||
// we should get a ImageNotFoundError
|
||||
if _, imageErr := err.(urlfetcher.ImageNotFoundError); !imageErr {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
insufficientScope = false
|
||||
|
||||
// valid token, existing image, correct header. We should succeed
|
||||
_, _, err = FetchImageManifest(ctx, ic.Options, 1, ic.progressOutput)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
180
vendor/github.com/vmware/vic/lib/imagec/layer_cache.go
generated
vendored
Normal file
180
vendor/github.com/vmware/vic/lib/imagec/layer_cache.go
generated
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package imagec
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/vmware/vic/lib/apiservers/engine/backends/kv"
|
||||
"github.com/vmware/vic/lib/apiservers/portlayer/client"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
// LCache is an in-memory cache to account for existing image layers
|
||||
// It is used primarily by imagec when coordinating layer downloads
|
||||
// The cache is initially hydrated by way of the image cache at startup
|
||||
type LCache struct {
|
||||
m sync.RWMutex
|
||||
layers map[string]*ImageWithMeta
|
||||
|
||||
client *client.PortLayer
|
||||
dirty bool
|
||||
}
|
||||
|
||||
// LayerNotFoundError is returned when a layer does not exist in the cache
|
||||
type LayerNotFoundError struct{}
|
||||
|
||||
func (e LayerNotFoundError) Error() string {
|
||||
return "Layer does not exist"
|
||||
}
|
||||
|
||||
const (
|
||||
layerCacheKey = "layers"
|
||||
)
|
||||
|
||||
var (
|
||||
layerCache *LCache
|
||||
)
|
||||
|
||||
func init() {
|
||||
layerCache = &LCache{
|
||||
layers: make(map[string]*ImageWithMeta),
|
||||
}
|
||||
}
|
||||
|
||||
// LayerCache returns a reference to the layer cache
|
||||
func LayerCache() *LCache {
|
||||
return layerCache
|
||||
}
|
||||
|
||||
// InitializeLayerCache will create a new layer cache or rehydrate an
|
||||
// existing layer cache from the portlayer k/v store
|
||||
func InitializeLayerCache(client *client.PortLayer) error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
log.Debugf("Initializing layer cache")
|
||||
|
||||
layerCache.client = client
|
||||
|
||||
val, err := kv.Get(client, layerCacheKey)
|
||||
if err != nil && err != kv.ErrKeyNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
l := struct {
|
||||
Layers map[string]*ImageWithMeta
|
||||
}{}
|
||||
|
||||
if val != "" {
|
||||
if err = json.Unmarshal([]byte(val), &l); err != nil {
|
||||
return fmt.Errorf("Failed to unmarshal layer cache: %s", err)
|
||||
}
|
||||
|
||||
layerCache.layers = l.Layers
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add adds a new layer to the cache
|
||||
func (lc *LCache) Add(layer *ImageWithMeta) {
|
||||
defer trace.End(trace.Begin(""))
|
||||
lc.m.Lock()
|
||||
defer lc.m.Unlock()
|
||||
|
||||
lc.layers[layer.ID] = layer
|
||||
lc.dirty = true
|
||||
}
|
||||
|
||||
// Remove removes a layer from the cache
|
||||
func (lc *LCache) Remove(id string) {
|
||||
defer trace.End(trace.Begin(""))
|
||||
lc.m.Lock()
|
||||
defer lc.m.Unlock()
|
||||
|
||||
if _, ok := lc.layers[id]; ok {
|
||||
delete(lc.layers, id)
|
||||
lc.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
// Commit marks a layer as downloaded
|
||||
func (lc *LCache) Commit(layer *ImageWithMeta) error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
lc.m.Lock()
|
||||
defer lc.m.Unlock()
|
||||
|
||||
lc.layers[layer.ID] = layer
|
||||
lc.layers[layer.ID].Downloading = false
|
||||
lc.dirty = true
|
||||
|
||||
return lc.save()
|
||||
}
|
||||
|
||||
// Get returns a cached layer, or LayerNotFoundError if it doesn't exist
|
||||
func (lc *LCache) Get(id string) (*ImageWithMeta, error) {
|
||||
defer trace.End(trace.Begin(""))
|
||||
lc.m.RLock()
|
||||
defer lc.m.RUnlock()
|
||||
|
||||
layer, ok := lc.layers[id]
|
||||
if !ok {
|
||||
return nil, LayerNotFoundError{}
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
func (lc *LCache) save() error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
if !lc.dirty {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := struct {
|
||||
Layers map[string]*ImageWithMeta
|
||||
}{
|
||||
Layers: lc.layers,
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to marshal layer cache: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
err = kv.Put(lc.client, layerCacheKey, string(bytes))
|
||||
if err != nil {
|
||||
log.Errorf("Unable to save layer cache: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
lc.dirty = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save will persist the image cache to the portlayer k/v store
|
||||
func (lc *LCache) Save() error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
lc.m.Lock()
|
||||
defer lc.m.Unlock()
|
||||
|
||||
return lc.save()
|
||||
}
|
||||
116
vendor/github.com/vmware/vic/lib/imagec/storage.go
generated
vendored
Normal file
116
vendor/github.com/vmware/vic/lib/imagec/storage.go
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package imagec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
rc "github.com/go-openapi/runtime/client"
|
||||
|
||||
apiclient "github.com/vmware/vic/lib/apiservers/portlayer/client"
|
||||
"github.com/vmware/vic/lib/apiservers/portlayer/client/misc"
|
||||
"github.com/vmware/vic/lib/apiservers/portlayer/client/storage"
|
||||
"github.com/vmware/vic/lib/apiservers/portlayer/models"
|
||||
"github.com/vmware/vic/lib/metadata"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.TODO()
|
||||
)
|
||||
|
||||
// PingPortLayer calls the _ping endpoint of the portlayer
|
||||
func PingPortLayer(host string) (bool, error) {
|
||||
defer trace.End(trace.Begin(host))
|
||||
|
||||
transport := rc.New(host, "/", []string{"http"})
|
||||
client := apiclient.New(transport, nil)
|
||||
|
||||
ok, err := client.Misc.Ping(misc.NewPingParamsWithContext(ctx))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ok.Payload == "OK", nil
|
||||
}
|
||||
|
||||
// ListImages lists the images from given image store
|
||||
func ListImages(host, storename string, images []*ImageWithMeta) (map[string]*models.Image, error) {
|
||||
defer trace.End(trace.Begin(storename))
|
||||
|
||||
transport := rc.New(host, "/", []string{"http"})
|
||||
client := apiclient.New(transport, nil)
|
||||
|
||||
ids := make([]string, len(images))
|
||||
|
||||
for i := range images {
|
||||
ids = append(ids, images[i].ID)
|
||||
}
|
||||
|
||||
imageList, err := client.Storage.ListImages(
|
||||
storage.NewListImagesParamsWithContext(ctx).WithStoreName(storename).WithIds(ids),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingImages := make(map[string]*models.Image)
|
||||
for i := range imageList.Payload {
|
||||
v := imageList.Payload[i]
|
||||
existingImages[v.ID] = v
|
||||
}
|
||||
return existingImages, nil
|
||||
}
|
||||
|
||||
// WriteImage writes the image to given image store
|
||||
func WriteImage(host string, image *ImageWithMeta, data io.ReadCloser) error {
|
||||
defer trace.End(trace.Begin(image.ID))
|
||||
|
||||
transport := rc.New(host, "/", []string{"http"})
|
||||
client := apiclient.New(transport, nil)
|
||||
|
||||
transport.Consumers["application/json"] = runtime.JSONConsumer()
|
||||
transport.Producers["application/json"] = runtime.JSONProducer()
|
||||
transport.Consumers["application/octet-stream"] = runtime.ByteStreamConsumer()
|
||||
transport.Producers["application/octet-stream"] = runtime.ByteStreamProducer()
|
||||
|
||||
key := new(string)
|
||||
blob := new(string)
|
||||
|
||||
*key = metadata.MetaDataKey
|
||||
*blob = image.Meta
|
||||
|
||||
r, err := client.Storage.WriteImage(
|
||||
storage.NewWriteImageParamsWithContext(ctx).
|
||||
WithImageID(image.ID).
|
||||
WithParentID(image.Parent).
|
||||
WithStoreName(image.Store).
|
||||
WithMetadatakey(key).
|
||||
WithMetadataval(blob).
|
||||
WithImageFile(data).
|
||||
WithSum(image.Layer.BlobSum),
|
||||
)
|
||||
if err != nil {
|
||||
log.Debugf("Creating an image failed: %s", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("Created an image %#v", r.Payload)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user