* 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
486 lines
14 KiB
Go
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
|
|
}
|