Files
virtual-kubelet/vendor/github.com/vmware/vic/lib/imagec/docker.go
Loc Nguyen 513cebe7b7 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
2018-06-04 15:41:32 -07:00

486 lines
14 KiB
Go

// 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
}