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:
Loc Nguyen
2018-06-04 15:41:32 -07:00
committed by Ria Bhatia
parent 98a111e8b7
commit 513cebe7b7
6296 changed files with 1123685 additions and 8 deletions

View File

@@ -0,0 +1,788 @@
// 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 backends
import (
"archive/tar"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"golang.org/x/net/context"
log "github.com/Sirupsen/logrus"
"github.com/tchap/go-patricia/patricia"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
viccontainer "github.com/vmware/vic/lib/apiservers/engine/backends/container"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"github.com/vmware/vic/lib/apiservers/engine/proxy"
"github.com/vmware/vic/lib/apiservers/portlayer/client/storage"
vicarchive "github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/pkg/trace"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
)
// ContainerArchivePath creates an archive of the filesystem resource at the
// specified path in the container identified by the given name. Returns a
// tar archive of the resource and whether it was a directory or a single file.
func (c *ContainerBackend) ContainerArchivePath(name string, path string) (io.ReadCloser, *types.ContainerPathStat, error) {
defer trace.End(trace.Begin(name))
op := trace.NewOperation(context.Background(), "ContainerArchivePath: %s", name)
path = "/" + strings.TrimPrefix(path, "/")
vc := cache.ContainerCache().GetContainer(name)
if vc == nil {
return nil, nil, errors.NotFoundError(name)
}
stat, err := c.ContainerStatPath(name, path)
if err != nil {
return nil, nil, err
}
reader, err := c.exportFromContainer(op, vc, path)
if err != nil {
if errors.IsResourceInUse(err) {
err = fmt.Errorf("ContainerArchivePath failed, resource in use: %s", err.Error())
}
return nil, nil, errors.InternalServerError(err.Error())
}
return reader, stat, nil
}
func (c *ContainerBackend) exportFromContainer(op trace.Operation, vc *viccontainer.VicContainer, path string) (io.ReadCloser, error) {
mounts := proxy.MountsFromContainer(vc)
mounts = append(mounts, types.MountPoint{Destination: "/"})
readerMap := NewArchiveStreamReaderMap(op, mounts, path)
readers, err := readerMap.ReadersForSourcePath(archiveProxy, vc.ContainerID, path)
if err != nil {
op.Errorf("Errors getting readers for export: %s", err.Error())
return nil, err
}
count := len(readers)
op.Infof("Got %d archive readers", count)
// We want to combine the streams, so need to strip the end-of-archive elements for all but the last
strippersWithCloser := make([]io.Reader, len(readers))
i := 0
for ; i < count-1; i++ {
stripper := vicarchive.NewStripper(op, tar.NewReader(readers[i]), readers[i].Close)
strippersWithCloser[i] = stripper
op.Debugf("Added stripping reader: %p", stripper)
}
op.Debugf("Adding closing reader: %p", readers[i])
strippersWithCloser[i] = readers[i]
return vicarchive.MultiReader(strippersWithCloser...), nil
}
// ContainerCopy performs a deprecated operation of archiving the resource at
// the specified path in the container identified by the given name.
func (c *ContainerBackend) ContainerCopy(name string, res string) (io.ReadCloser, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "ContainerCopy")
}
// ContainerExport writes the contents of the container to the given
// writer. An error is returned if the container cannot be found.
func (c *ContainerBackend) ContainerExport(name string, out io.Writer) error {
return errors.APINotSupportedMsg(ProductName(), "ContainerExport")
}
// ContainerExtractToDir extracts the given archive to the specified location
// in the filesystem of the container identified by the given name. The given
// path must be of a directory in the container. If it is not, the error will
// be ErrExtractPointNotDirectory. If noOverwriteDirNonDir is true then it will
// be an error if unpacking the given content would cause an existing directory
// to be replaced with a non-directory and vice versa.
func (c *ContainerBackend) ContainerExtractToDir(name, path string, noOverwriteDirNonDir bool, content io.Reader) error {
defer trace.End(trace.Begin(name))
op := trace.NewOperation(context.Background(), "ContainerExtractToDir: %s", name)
path = "/" + strings.TrimPrefix(path, "/")
vc := cache.ContainerCache().GetContainer(name)
if vc == nil {
return errors.NotFoundError(name)
}
err := c.importToContainer(op, vc, path, content)
if err != nil && errors.IsResourceInUse(err) {
op.Errorf("ContainerExtractToDir failed, resource in use: %s", err.Error())
err = fmt.Errorf("Resource in use")
}
return err
}
func (c *ContainerBackend) importToContainer(op trace.Operation, vc *viccontainer.VicContainer, target string, content io.Reader) (err error) {
rawReader, err := archive.DecompressStream(content)
if err != nil {
op.Errorf("Input tar stream to ContainerExtractToDir not recognized: %s", err.Error())
return errors.StreamFormatNotRecognized()
}
tarReader := tar.NewReader(rawReader)
mounts := proxy.MountsFromContainer(vc)
mounts = append(mounts, types.MountPoint{Destination: "/"})
writerMap := NewArchiveStreamWriterMap(op, mounts, target)
defer func() {
// This should shutdown all the stream connections to the portlayer.
e1 := writerMap.Close(op)
if e1 != nil {
err = e1
op.Debugf("import to container: assigned err as %v", err)
}
}()
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
op.Errorf("Error reading tar header from client archive: %s", err)
return err
}
// Lookup the writer for that mount prefix
tarWriter, err := writerMap.WriterForAsset(archiveProxy, vc.ContainerID, target, *header)
if err != nil {
return err
}
if err = tarWriter.WriteHeader(header); err != nil {
op.Errorf("Error while copying tar header %#v: %s", *header, err.Error())
return err
}
if _, err = io.Copy(tarWriter, tarReader); err != nil {
op.Errorf("Error while copying tar data for %s: %s", header.Name, err.Error())
return err
}
// TODO: change this to log level 3
if vchConfig.Cfg.Diagnostics.DebugLevel >= 1 {
op.Debugf("Wrote entry: %s", header.Name)
}
}
return nil
}
// ContainerStatPath stats the filesystem resource at the specified path in the
// container identified by the given name.
func (c *ContainerBackend) ContainerStatPath(name string, path string) (stat *types.ContainerPathStat, err error) {
defer trace.End(trace.Begin(name))
op := trace.NewOperation(context.Background(), "ContainerStatPath: %s", name)
op.Debugf("path received by statpath %s", path)
vc := cache.ContainerCache().GetContainer(name)
if vc == nil {
return nil, errors.NotFoundError(name)
}
// trim / and . off from path and then append / to ensure the format is correct
path = filepath.Clean(path)
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
mounts := proxy.MountsFromContainer(vc)
mounts = append(mounts, types.MountPoint{Destination: "/"})
// handle the special case of targeting a volume mount point before it exists.
// this will be important for non started container cp, will also be important
// to certain behaviors for diff on a non started container.
if stat, succeed := tryFakeStatPath(mounts, path); succeed {
op.Debugf("faking container stat path %#v", stat)
return stat, nil
}
primaryTarget := resolvePathWithMountPoints(op, mounts, path)
fs := primaryTarget.filterSpec
var deviceID string
var store string
if primaryTarget.mountPoint.Destination == "/" {
// Special case. / refers to container VMDK and not a volume vmdk.
deviceID = vc.ContainerID
store = constants.ContainerStoreName
} else {
deviceID = primaryTarget.mountPoint.Name
store = constants.VolumeStoreName
}
stat, err = archiveProxy.StatPath(op, store, deviceID, fs)
if err != nil {
op.Errorf("error getting statpath: %s", err.Error())
switch err := err.(type) {
case *storage.StatPathNotFound:
return nil, errors.ContainerResourceNotFoundError(vc.Name, "file or directory")
case *storage.StatPathUnprocessableEntity:
return nil, errors.InternalServerError("failed to process given path")
default:
return nil, errors.InternalServerError(err.Error())
}
}
op.Debugf("container stat path %#v", stat)
return stat, nil
}
//----------------------------------
// Docker cp utility
//----------------------------------
type ArchiveReader struct {
mountPoint types.MountPoint
filterSpec vicarchive.FilterSpec
reader io.ReadCloser
}
type ArchiveStreamReaderMap struct {
prefixTrie *patricia.Trie
op trace.Operation
}
type ArchiveWriter struct {
mountPoint types.MountPoint
filterSpec vicarchive.FilterSpec
writer io.WriteCloser
tarWriter *tar.Writer
}
// ArchiveStreamWriterMap maps mount prefix to io.WriteCloser
type ArchiveStreamWriterMap struct {
prefixTrie *patricia.Trie
op trace.Operation
wg *sync.WaitGroup
errchan chan error
}
// NewArchiveStreamWriterMap creates a new ArchiveStreamWriterMap. The map contains all information
// needed to create a writers for every volume mounts for the container. This includes the root
// volume of the container.
//
// mounts is the mount data from inspect
// containerDestPath is the destination path in the container
func NewArchiveStreamWriterMap(op trace.Operation, mounts []types.MountPoint, dest string) *ArchiveStreamWriterMap {
writerMap := &ArchiveStreamWriterMap{}
writerMap.prefixTrie = patricia.NewTrie()
writerMap.op = op
writerMap.errchan = make(chan error, 1)
writerMap.wg = &sync.WaitGroup{}
for _, m := range mounts {
aw := ArchiveWriter{
mountPoint: m,
writer: nil,
}
// If container destination path is part of this mount point's prefix, we must remove it and
// add to the filterspec. If the container destination path is "/" we do no stripping.
//
// e.g. mount A at /mnt/A
//
// cp /mnt cid:/mnt
//
// file data.txt from local /mnt/A/data.txt will come to the persona as A/data.txt. We must
// tell the storage portlayer to remove "A".
//
// e.g. mount A at /mnt/A
//
// cp / cid:/
//
// file data.txt from local /mnt/A/data.txt will come to the persona as mnt/A/data.txt.
// Here, we must tell the portlayer to remove "mnt/A". The key to determining whether to
// strip "A" or "mnt/A" is based on the container destination path.
isPrimary := !strings.Contains(aw.mountPoint.Destination, dest) || aw.mountPoint.Destination == dest
aw.filterSpec = vicarchive.GenerateFilterSpec(dest, aw.mountPoint.Destination, isPrimary, vicarchive.CopyTo)
writerMap.prefixTrie.Insert(patricia.Prefix(m.Destination), &aw)
}
return writerMap
}
// NewArchiveStreamReaderMap creates a new ArchiveStreamReaderMap. After the call, it contains
// information to create readers for every volume mounts for the container
//
// mounts is the mount data from inspect
func NewArchiveStreamReaderMap(op trace.Operation, mounts []types.MountPoint, dest string) *ArchiveStreamReaderMap {
readerMap := &ArchiveStreamReaderMap{}
readerMap.prefixTrie = patricia.NewTrie()
readerMap.op = op
for _, m := range mounts {
ar := ArchiveReader{
mountPoint: m,
reader: nil,
}
// If the mount point is not the root file system, we must tell the portlayer to rebase the
// files in the return tar stream with the mount point path since the volume does not know
// the path it is mounted to. It only knows it's root file system.
//
// e.g. mount A at /mnt/A with a file data.txt in A
//
// /mnt/A/data.txt <-- from container point of view
// /data.txt <-- from volume point of view
//
// Neither the volume nor the storage portlayer knows about /mnt/A. The persona must tell
// the portlayer to rebase all files from this volume to the /mnt/A/ in the final tar stream.
cleanDest := vicarchive.Clean(dest, false)
isPrimary := !strings.Contains(ar.mountPoint.Destination, cleanDest) || ar.mountPoint.Destination == cleanDest
ar.filterSpec = vicarchive.GenerateFilterSpec(dest, ar.mountPoint.Destination, isPrimary, vicarchive.CopyFrom)
readerMap.prefixTrie.Insert(patricia.Prefix(m.Destination), &ar)
}
return readerMap
}
// FindArchiveWriter finds the one writer that matches the asset name. There should only be one
// stream this asset needs to be written to.
func (wm *ArchiveStreamWriterMap) FindArchiveWriter(containerDestPath, assetName string) (*ArchiveWriter, error) {
defer trace.End(trace.Begin(""))
var aw *ArchiveWriter
var err error
// go function used later for searching
findPrefix := func(prefix patricia.Prefix, item patricia.Item) error {
if _, ok := item.(*ArchiveWriter); !ok {
return fmt.Errorf("item not ArchiveWriter")
}
aw, _ = item.(*ArchiveWriter)
return nil
}
// Find the prefix for the final destination. Final destination is the combination of container destination path
// and the asset's name. For example,
//
// container destination path = /
// asset name = mnt/A/file.txt
// mount 1 = /mnt/A
// mount prefix = /mnt/A
//
// In the above example, mount prefxi can only be determined by combining both the container destination path and
// the asset name, as the final destination includes a mounted volume.
combinedPath := path.Join(containerDestPath, assetName)
prefix := patricia.Prefix(combinedPath)
err = wm.prefixTrie.VisitPrefixes(prefix, findPrefix)
if err != nil {
wm.op.Errorf(err.Error())
return nil, fmt.Errorf("Failed to find a node for prefix %s: %s", containerDestPath, err.Error())
}
if aw == nil {
return nil, fmt.Errorf("No archive writer found for container destination %s and asset name %s", containerDestPath, assetName)
}
return aw, nil
}
// WriterForAsset takes a destination path and subpath of the archive data and returns the
// appropriate writer for the two. It's intention is to solve the case where there exist
// a mount point and another mount point within the first mount point. For instance, the
// prefix map can have,
//
// R/W - /
// mount 1 - /mnt/a
// mount 2 - /mnt/a/b
//
// case 1:
// containerDestPath - /mnt/a
// archive header source - b/file.txt
//
// The correct writer would be the one corresponding to mount 2.
//
// case 2:
// containerDestPath - /mnt/a
// archive header source - file.txt
//
// The correct writer would be the one corresponding to mount 1.
//
// case 3:
// containerDestPath - /
// archive header source - mnt/a/file.txt
//
// The correct writer would be the one corresponding to mount 1
//
// As demonstrated above, the mount prefix and writer cannot be determined with just the
// container destination path. It must be combined with the actual asset's name.
func (wm *ArchiveStreamWriterMap) WriterForAsset(proxy proxy.VicArchiveProxy, cid, containerDestPath string, assetHeader tar.Header) (*tar.Writer, error) {
defer trace.End(trace.Begin(assetHeader.Name))
var err error
aw, err := wm.FindArchiveWriter(containerDestPath, assetHeader.Name)
if err != nil {
return nil, err
}
// Perform the lazy initialization here.
if aw.writer == nil || aw.tarWriter == nil {
// lazy initialize.
wm.op.Debugf("Lazily initializing import stream for %s", aw.mountPoint.Destination)
var deviceID string
var store string
if aw.mountPoint.Destination == "/" {
// Special case. / refers to container VMDK and not a volume vmdk.
deviceID = cid
store = constants.ContainerStoreName
} else {
deviceID = aw.mountPoint.Name
store = constants.VolumeStoreName
}
rawWriter, err := proxy.ArchiveImportWriter(wm.op, store, deviceID, aw.filterSpec, wm.wg, wm.errchan)
if err != nil {
err = fmt.Errorf("Unable to initialize import stream writer for mount prefix %s", aw.mountPoint.Destination)
wm.op.Errorf(err.Error())
return nil, err
}
aw.writer = rawWriter
aw.tarWriter = tar.NewWriter(rawWriter)
}
return aw.tarWriter, nil
}
// Close visits all the archive writer in the trie and closes the actual io.WritCloser
func (wm *ArchiveStreamWriterMap) Close(op trace.Operation) error {
defer trace.End(trace.Begin(""))
closeStream := func(prefix patricia.Prefix, item patricia.Item) error {
if aw, ok := item.(*ArchiveWriter); ok && aw.writer != nil {
aw.tarWriter.Close()
aw.writer.Close()
aw.tarWriter = nil
aw.writer = nil
}
return nil
}
wm.prefixTrie.Visit(closeStream)
// wait for all pl calls to return and close the channel
go func() {
wm.wg.Wait()
close(wm.errchan)
}()
var err error
// wait for all the streams to finish
for result := range wm.errchan {
if result != nil {
err = result
op.Errorf("Error received from portlayer for import streams: %s", result.Error())
}
}
return err
}
// FindArchiveReaders finds all archive readers that are within the container source path. For example,
//
// mount A - /mnt/A
// mount B - /mnt/B
// mount AB - /mnt/A/AB
// base container - /
//
// container source path - /mnt/A
//
// For the above example, this function returns the readers for mount A and mount AB but not the
// readers for / or mount B.
func (rm *ArchiveStreamReaderMap) FindArchiveReaders(containerSourcePath string) ([]*ArchiveReader, error) {
defer trace.End(trace.Begin(containerSourcePath))
var nodes []*ArchiveReader
var startingNode *ArchiveReader
var err error
var isMountPoint bool
findStartingPrefix := func(prefix patricia.Prefix, item patricia.Item) error {
if _, ok := item.(*ArchiveReader); !ok {
return fmt.Errorf("item not ArchiveReader")
}
startingNode = item.(*ArchiveReader)
return nil
}
walkPrefixSubtree := func(prefix patricia.Prefix, item patricia.Item) error {
if _, ok := item.(*ArchiveReader); !ok {
return fmt.Errorf("item not ArchiveReader")
}
ar, _ := item.(*ArchiveReader)
nodes = append(nodes, ar)
isMountPoint = ar.mountPoint.Destination != "/" &&
(isMountPoint || strings.HasPrefix(containerSourcePath, ar.mountPoint.Destination))
return nil
}
// Clean off any trailing periods from the path, such as `cp cid:/mnt/. -`
// Including the periods in the prefix walk would not match with subvolume
// mounts like /mnt/vol1 or /mnt/vol2.
// Find all mounts for the sourcepath
cleanPath := vicarchive.Clean(containerSourcePath, false)
prefix := patricia.Prefix(cleanPath)
err = rm.prefixTrie.VisitSubtree(prefix, walkPrefixSubtree)
if err != nil {
msg := fmt.Sprintf("Failed to find a node for prefix %s: %s", containerSourcePath, err.Error())
rm.op.Errorf(msg)
return nil, fmt.Errorf(msg)
}
// The above subtree walking MAY NOT find the starting prefix. For example /etc will not find /.
// Subtree only finds prefix that starts with /etc. VisitPrefixes will find the starting prefix.
// If the search was for /, then it will not find the starting node. In that case, we grab the
// first node in the slice.
err = rm.prefixTrie.VisitPrefixes(prefix, findStartingPrefix)
if err != nil {
msg := fmt.Sprintf("Failed to find starting node for prefix %s: %s", containerSourcePath, err.Error())
rm.op.Errorf(msg)
return nil, fmt.Errorf(msg)
}
if startingNode != nil {
found := false
for _, node := range nodes {
if node.mountPoint.Destination == startingNode.mountPoint.Destination {
found = true
break
}
}
if !found {
// prepend the starting node at the beginning
nodes = append([]*ArchiveReader{startingNode}, nodes...)
}
} else if len(nodes) > 0 {
startingNode = nodes[0]
} else {
msg := fmt.Sprintf("Failed to find starting node for prefix %s: %s", containerSourcePath, err.Error())
rm.op.Errorf(msg)
return nil, fmt.Errorf(msg)
}
// if the path is a mount path, we need to include the directory header of the actual mountpoint
// to ensure the corrent permissions of the directory, eg docker cp cid:/mnt/vol1/ needs to include
// header from /mnt/vol1 located on containerfs
// data from /mnt/vol1/ located on deviceId vol1
if isMountPoint && path.Base(containerSourcePath) != "." {
rm.op.Debugf("%s is a mountpoint, getting dir permissions from parent", cleanPath)
// find the parent node using VisitPrefixes
parent := path.Dir(cleanPath)
prefix = patricia.Prefix(parent)
startingNode = nil
err = rm.prefixTrie.VisitPrefixes(prefix, findStartingPrefix)
if err != nil {
msg := fmt.Sprintf("Failed to generate parent node for mountpoint %s: %s", parent, err.Error())
rm.op.Errorf(msg)
return nil, fmt.Errorf(msg)
}
var found bool
if startingNode != nil {
for _, node := range nodes {
found = found || node.mountPoint.Destination == startingNode.mountPoint.Destination
}
if !found {
nodes = append([]*ArchiveReader{startingNode}, nodes...)
}
}
}
err = rm.buildFilterSpec(containerSourcePath, nodes, startingNode)
if err != nil {
return nil, err
}
return nodes, nil
}
// ReadersForSourcePath returns all an array of io.Reader for all the readers within a container source path.
// Example:
// Reader 1 - /mnt/A
// Reader 2 - /mnt/A/B
//
// containerSourcePath - /mnt/A
// In the above, both readers are within the the container source path.
func (rm *ArchiveStreamReaderMap) ReadersForSourcePath(proxy proxy.VicArchiveProxy, cid, containerSourcePath string) ([]io.ReadCloser, error) {
defer trace.End(trace.Begin(containerSourcePath))
var streamReaders []io.ReadCloser
nodes, err := rm.FindArchiveReaders(containerSourcePath)
if err != nil {
return nil, err
}
mounts := []string{}
for _, node := range nodes {
mounts = append(mounts, node.mountPoint.Destination)
}
// Create the io.Reader for those mounts if they haven't already been initialized
for _, node := range nodes {
if node.reader == nil {
var store, deviceID string
if node.mountPoint.Destination == "/" {
// Special case. / refers to container VMDK and not a volume vmdk.
store = constants.ContainerStoreName
deviceID = cid
} else {
store = constants.VolumeStoreName
deviceID = node.mountPoint.Name
}
rm.op.Infof("Lazily initializing export stream for %s [%s]", node.mountPoint.Name, node.mountPoint.Destination)
reader, err := proxy.ArchiveExportReader(rm.op, store, "", deviceID, "", true, node.filterSpec)
if err != nil {
err = fmt.Errorf("Unable to initialize export stream reader for prefix %s", node.mountPoint.Destination)
rm.op.Errorf(err.Error())
return nil, err
}
rm.op.Infof("Lazy initialization created reader %#v", reader)
streamReaders = append(streamReaders, reader)
} else {
streamReaders = append(streamReaders, node.reader)
}
}
if len(nodes) == 0 {
rm.op.Infof("Found no archive readers for %s", containerSourcePath)
}
return streamReaders, nil
}
// Close visits all the archive readers in the trie and closes the actual io.ReadCloser
func (rm *ArchiveStreamReaderMap) Close() {
defer trace.End(trace.Begin(""))
closeStream := func(prefix patricia.Prefix, item patricia.Item) error {
if aw, ok := item.(*ArchiveReader); ok && aw.reader != nil {
aw.reader.Close()
aw.reader = nil
}
return nil
}
rm.prefixTrie.Visit(closeStream)
}
// tryFakeStatPath tries to fake the statpath for path that targets the mountpoint or along the mountpoint
func tryFakeStatPath(mounts []types.MountPoint, target string) (*types.ContainerPathStat, bool) {
isMountPathTarget := false
for _, mount := range mounts {
if strings.HasPrefix(mount.Destination, target) {
isMountPathTarget = true
}
}
// check to see if the path is a mount point, if so, return fake path
if isMountPathTarget {
return &types.ContainerPathStat{
Name: filepath.Base(target),
Size: int64(4096),
Mode: os.ModeDir,
Mtime: time.Now(),
LinkTarget: "",
}, true
}
return nil, false
}
// resolvePathWithMountPoints use mounts to generate a filter spec for the given path
func resolvePathWithMountPoints(op trace.Operation, mounts []types.MountPoint, path string) *ArchiveReader {
var primaryTarget *ArchiveReader
readerMap := NewArchiveStreamReaderMap(op, mounts, path)
// #nosec: Errors unhandled.
nodes, _ := readerMap.FindArchiveReaders(path)
for _, node := range nodes {
if strings.HasPrefix(path, node.mountPoint.Destination) &&
(primaryTarget == nil || len(node.mountPoint.Destination) > len(primaryTarget.mountPoint.Destination)) {
primaryTarget = node
}
}
return primaryTarget
}
func (rm *ArchiveStreamReaderMap) buildFilterSpec(containerSourcePath string, nodes []*ArchiveReader, startingNode *ArchiveReader) error {
mounts, foundNodes, err := rm.buildMountsAndNodes(startingNode.mountPoint.Destination, startingNode)
if err != nil {
return err
}
for _, node := range foundNodes {
vicarchive.AddMountInclusionsExclusions(node.mountPoint.Destination, &node.filterSpec, mounts, containerSourcePath)
}
return nil
}
// buildMountsAndNodes returns the node pointers from the prefix tree as well as all mounts involved in the operation
func (rm *ArchiveStreamReaderMap) buildMountsAndNodes(path string, node *ArchiveReader) ([]string, []*ArchiveReader, error) {
// NOTE(sflxn): We can modify this to make proper exclusions in the future. For now,
// we assemble the list of mounts which are involved in the operation
// and use the util.go function for generating all the needed information
mounts := []string{}
nodes := []*ArchiveReader{}
childWalker := func(prefix patricia.Prefix, item patricia.Item) error {
if _, ok := item.(*ArchiveReader); !ok {
return fmt.Errorf("item not ArchiveReader")
}
ar, _ := item.(*ArchiveReader)
mounts = append(mounts, ar.mountPoint.Destination)
nodes = append(nodes, ar)
return nil
}
// prefix = current node's mount path
nodePrefix := patricia.Prefix(path)
err := rm.prefixTrie.VisitSubtree(nodePrefix, childWalker)
if err != nil {
msg := fmt.Sprintf("Failed to build exclusion filter for %s: %s", path, err.Error())
log.Error(msg)
return nil, nil, fmt.Errorf(msg)
}
return mounts, nodes, nil
}

View File

@@ -0,0 +1,341 @@
// 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 backends
import (
"context"
"testing"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/pkg/trace"
)
type MockCopyToData struct {
containerDestPath string
tarAssetName string
expectedPrefix string
}
type ReaderFilters struct {
rebase string
strip string
exclude []string
include string
}
type MockCopyFromData struct {
containerSourcePath string
expectedPrefices []string
expectedFilterSpecs map[string]ReaderFilters
}
func TestFindArchiveWriter(t *testing.T) {
mounts := []types.MountPoint{
{Name: "volA", Destination: "/mnt/A"},
{Name: "volAB", Destination: "/mnt/A/AB"},
{Name: "volB", Destination: "/mnt/B"},
{Name: "R/W", Destination: "/"},
}
mockData := []MockCopyToData{
// mock data for tar asset as a file and container dest path including a mount point
{
containerDestPath: "/mnt/A/",
tarAssetName: "file.txt",
expectedPrefix: "/mnt/A",
},
{
containerDestPath: "/mnt/A/AB",
tarAssetName: "file.txt",
expectedPrefix: "/mnt/A/AB",
},
// mock data for tar asset containing a mount point and the container dest path as /
{
containerDestPath: "/",
tarAssetName: "mnt/A/file.txt",
expectedPrefix: "/mnt/A",
},
{
containerDestPath: "/",
tarAssetName: "mnt/A/AB/file.txt",
expectedPrefix: "/mnt/A/AB",
},
// mock data for cases that do not involve mount points
{
containerDestPath: "/",
tarAssetName: "test/file.txt",
expectedPrefix: "/",
},
}
for _, data := range mockData {
op := trace.NewOperation(context.Background(), "")
writerMap := NewArchiveStreamWriterMap(op, mounts, data.containerDestPath)
aw, err := writerMap.FindArchiveWriter(data.containerDestPath, data.tarAssetName)
assert.Nil(t, err, "Expected success from finding archive writer for container dest %s and tar asset path %s", data.containerDestPath, data.tarAssetName)
assert.NotNil(t, aw, "Expected non-nil archive writer")
if aw != nil {
assert.Contains(t, aw.mountPoint.Destination, data.expectedPrefix,
"Expected to find prefix %s for container dest %s and tar asset path %s",
data.expectedPrefix, data.containerDestPath, data.tarAssetName)
}
}
}
func TestFindArchiveReaders(t *testing.T) {
mounts := []types.MountPoint{
{Name: "volA", Destination: "/mnt/A"}, //mount point
{Name: "volAB", Destination: "/mnt/A/AB"}, //mount point
{Name: "volB", Destination: "/mnt/B"}, //mount point
{Name: "R/W", Destination: "/"}, //container base volume
}
mockData := []MockCopyFromData{
// case 1: Get all mount prefix
{
containerSourcePath: "/",
expectedPrefices: []string{"/", "/mnt/A", "/mnt/B", "/mnt/A/AB"},
expectedFilterSpecs: map[string]ReaderFilters{
"/": {
rebase: "",
strip: "",
exclude: []string{"mnt/A/", "mnt/B/", "mnt/A/AB/"},
include: "",
},
"/mnt/A": {
rebase: "mnt/A",
strip: "",
exclude: []string{"AB/"},
include: "",
},
"/mnt/B": {
rebase: "mnt/B",
strip: "",
exclude: []string{},
include: "",
},
"/mnt/A/AB": {
rebase: "mnt/A/AB",
strip: "",
exclude: []string{},
include: "",
},
},
},
{
containerSourcePath: "/mnt",
expectedPrefices: []string{"/", "/mnt/A", "/mnt/B", "/mnt/A/AB"},
expectedFilterSpecs: map[string]ReaderFilters{
"/": {
rebase: "mnt",
strip: "mnt",
exclude: []string{"mnt/A/", "mnt/B/", "mnt/A/AB/"},
include: "",
},
"/mnt/A": {
rebase: "mnt/A",
strip: "",
exclude: []string{"AB/"},
include: "",
},
"/mnt/B": {
rebase: "mnt/B",
strip: "",
exclude: []string{},
include: "",
},
"/mnt/A/AB": {
rebase: "mnt/A/AB",
strip: "",
exclude: []string{},
include: "",
},
},
},
{
containerSourcePath: "/mnt/",
expectedPrefices: []string{"/", "/mnt/A", "/mnt/B", "/mnt/A/AB"},
expectedFilterSpecs: map[string]ReaderFilters{
"/": {
rebase: "mnt",
strip: "mnt",
exclude: []string{"mnt/A/", "mnt/B/", "mnt/A/AB/"},
include: "",
},
"/mnt/A": {
rebase: "mnt/A",
strip: "",
exclude: []string{"AB/"},
include: "",
},
"/mnt/B": {
rebase: "mnt/B",
strip: "",
exclude: []string{},
include: "",
},
"/mnt/A/AB": {
rebase: "mnt/A/AB",
strip: "",
exclude: []string{},
include: "",
},
},
},
// case 2: Do not include /mnt/B
{
containerSourcePath: "/mnt/A",
expectedPrefices: []string{"/", "/mnt/A", "/mnt/A/AB"},
expectedFilterSpecs: map[string]ReaderFilters{
"/": {
rebase: "A",
strip: "mnt/A",
exclude: []string{"mnt/A/", "mnt/A/AB/"},
include: "/mnt/A",
},
"/mnt/A": {
rebase: "A",
strip: "",
exclude: []string{"AB/"},
},
"/mnt/A/AB": {
rebase: "A/AB",
exclude: []string{},
include: "",
},
},
},
// case 3: Return only the container base "/"
{
containerSourcePath: "/mnt/not-a-mount",
expectedPrefices: []string{"/"},
expectedFilterSpecs: map[string]ReaderFilters{
"/": {
rebase: "not-a-mount",
strip: "mnt/not-a-mount",
exclude: []string{""},
include: "mnt/not-a-mount",
},
},
},
{
containerSourcePath: "/etc/",
expectedPrefices: []string{"/"},
expectedFilterSpecs: map[string]ReaderFilters{
"/": {
rebase: "etc",
strip: "etc",
exclude: []string{""},
include: "etc",
},
},
},
// case 4: Check inclusion filter
{
containerSourcePath: "/mnt/A/a/file.txt",
expectedPrefices: []string{"/mnt/A"},
expectedFilterSpecs: map[string]ReaderFilters{
"/mnt/A": {
rebase: "file.txt",
strip: "a/file.txt",
exclude: []string{""},
include: "a/file.txt",
},
},
},
}
for i, data := range mockData {
op := trace.NewOperation(context.Background(), "")
readerMap := NewArchiveStreamReaderMap(op, mounts, data.containerSourcePath)
archiveReaders, err := readerMap.FindArchiveReaders(data.containerSourcePath)
assert.Nil(t, err, "Expected success from finding archive readers for container source %s", data.containerSourcePath)
assert.NotNil(t, archiveReaders, "Expected an array of archive readers but got nil for container source path %s", data.containerSourcePath)
assert.NotEmpty(t, archiveReaders, "Expected an array of archive readers %s with more than one items", data.containerSourcePath)
log.Debugf("Data = %#v", data)
pa := PrefixArray(archiveReaders)
nonOverlap := UnionMinusIntersection(pa, data.expectedPrefices)
assert.Empty(t, nonOverlap, "Found mismatch in the prefix array and expected array for source path %s. Non-overlapped result = %#v", data.containerSourcePath, nonOverlap)
// Check filter spec
for _, ar := range archiveReaders {
currPath := ar.mountPoint.Destination
assert.Equal(t, data.expectedFilterSpecs[currPath].rebase, ar.filterSpec.RebasePath, "rebase filterspec not correct")
assert.Equal(t, data.expectedFilterSpecs[currPath].strip, ar.filterSpec.StripPath, "strip filterspec not correct")
for _, ex := range data.expectedFilterSpecs[currPath].exclude {
_, ok := ar.filterSpec.Exclusions[ex]
assert.True(t, ok, "Did not find %s in exclusion map for reader %s in mock #%d", ex, currPath, i)
}
}
// Check inclusion filter
if len(archiveReaders) == 1 {
ar := archiveReaders[0]
currPath := ar.mountPoint.Destination
expectedInclusion := data.expectedFilterSpecs[currPath].include
assert.Len(t, ar.filterSpec.Inclusions, 1, "Expected only 1 inclusion filter for %s but got %d in mock #%d", data.containerSourcePath, len(ar.filterSpec.Inclusions), i)
if len(ar.filterSpec.Inclusions) == 1 {
_, ok := ar.filterSpec.Inclusions[expectedInclusion]
assert.True(t, ok, "Expected inclusion filter to contain %s in mock #%d", expectedInclusion, i)
// Sanity check to make sure include isn't in exclusion. This should never happen.
for _, ex := range data.expectedFilterSpecs[currPath].exclude {
assert.NotEqual(t, expectedInclusion, ex, "Expected inclusion %s not to be in exclusion list %#v in mock #%d", expectedInclusion, ex, i)
}
}
}
}
}
func PrefixArray(readers []*ArchiveReader) (pa []string) {
for _, reader := range readers {
pa = append(pa, reader.mountPoint.Destination)
}
log.Debugf("prefix array - %#v", pa)
return
}
func UnionMinusIntersection(A, B []string) (res []string) {
test := make(map[string]bool)
log.Debugf("Looking for non overlapping in array A-%#v and array B-%#v", A, B)
for _, data := range A {
test[data] = true
}
for _, data := range B {
if _, ok := test[data]; ok {
delete(test, data)
} else {
res = append(res, data)
}
}
for key := range test {
res = append(res, key)
}
log.Debugf("Resulting non overlapped array - %#v", res)
return
}

View File

@@ -0,0 +1,543 @@
// 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 backends
import (
"context"
"crypto/x509"
"fmt"
"net"
"net/url"
"strings"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/events"
"github.com/go-openapi/runtime"
rc "github.com/go-openapi/runtime/client"
"github.com/go-openapi/swag"
"golang.org/x/sync/singleflight"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
"github.com/vmware/vic/lib/apiservers/engine/backends/container"
"github.com/vmware/vic/lib/apiservers/engine/network"
"github.com/vmware/vic/lib/apiservers/engine/proxy"
apiclient "github.com/vmware/vic/lib/apiservers/portlayer/client"
"github.com/vmware/vic/lib/apiservers/portlayer/client/containers"
"github.com/vmware/vic/lib/apiservers/portlayer/client/misc"
"github.com/vmware/vic/lib/apiservers/portlayer/client/scopes"
"github.com/vmware/vic/lib/apiservers/portlayer/client/storage"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
"github.com/vmware/vic/lib/config"
"github.com/vmware/vic/lib/config/dynamic"
"github.com/vmware/vic/lib/config/dynamic/admiral"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/imagec"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/registry"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/sys"
)
const (
PortlayerName = "Backend Engine"
// RetryTimeSeconds defines how many seconds to wait between retries
RetryTimeSeconds = 2
defaultSessionKeepAlive = 20 * time.Second
APITimeout = constants.PropertyCollectorTimeout + 3*time.Second
)
var (
portLayerClient *apiclient.PortLayer
portLayerServerAddr string
portLayerName string
productName string
productVersion string
vchConfig *dynConfig
RegistryCertPool *x509.CertPool
archiveProxy proxy.VicArchiveProxy
eventService *events.Events
servicePort uint
)
type dynConfig struct {
sync.Mutex
Cfg *config.VirtualContainerHostConfigSpec
src dynamic.Source
merger dynamic.Merger
sess *session.Session
Whitelist, Blacklist, Insecure registry.Set
remoteWl bool
group singleflight.Group
lastCfg *dynConfig
}
func Init(portLayerAddr, product string, port uint, config *config.VirtualContainerHostConfigSpec) error {
servicePort = port
_, _, err := net.SplitHostPort(portLayerAddr)
if err != nil {
return err
}
if config == nil {
return fmt.Errorf("docker API server requires VCH config")
}
productName = product
if config.Version != nil {
productVersion = config.Version.ShortVersion()
}
if productVersion == "" {
portLayerName = product + " Backend Engine"
} else {
portLayerName = product + " " + productVersion + " Backend Engine"
}
if vchConfig, err = newDynConfig(ctx, config); err != nil {
return err
}
loadRegistryCACerts()
t := rc.New(portLayerAddr, "/", []string{"http"})
t.Consumers["application/x-tar"] = runtime.ByteStreamConsumer()
t.Consumers["application/octet-stream"] = runtime.ByteStreamConsumer()
t.Producers["application/x-tar"] = runtime.ByteStreamProducer()
t.Producers["application/octet-stream"] = runtime.ByteStreamProducer()
portLayerClient = apiclient.New(t, nil)
portLayerServerAddr = portLayerAddr
log.Infof("*** Portlayer Address = %s", portLayerAddr)
// block indefinitely while waiting on the portlayer to respond to pings
// the vic-machine installer timeout will intervene if this blocks for too long
pingPortLayer()
if err := hydrateCaches(); err != nil {
return err
}
log.Info("Creating image store")
if err := createImageStore(); err != nil {
log.Errorf("Failed to create image store")
return err
}
archiveProxy = proxy.NewArchiveProxy(portLayerClient)
eventService = events.New()
return nil
}
func hydrateCaches() error {
const waiters = 3
wg := sync.WaitGroup{}
wg.Add(waiters)
errChan := make(chan error, waiters)
go func() {
defer wg.Done()
if err := imagec.InitializeLayerCache(portLayerClient); err != nil {
errChan <- fmt.Errorf("Failed to initialize layer cache: %s", err)
return
}
log.Info("Layer cache initialized successfully")
errChan <- nil
}()
go func() {
defer wg.Done()
if err := cache.InitializeImageCache(portLayerClient); err != nil {
errChan <- fmt.Errorf("Failed to initialize image cache: %s", err)
return
}
log.Info("Image cache initialized successfully")
// container cache relies on image cache so we share a goroutine to update
// them serially
if err := syncContainerCache(); err != nil {
errChan <- fmt.Errorf("Failed to update container cache: %s", err)
return
}
log.Info("Container cache updated successfully")
errChan <- nil
}()
go func() {
log.Info("Refreshing repository cache")
defer wg.Done()
if err := cache.NewRepositoryCache(portLayerClient); err != nil {
errChan <- fmt.Errorf("Failed to create repository cache: %s", err.Error())
return
}
errChan <- nil
log.Info("Repository cache updated successfully")
}()
wg.Wait()
close(errChan)
var errs []string
for err := range errChan {
if err != nil {
// accumulate all errors into one
errs = append(errs, err.Error())
}
}
var e error
if len(errs) > 0 {
e = fmt.Errorf(strings.Join(errs, ", "))
}
if e != nil {
log.Errorf("Errors occurred during cache hydration at VCH start: %s", e)
}
return e
}
func PortLayerClient() *apiclient.PortLayer {
return portLayerClient
}
func PortLayerServer() string {
return portLayerServerAddr
}
func PortLayerName() string {
return portLayerName
}
func ProductName() string {
return productName
}
func ProductVersion() string {
return productVersion
}
func pingPortLayer() {
ticker := time.NewTicker(RetryTimeSeconds * time.Second)
defer ticker.Stop()
params := misc.NewPingParamsWithContext(context.TODO())
log.Infof("Waiting for portlayer to come up")
for range ticker.C {
if _, err := portLayerClient.Misc.Ping(params); err == nil {
log.Info("Portlayer is up and responding to pings")
return
}
}
}
func createImageStore() error {
// TODO(jzt): we should move this to a utility package or something
host, err := sys.UUID()
if err != nil {
log.Errorf("Failed to determine host UUID")
return err
}
log.Infof("*** UUID = %s", host)
// attempt to create the image store if it doesn't exist
store := &models.ImageStore{Name: host}
_, err = portLayerClient.Storage.CreateImageStore(
storage.NewCreateImageStoreParamsWithContext(ctx).WithBody(store),
)
if err != nil {
if _, ok := err.(*storage.CreateImageStoreConflict); ok {
log.Debugf("Store already exists")
return nil
}
return err
}
log.Infof("Image store created successfully")
return nil
}
// syncContainerCache runs once at startup to populate the container cache
func syncContainerCache() error {
log.Debugf("Updating container cache")
backend := NewContainerBackend()
client := PortLayerClient()
reqParams := containers.NewGetContainerListParamsWithContext(ctx).WithAll(swag.Bool(true))
containme, err := client.Containers.GetContainerList(reqParams)
if err != nil {
return errors.Errorf("Failed to retrieve container list from portlayer: %s", err)
}
log.Debugf("Found %d containers", len(containme.Payload))
cc := cache.ContainerCache()
var errs []string
for _, info := range containme.Payload {
container := proxy.ContainerInfoToVicContainer(*info, portLayerName)
cc.AddContainer(container)
if err = setPortMapping(info, backend, container); err != nil {
errs = append(errs, err.Error())
}
}
if len(errs) > 0 {
return errors.Errorf("Failed to set port mapping: %s", strings.Join(errs, "\n"))
}
return nil
}
func setPortMapping(info *models.ContainerInfo, backend *ContainerBackend, container *container.VicContainer) error {
if info.ContainerConfig.State == "" {
log.Infof("container state is nil")
return nil
}
if info.ContainerConfig.State != "Running" || len(container.HostConfig.PortBindings) == 0 {
log.Infof("No need to restore port bindings, state: %s, portbinding: %+v", info.ContainerConfig.State, container.HostConfig.PortBindings)
return nil
}
log.Debugf("Set port mapping for container %q, portmapping %+v", container.Name, container.HostConfig.PortBindings)
client := PortLayerClient()
endpointsOK, err := client.Scopes.GetContainerEndpoints(
scopes.NewGetContainerEndpointsParamsWithContext(ctx).WithHandleOrID(container.ContainerID))
if err != nil {
return err
}
for _, e := range endpointsOK.Payload {
if len(e.Ports) > 0 && e.Scope == constants.BridgeScopeType {
if err = network.MapPorts(container, e, container.ContainerID); err != nil {
log.Errorf(err.Error())
return err
}
}
}
return nil
}
func loadRegistryCACerts() {
var err error
RegistryCertPool, err = x509.SystemCertPool()
log.Debugf("Loaded %d CAs for registries from system CA bundle", len(RegistryCertPool.Subjects()))
if err != nil {
log.Errorf("Unable to load system CAs")
return
}
vchConfig.Lock()
defer vchConfig.Unlock()
if !RegistryCertPool.AppendCertsFromPEM(vchConfig.Cfg.RegistryCertificateAuthorities) {
log.Errorf("Unable to load CAs for registry access in config")
return
}
log.Debugf("Loaded %d CAs for registries from config", len(RegistryCertPool.Subjects()))
}
func EventService() *events.Events {
return eventService
}
// RegistryCheck checkes the given url against the registry whitelist, blacklist, and insecure
// registries lists. It returns true for each list where u matches that list.
func (d *dynConfig) RegistryCheck(ctx context.Context, u *url.URL) (wl bool, bl bool, insecure bool) {
m := d.update(ctx)
us := u.String()
wl = len(m.Whitelist) == 0 || m.Whitelist.Match(us)
bl = len(m.Blacklist) == 0 || !m.Blacklist.Match(us)
insecure = m.Insecure.Match(us)
return
}
func (d *dynConfig) update(ctx context.Context) *dynConfig {
const key = "RegistryCheck"
resCh := d.group.DoChan(key, func() (interface{}, error) {
d.Lock()
src := d.src
d.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
c, err := src.Get(ctx)
if err != nil {
log.Warnf("error getting config from source: %s", err)
}
d.Lock()
defer d.Unlock()
m := d
if c != nil {
// update config
if m, err = d.merged(c); err != nil {
log.Errorf("error updating config: %s", err)
m = d
} else {
if len(c.RegistryWhitelist) > 0 {
m.remoteWl = true
}
}
} else if err == nil && src == d.src {
// err == nil and c == nil, which
// indicates no remote sources
// were found, try resetting the
// source for next time
if err := d.resetSrc(); err != nil {
log.Warnf("could not reset config source: %s", err)
}
}
d.lastCfg = m
return m, nil
})
select {
case res := <-resCh:
return res.Val.(*dynConfig)
case <-ctx.Done():
return func() *dynConfig {
d.Lock()
defer d.Unlock()
if d.lastCfg == nil {
return d
}
return d.lastCfg
}()
}
}
func (d *dynConfig) resetSrc() error {
ep, err := d.clientEndpoint()
if err != nil {
return err
}
d.src = admiral.NewSource(d.sess, ep.String())
return nil
}
func newDynConfig(ctx context.Context, c *config.VirtualContainerHostConfigSpec) (*dynConfig, error) {
d := &dynConfig{
Cfg: c,
}
var err error
if d.Insecure, err = dynamic.ParseRegistries(c.InsecureRegistries); err != nil {
return nil, err
}
if d.Whitelist, err = dynamic.ParseRegistries(c.RegistryWhitelist); err != nil {
return nil, err
}
if d.sess, err = newSession(ctx, c); err != nil {
return nil, err
}
d.merger = dynamic.NewMerger()
if err := d.resetSrc(); err != nil {
return nil, err
}
return d, nil
}
// update merges another config into this config. d should be locked before
// calling this.
func (d *dynConfig) merged(c *config.VirtualContainerHostConfigSpec) (*dynConfig, error) {
if c == nil {
return d, nil
}
newcfg, err := d.merger.Merge(d.Cfg, c)
if err != nil {
return nil, err
}
var wl, bl, insecure registry.Set
if wl, err = dynamic.ParseRegistries(newcfg.RegistryWhitelist); err != nil {
return nil, err
}
if bl, err = dynamic.ParseRegistries(newcfg.RegistryBlacklist); err != nil {
return nil, err
}
if insecure, err = dynamic.ParseRegistries(newcfg.InsecureRegistries); err != nil {
return nil, err
}
return &dynConfig{
Whitelist: wl,
Blacklist: bl,
Insecure: insecure,
Cfg: newcfg,
src: d.src,
}, nil
}
func (d *dynConfig) clientEndpoint() (*url.URL, error) {
ips, err := net.LookupIP("client.localhost")
if err != nil {
return nil, err
}
scheme := "https"
if d.Cfg.HostCertificate.IsNil() {
scheme = "http"
}
return url.Parse(fmt.Sprintf("%s://%s:%d", scheme, ips[0], servicePort))
}
func newSession(ctx context.Context, config *config.VirtualContainerHostConfigSpec) (*session.Session, error) {
// strip the path off of the target url since it may contain the
// datacenter
u, err := url.Parse(config.Target)
if err != nil {
return nil, err
}
u.Path = ""
sessCfg := &session.Config{
Service: u.String(),
User: url.UserPassword(config.Username, config.Token),
Thumbprint: config.TargetThumbprint,
Keepalive: defaultSessionKeepAlive,
}
sess := session.NewSession(sessCfg)
if sess.Connect(ctx); err != nil {
return nil, err
}
return sess, nil
}

View File

@@ -0,0 +1,32 @@
// 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 backends
import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
"golang.org/x/net/context"
"github.com/vmware/vic/lib/apiservers/engine/errors"
)
type Builder struct {
}
func (b *Builder) BuildFromContext(ctx context.Context, src io.ReadCloser, remote string, buildOptions *types.ImageBuildOptions, pg backend.ProgressWriter) (string, error) {
return "", errors.APINotSupportedMsg(ProductName(), "BuildFromContext")
}

View File

@@ -0,0 +1,199 @@
// 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 cache
import (
"fmt"
"sync"
log "github.com/Sirupsen/logrus"
derr "github.com/docker/docker/api/errors"
"github.com/docker/docker/pkg/truncindex"
"github.com/vmware/vic/lib/apiservers/engine/backends/container"
)
// Tracks our container info from calls
type CCache struct {
m sync.RWMutex
idIndex *truncindex.TruncIndex
containersByID map[string]*container.VicContainer
containersByName map[string]*container.VicContainer
containersByExecID map[string]*container.VicContainer
}
var containerCache *CCache
func init() {
containerCache = &CCache{
idIndex: truncindex.NewTruncIndex([]string{}),
containersByID: make(map[string]*container.VicContainer),
containersByName: make(map[string]*container.VicContainer),
containersByExecID: make(map[string]*container.VicContainer),
}
}
// ContainerCache returns a reference to the container cache
func ContainerCache() *CCache {
return containerCache
}
func (cc *CCache) getContainerByName(nameOnly string) *container.VicContainer {
if container, exist := cc.containersByName[nameOnly]; exist {
return container
}
return nil
}
func (cc *CCache) getContainer(nameOrID string) *container.VicContainer {
// full name matching should take precedence over id prefix matching
if container, exist := cc.containersByName[nameOrID]; exist {
return container
}
// get the full ID if we only have a prefix
if cid, err := cc.idIndex.Get(nameOrID); err == nil {
nameOrID = cid
}
if container, exist := cc.containersByID[nameOrID]; exist {
return container
}
return nil
}
// GetContainerByName returns a container whose name "exactly" matches nameOnly
func (cc *CCache) GetContainerByName(nameOnly string) *container.VicContainer {
cc.m.RLock()
defer cc.m.RUnlock()
return cc.getContainerByName(nameOnly)
}
func (cc *CCache) GetContainer(nameOrID string) *container.VicContainer {
cc.m.RLock()
defer cc.m.RUnlock()
return cc.getContainer(nameOrID)
}
func (cc *CCache) AddContainer(container *container.VicContainer) {
cc.m.Lock()
defer cc.m.Unlock()
// TODO(jzt): this probably shouldn't assume a valid container ID
if err := cc.idIndex.Add(container.ContainerID); err != nil {
log.Warnf("Error adding ID into index: %s", err)
}
cc.containersByID[container.ContainerID] = container
cc.containersByName[container.Name] = container
}
func (cc *CCache) DeleteContainer(nameOrID string) {
cc.m.Lock()
defer cc.m.Unlock()
container := cc.getContainer(nameOrID)
if container == nil {
return
}
delete(cc.containersByID, container.ContainerID)
delete(cc.containersByName, container.Name)
if err := cc.idIndex.Delete(container.ContainerID); err != nil {
log.Warnf("Error deleting ID from index: %s", err)
}
// remove exec references
for _, id := range container.List() {
container.Delete(id)
}
}
func (cc *CCache) AddExecToContainer(container *container.VicContainer, eid string) {
cc.m.Lock()
defer cc.m.Unlock()
// ignore if we already have it
if _, ok := cc.containersByExecID[eid]; ok {
return
}
container.Add(eid)
cc.containersByExecID[eid] = container
}
func (cc *CCache) GetContainerFromExec(eid string) *container.VicContainer {
cc.m.RLock()
defer cc.m.RUnlock()
if container, exist := cc.containersByExecID[eid]; exist {
return container
}
return nil
}
// UpdateContainerName assumes that the newName is already reserved by ReserveName
// so no need to check the existence of a container with the new name.
func (cc *CCache) UpdateContainerName(oldName, newName string) error {
cc.m.Lock()
defer cc.m.Unlock()
container := cc.getContainer(oldName)
if container == nil {
return derr.NewRequestNotFoundError(fmt.Errorf("no such container: %s", oldName))
}
delete(cc.containersByName, container.Name)
container.Name = newName
cc.containersByName[newName] = container
cc.containersByID[container.ContainerID] = container
return nil
}
// ReserveName is used during a container create/rename operation to prevent concurrent
// container create/rename operations from grabbing the new name.
func (cc *CCache) ReserveName(container *container.VicContainer, name string) error {
cc.m.Lock()
defer cc.m.Unlock()
if cont, exist := cc.containersByName[name]; exist {
return fmt.Errorf("conflict. The name %q is already in use by container %s. You have to remove (or rename) that container to be able to re use that name.", name, cont.ContainerID)
}
cc.containersByName[name] = container
return nil
}
// ReleaseName is used during a container rename operation to allow concurrent container
// create/rename operations to use the name. It is also used during a failed create
// operation to allow subsequent create operations to use that name.
func (cc *CCache) ReleaseName(name string) {
cc.m.Lock()
defer cc.m.Unlock()
if _, exist := cc.containersByName[name]; !exist {
log.Errorf("ReleaseName error: Name %s not found", name)
return
}
delete(cc.containersByName, name)
}

View File

@@ -0,0 +1,353 @@
// Copyright 2016-2018 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 cache
import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/docker/distribution/digest"
derr "github.com/docker/docker/api/errors"
"github.com/docker/docker/pkg/truncindex"
"github.com/docker/docker/reference"
"github.com/vmware/vic/lib/apiservers/engine/backends/kv"
"github.com/vmware/vic/lib/apiservers/portlayer/client"
"github.com/vmware/vic/lib/metadata"
"github.com/vmware/vic/pkg/trace"
)
// ICache is an in-memory cache of image metadata. It is refreshed at startup
// by a call to the portlayer. It is updated when new images are pulled or
// images are deleted.
type ICache struct {
m sync.RWMutex
iDIndex *truncindex.TruncIndex
cacheByID map[string]*metadata.ImageConfig
cacheByName map[string]*metadata.ImageConfig
dirty bool
client *client.PortLayer
}
const (
imageCacheKey = "images"
)
var (
imageCache *ICache
)
// byCreated is a temporary type used to sort a list of images by creation
// time.
type byCreated []*metadata.ImageConfig
func (r byCreated) Len() int { return len(r) }
func (r byCreated) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byCreated) Less(i, j int) bool { return r[i].Created.Unix() < r[j].Created.Unix() }
func init() {
imageCache = &ICache{
iDIndex: truncindex.NewTruncIndex(nil),
cacheByID: make(map[string]*metadata.ImageConfig),
cacheByName: make(map[string]*metadata.ImageConfig),
}
}
// ImageCache returns a reference to the image cache
func ImageCache() *ICache {
return imageCache
}
// InitializeImageCache will create a new image cache or rehydrate an
// existing image cache from the portlayer k/v store
func InitializeImageCache(client *client.PortLayer) error {
defer trace.End(trace.Begin(""))
imageCache.client = client
log.Debugf("Initializing image cache")
val, err := kv.Get(client, imageCacheKey)
if err != nil && err != kv.ErrKeyNotFound {
return err
}
i := struct {
IDIndex *truncindex.TruncIndex
CacheByID map[string]*metadata.ImageConfig
CacheByName map[string]*metadata.ImageConfig
}{}
if val != "" {
if err = json.Unmarshal([]byte(val), &i); err != nil {
return fmt.Errorf("Failed to unmarshal image cache: %s", err)
}
// populate the trie with IDs
for k := range i.CacheByID {
// Separate out the hash prefix from the CacheByID key before indexing iDIndex
// as it is keyed by the full image ID without the hash prefix.
fields := strings.SplitN(k, ":", 2)
if len(fields) == 2 {
k = fields[1]
}
imageCache.iDIndex.Add(k)
}
imageCache.cacheByID = i.CacheByID
imageCache.cacheByName = i.CacheByName
}
return nil
}
// GetImages returns a slice containing metadata for all cached images
func (ic *ICache) GetImages() []*metadata.ImageConfig {
defer trace.End(trace.Begin(""))
ic.m.RLock()
defer ic.m.RUnlock()
result := make([]*metadata.ImageConfig, 0, len(ic.cacheByID))
for _, image := range ic.cacheByID {
result = append(result, copyImageConfig(image))
}
sort.Sort(sort.Reverse(byCreated(result)))
return result
}
// IsImageID will check that a full or partial imageID
// exists in the cache
func (ic *ICache) IsImageID(id string) bool {
ic.m.RLock()
defer ic.m.RUnlock()
if _, err := ic.iDIndex.Get(id); err == nil {
return true
}
return false
}
// Get parses input to retrieve a cached image
func (ic *ICache) Get(idOrRef string) (*metadata.ImageConfig, error) {
defer trace.End(trace.Begin(idOrRef))
ic.m.RLock()
defer ic.m.RUnlock()
// cover the case of creating by a full reference
if config, ok := ic.cacheByName[idOrRef]; ok {
return copyImageConfig(config), nil
}
// get the full image ID if supplied a prefix
if id, err := ic.iDIndex.Get(idOrRef); err == nil {
idOrRef = id
}
imgDigest, named, err := reference.ParseIDOrReference(idOrRef)
if err != nil {
return nil, err
}
var config *metadata.ImageConfig
if imgDigest != "" {
config = ic.getImageByDigest(imgDigest)
} else {
config = ic.getImageByNamed(named)
}
if config == nil {
// docker automatically prints out ":latest" tag if not specified in case if image is not found.
postfixLatest := ""
if !strings.Contains(idOrRef, ":") {
postfixLatest += ":" + reference.DefaultTag
}
return nil, derr.NewRequestNotFoundError(fmt.Errorf(
"No such image: %s%s", idOrRef, postfixLatest))
}
return copyImageConfig(config), nil
}
func (ic *ICache) getImageByDigest(digest digest.Digest) *metadata.ImageConfig {
defer trace.End(trace.Begin(digest.String()))
var config *metadata.ImageConfig
config, ok := ic.cacheByID[string(digest)]
if !ok {
return nil
}
return copyImageConfig(config)
}
// Looks up image by reference.Named
func (ic *ICache) getImageByNamed(named reference.Named) *metadata.ImageConfig {
defer trace.End(trace.Begin(""))
// get the imageID from the repoCache
// #nosec: Errors unhandled.
id, _ := RepositoryCache().Get(named)
return copyImageConfig(ic.cacheByID[prefixImageID(id)])
}
// Add the default "sha256:" prefix to the image ID if it doesn't include a hash
// prefix. Don't assume the image ID has "<hash>:<id> as format (e.g. "sha256:<id>").
// We store it in this format to make it easier to lookup by digest.
func prefixImageID(imageID string) string {
if strings.Contains(imageID, ":") {
return imageID
}
return digest.Canonical.String() + ":" + imageID
}
// Add adds an image to the image cache
func (ic *ICache) Add(imageConfig *metadata.ImageConfig) error {
defer trace.End(trace.Begin(""))
ic.m.Lock()
defer ic.m.Unlock()
imageID := prefixImageID(imageConfig.ImageID)
err := ic.iDIndex.Add(imageConfig.ImageID)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("error adding image %s to index: %s", imageID, err)
}
err = nil
ic.cacheByID[imageID] = imageConfig
ic.dirty = true
if imageConfig.Name == "" {
log.Debugf("Image %s has no name", imageID)
return nil
}
// Construct a reference after the image is added into cacheByID so that an image
// without a name can at least be added to cacheByID.
// Normalize the name stored in imageConfig using Docker's reference code
ref, err := reference.WithName(imageConfig.Name)
if err != nil {
return fmt.Errorf("error trying to create reference from %s: %s", imageConfig.Name, err)
}
for _, tag := range imageConfig.Tags {
ref, err = reference.WithTag(ref, tag)
if err != nil {
return fmt.Errorf("error trying to create tagged reference from %s and tag %s: %s", imageConfig.Name, tag, err)
}
ic.cacheByName[imageConfig.Reference] = imageConfig
}
return nil
}
// RemoveImageByConfig removes image from the cache.
func (ic *ICache) RemoveImageByConfig(imageConfig *metadata.ImageConfig) {
defer trace.End(trace.Begin(""))
ic.m.Lock()
defer ic.m.Unlock()
// If we get here we definitely want to remove image config from any data structure
// where it can be present. So that, if there is something is wrong
// it could be tracked on debug level.
if err := ic.iDIndex.Delete(imageConfig.ImageID); err != nil {
log.Debugf("Not found in image cache index: %v", err)
}
prefixedID := prefixImageID(imageConfig.ImageID)
delete(ic.cacheByID, prefixedID)
delete(ic.cacheByName, imageConfig.Reference)
ic.dirty = true
}
// Save will persist the image cache to the portlayer k/v store
func (ic *ICache) Save() error {
defer trace.End(trace.Begin(""))
ic.m.Lock()
defer ic.m.Unlock()
if !ic.dirty {
return nil
}
m := struct {
IDIndex *truncindex.TruncIndex
CacheByID map[string]*metadata.ImageConfig
CacheByName map[string]*metadata.ImageConfig
}{
ic.iDIndex,
ic.cacheByID,
ic.cacheByName,
}
bytes, err := json.Marshal(m)
if err != nil {
log.Errorf("Unable to marshal image cache: %s", err.Error())
return err
}
err = kv.Put(ic.client, imageCacheKey, string(bytes))
if err != nil {
log.Errorf("Unable to save image cache: %s", err.Error())
return err
}
ic.dirty = false
return nil
}
// copyImageConfig performs and returns deep copy of an ImageConfig struct
func copyImageConfig(image *metadata.ImageConfig) *metadata.ImageConfig {
if image == nil {
return nil
}
// copy everything
newImage := *image
// replace the pointer to metadata.ImageConfig.Config and copy the contents
newConfig := *image.Config
newImage.Config = &newConfig
// get tags and digests from repo
tags := RepositoryCache().Tags(newImage.ImageID)
digests := RepositoryCache().Digests(newImage.ImageID)
// if image has neither then set <none> vals
if len(tags) == 0 && len(digests) == 0 {
tags = append(tags, "<none>:<none>")
digests = append(digests, "<none>@<none>")
}
newImage.Tags = tags
if digests != nil {
newImage.Digests = digests
}
return &newImage
}

View File

@@ -0,0 +1,413 @@
// 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 cache
import (
"encoding/json"
"errors"
"fmt"
"sort"
"sync"
"github.com/vmware/vic/lib/apiservers/engine/backends/kv"
"github.com/vmware/vic/lib/apiservers/portlayer/client"
"github.com/docker/distribution/digest"
"github.com/docker/docker/reference"
log "github.com/Sirupsen/logrus"
)
// repoCache is a cache of the docker repository information.
// This info will help to provide proper tag and digest support
//
// The cache will be persisted to disk via the portlayer k/v
// store and will be restored at system start
//
// This code is a heavy leverage of docker's reference store:
// github.com/docker/docker/reference/store.go
var (
rCache *repoCache
repoKey = "repositories"
)
// Repo provides the set of methods which can operate on a tag store.
type Repo interface {
References(imageID string) []reference.Named
ReferencesByName(ref reference.Named) []Association
Delete(ref reference.Named, save bool) (bool, error)
Get(ref reference.Named) (string, error)
Save() error
GetImageID(layerID string) string
Tags(imageID string) []string
Digests(imageID string) []string
AddReference(ref reference.Named, imageID string, force bool, layerID string, save bool) error
// Remove will remove from the cache and returns the
// stringified Named if successful -- save bool instructs
// func to persist to portlayer k/v or not
Remove(ref string, save bool) (string, error)
}
type repoCache struct {
// client is needed for k/v store operations
client *client.PortLayer
mu sync.RWMutex
// repositories is a map of repositories, indexed by name.
Repositories map[string]repository
// referencesByIDCache is a cache of references indexed by imageID
referencesByIDCache map[string]map[string]reference.Named
// Layers is a map of layerIDs to imageIDs
// TODO: we might be able to remove this later -- currently
// needed because an ImageID isn't generated for every pull
Layers map[string]string
// images is a map of imageIDs to layerIDs
// TODO: much like the Layers map this might be able to be
// removed
images map[string]string
}
// Repository maps tags to image IDs. The key is a a stringified Reference,
// including the repository name.
type repository map[string]string
var (
// ErrDoesNotExist returned if a reference is not found in the
// store.
ErrDoesNotExist = errors.New("reference does not exist")
)
// An Association is a tuple associating a reference with an image ID.
type Association struct {
Ref reference.Named
ImageID string
}
type lexicalRefs []reference.Named
func (a lexicalRefs) Len() int { return len(a) }
func (a lexicalRefs) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a lexicalRefs) Less(i, j int) bool { return a[i].String() < a[j].String() }
type lexicalAssociations []Association
func (a lexicalAssociations) Len() int { return len(a) }
func (a lexicalAssociations) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a lexicalAssociations) Less(i, j int) bool { return a[i].Ref.String() < a[j].Ref.String() }
// RepositoryCache returns a ref to the repoCache interface
func RepositoryCache() Repo {
return rCache
}
func init() {
rCache = &repoCache{
Repositories: make(map[string]repository),
Layers: make(map[string]string),
images: make(map[string]string),
referencesByIDCache: make(map[string]map[string]reference.Named),
}
}
// NewRespositoryCache will create a new repoCache or rehydrate
// an existing repoCache from the portlayer k/v store
func NewRepositoryCache(client *client.PortLayer) error {
rCache.client = client
val, err := kv.Get(client, repoKey)
if err != nil && err != kv.ErrKeyNotFound {
return err
}
if val != "" {
if err = json.Unmarshal([]byte(val), rCache); err != nil {
return fmt.Errorf("Failed to unmarshal repository cache: %s", err)
}
// hydrate refByIDCache
for _, repository := range rCache.Repositories {
for refStr, refID := range repository {
// #nosec: Errors unhandled.
ref, _ := reference.ParseNamed(refStr)
if rCache.referencesByIDCache[refID] == nil {
rCache.referencesByIDCache[refID] = make(map[string]reference.Named)
}
rCache.referencesByIDCache[refID][refStr] = ref
}
}
// hydrate image -> layer cache
for image, layer := range rCache.Layers {
rCache.images[image] = layer
}
log.Infof("found %d repositories", len(rCache.Repositories))
log.Infof("found %d image layers", len(rCache.Layers))
}
return nil
}
// Save will persist the repository cache to the
// portlayer k/v
func (store *repoCache) Save() error {
b, err := json.Marshal(store)
if err != nil {
log.Errorf("Unable to marshal repository cache: %s", err.Error())
return err
}
err = kv.Put(store.client, repoKey, string(b))
if err != nil {
log.Errorf("Unable to save repository cache: %s", err.Error())
return err
}
return nil
}
func (store *repoCache) AddReference(ref reference.Named, imageID string, force bool, layerID string, save bool) error {
if ref.Name() == string(digest.Canonical) {
return errors.New("refusing to create an ambiguous tag using digest algorithm as name")
}
var err error
store.mu.Lock()
defer store.mu.Unlock()
// does this repo (i.e. busybox) exist?
repository, exists := store.Repositories[ref.Name()]
if !exists || repository == nil {
repository = make(map[string]string)
store.Repositories[ref.Name()] = repository
}
refStr := ref.String()
oldID, exists := repository[refStr]
if exists {
if oldID == imageID {
log.Debugf("Image %s is already tagged as %s", oldID, ref.String())
return nil
}
// force only works for tags
if digested, isDigest := ref.(reference.Canonical); isDigest {
log.Debugf("Unable to overwrite %s with digest %s", oldID, digested.Digest().String())
return fmt.Errorf("Cannot overwrite digest %s", digested.Digest().String())
}
if !force {
log.Debugf("Refusing to overwrite %s with %s unless force is specified", oldID, ref.String())
return fmt.Errorf("Conflict: Tag %s is already set to image %s, if you want to replace it, please use -f option", ref.String(), oldID)
}
if store.referencesByIDCache[oldID] != nil {
delete(store.referencesByIDCache[oldID], refStr)
if len(store.referencesByIDCache[oldID]) == 0 {
delete(store.referencesByIDCache, oldID)
}
}
}
repository[refStr] = imageID
if store.referencesByIDCache[imageID] == nil {
store.referencesByIDCache[imageID] = make(map[string]reference.Named)
}
store.referencesByIDCache[imageID][refStr] = ref
if layerID != "" {
store.Layers[layerID] = imageID
store.images[imageID] = layerID
}
// should we save this input?
if save {
err = store.Save()
}
return err
}
// Remove is a convenience function to allow the passing of a properly
// formed string that can be parsed into a Named object.
//
// Examples:
// Tags: busybox:1.25.1
// Digest: nginx@sha256:7281cf7c854b0dfc7c68a6a4de9a785a973a14f1481bc028e2022bcd6a8d9f64
func (store *repoCache) Remove(ref string, save bool) (string, error) {
n, err := reference.ParseNamed(ref)
if err != nil {
return "", err
}
_, err = store.Delete(n, save)
if err != nil {
return "", err
}
return n.String(), nil
}
// Delete deletes a reference from the store. It returns true if a deletion
// happened, or false otherwise.
func (store *repoCache) Delete(ref reference.Named, save bool) (bool, error) {
ref = reference.WithDefaultTag(ref)
store.mu.Lock()
defer store.mu.Unlock()
var err error
// return code -- assume success
rtc := true
repoName := ref.Name()
repository, exists := store.Repositories[repoName]
if !exists {
return false, ErrDoesNotExist
}
refStr := ref.String()
if imageID, exists := repository[refStr]; exists {
delete(repository, refStr)
if len(repository) == 0 {
delete(store.Repositories, repoName)
}
if store.referencesByIDCache[imageID] != nil {
delete(store.referencesByIDCache[imageID], refStr)
if len(store.referencesByIDCache[imageID]) == 0 {
delete(store.referencesByIDCache, imageID)
}
}
if layer, exists := store.images[imageID]; exists {
delete(store.Layers, imageID)
delete(store.images, layer)
}
if save {
err = store.Save()
if err != nil {
rtc = false
}
}
return rtc, err
}
return false, ErrDoesNotExist
}
// GetImageID will return the imageID associated with the
// specified layerID
func (store *repoCache) GetImageID(layerID string) string {
var imageID string
store.mu.RLock()
defer store.mu.RUnlock()
if image, exists := store.Layers[layerID]; exists {
imageID = image
}
return imageID
}
// Get returns the imageID for a parsed reference
func (store *repoCache) Get(ref reference.Named) (string, error) {
ref = reference.WithDefaultTag(ref)
store.mu.RLock()
defer store.mu.RUnlock()
repository, exists := store.Repositories[ref.Name()]
if !exists || repository == nil {
return "", ErrDoesNotExist
}
imageID, exists := repository[ref.String()]
if !exists {
return "", ErrDoesNotExist
}
return imageID, nil
}
// Tags returns a slice of tags for the specified imageID
func (store *repoCache) Tags(imageID string) []string {
store.mu.RLock()
defer store.mu.RUnlock()
var tags []string
for _, ref := range store.referencesByIDCache[imageID] {
if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
tags = append(tags, tagged.String())
}
}
return tags
}
// Digests returns a slice of digests for the specified imageID
func (store *repoCache) Digests(imageID string) []string {
store.mu.RLock()
defer store.mu.RUnlock()
var digests []string
for _, ref := range store.referencesByIDCache[imageID] {
if d, isCanonical := ref.(reference.Canonical); isCanonical {
digests = append(digests, d.String())
}
}
return digests
}
// References returns a slice of references to the given imageID. The slice
// will be nil if there are no references to this imageID.
func (store *repoCache) References(imageID string) []reference.Named {
store.mu.RLock()
defer store.mu.RUnlock()
// Convert the internal map to an array for two reasons:
// 1) We must not return a mutable
// 2) It would be ugly to expose the extraneous map keys to callers.
var references []reference.Named
for _, ref := range store.referencesByIDCache[imageID] {
references = append(references, ref)
}
sort.Sort(lexicalRefs(references))
return references
}
// ReferencesByName returns the references for a given repository name.
// If there are no references known for this repository name,
// ReferencesByName returns nil.
func (store *repoCache) ReferencesByName(ref reference.Named) []Association {
store.mu.RLock()
defer store.mu.RUnlock()
repository, exists := store.Repositories[ref.Name()]
if !exists {
return nil
}
var associations []Association
for refStr, refID := range repository {
ref, err := reference.ParseNamed(refStr)
if err != nil {
// Should never happen
return nil
}
associations = append(associations,
Association{
Ref: ref,
ImageID: refID,
})
}
sort.Sort(lexicalAssociations(associations))
return associations
}

View File

@@ -0,0 +1,104 @@
// 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 cache
import (
"testing"
"github.com/vmware/vic/lib/apiservers/portlayer/client"
"github.com/vmware/vic/pkg/uid"
"github.com/docker/docker/reference"
"github.com/stretchr/testify/assert"
)
func repoSetup() {
rCache = &repoCache{
client: &client.PortLayer{},
Repositories: make(map[string]repository),
Layers: make(map[string]string),
images: make(map[string]string),
referencesByIDCache: make(map[string]map[string]reference.Named),
}
}
func TestRepo(t *testing.T) {
repoSetup()
notInRepo, _ := reference.ParseNamed("alpine")
noImageID := uid.New()
ref, _ := reference.ParseNamed("busybox:1.25.1")
imageID := uid.New()
layerID := uid.New()
// add busybox:1.25.1
err := RepositoryCache().AddReference(ref, imageID.String(), false, layerID.String(), false)
assert.NoError(t, err)
// Get will return the imageID for the named object
n, err := RepositoryCache().Get(ref)
assert.NoError(t, err)
assert.Equal(t, imageID.String(), n)
// Get all references
refs := RepositoryCache().References(imageID.String())
assert.Equal(t, 1, len(refs))
// Get reference by Named
associated := RepositoryCache().ReferencesByName(ref)
assert.Equal(t, 1, len(associated))
// Get tags for image
tags := RepositoryCache().Tags(imageID.String())
assert.Equal(t, 1, len(tags))
// Get references for non-existent image
refs = RepositoryCache().References(noImageID.String())
assert.Equal(t, 0, len(refs))
// Get reference by Named
associated = RepositoryCache().ReferencesByName(notInRepo)
assert.Equal(t, 0, len(associated))
// get image id via layer id
ig := RepositoryCache().GetImageID(layerID.String())
assert.Equal(t, imageID.String(), ig)
// remove busybox from the cache
r, err := RepositoryCache().Remove(ref.String(), false)
assert.NoError(t, err)
assert.Equal(t, ref.String(), r)
// busybox is removed, so this should fail
x, err := RepositoryCache().Remove(ref.String(), false)
assert.Error(t, err)
assert.Equal(t, "", x)
// add reference by digest
ng, _ := reference.ParseNamed("nginx@sha256:7281cf7c854b0dfc7c68a6a4de9a785a973a14f1481bc028e2022bcd6a8d9f64")
err = RepositoryCache().AddReference(ng, imageID.String(), true, layerID.String(), false)
assert.NoError(t, err)
dd := RepositoryCache().Digests(imageID.String())
assert.Equal(t, 1, len(dd))
// remove the digest
ngx, err := RepositoryCache().Remove(ng.String(), false)
assert.NoError(t, err)
assert.Equal(t, ng.String(), ngx)
// nada
nada := RepositoryCache().Digests(imageID.String())
assert.Equal(t, 0, len(nada))
}

View File

@@ -0,0 +1,40 @@
// 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 backends
import (
"github.com/docker/docker/api/types"
"github.com/vmware/vic/lib/apiservers/engine/errors"
)
type CheckpointBackend struct {
}
func NewCheckpointBackend() *CheckpointBackend {
return &CheckpointBackend{}
}
func (c *CheckpointBackend) CheckpointCreate(container string, config types.CheckpointCreateOptions) error {
return errors.APINotSupportedMsg(ProductName(), "checkpointing")
}
func (c *CheckpointBackend) CheckpointDelete(container string, config types.CheckpointDeleteOptions) error {
return errors.APINotSupportedMsg(ProductName(), "checkpointing")
}
func (c *CheckpointBackend) CheckpointList(container string, config types.CheckpointListOptions) ([]types.Checkpoint, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "checkpointing")
}

View File

@@ -0,0 +1,479 @@
// 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 backends
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"runtime"
"strings"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/distribution/digest"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
containertypes "github.com/docker/docker/api/types/container"
eventtypes "github.com/docker/docker/api/types/events"
"github.com/docker/docker/builder/dockerfile"
dockerimage "github.com/docker/docker/image"
dockerLayer "github.com/docker/docker/layer"
"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/engine/errors"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/imagec"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/version"
"github.com/vmware/vic/pkg/vsphere/sys"
)
// Commit creates a new filesystem image from the current state of a container.
// The image can optionally be tagged into a repository.
func (i *ImageBackend) Commit(name string, config *backend.ContainerCommitConfig) (imageID string, err error) {
defer trace.End(trace.Begin(name))
op := trace.NewOperation(context.Background(), "Commit")
// Look up the container name in the metadata cache to get long ID
vc := cache.ContainerCache().GetContainer(name)
if vc == nil {
return "", errors.NotFoundError(name)
}
// get container info
c, err := containerEngine.ContainerInspect(name, false, "")
if err != nil {
return "", errors.InternalServerError(err.Error())
}
container, ok := c.(*types.ContainerJSON)
if !ok {
return "", errors.InternalServerError(fmt.Sprintf("Container type assertion failed"))
}
if container.State.Running || container.State.Restarting {
return "", errors.ConflictError(fmt.Sprintf("%s does not yet support commit of a running container", ProductName()))
}
// TODO: pause container after container.Pause is implemented
newConfig, err := dockerfile.BuildFromConfig(config.Config, config.Changes)
if err != nil {
return "", err
}
if config.MergeConfigs {
if err := merge(newConfig, container.Config); err != nil {
return "", err
}
}
ic, err := getImagec(config)
if err != nil {
return "", err
}
rc, err := containerEngine.GetContainerChanges(op, vc, true)
if err != nil {
return "", fmt.Errorf("Unable to initialize export stream reader for container %s", name)
}
layer, err := downloadDiff(rc, container.ID, ic.Options)
if err != nil {
rc.Close()
return "", fmt.Errorf("Unable to export stream reader for container %s: %s", name, err)
}
// close reader before write image to avoid resource conflict
rc.Close()
if err = setLayerConfig(layer, container, config, newConfig); err != nil {
return "", err
}
// Dump metadata next to diff file
destination := path.Join(imagec.DestinationDirectory(ic.Options), layer.ID)
err = ioutil.WriteFile(path.Join(destination, layer.ID+".json"), []byte(layer.Meta), 0644)
if err != nil {
return "", err
}
imagec.LayerCache().Add(layer)
var layers []*imagec.ImageWithMeta
layers = append(layers, layer)
lm := layer
for pl := lm.Parent; pl != constants.ScratchLayerID; pl = lm.Parent {
// populate manifest layer with existing cached data
if lm, err = imagec.LayerCache().Get(pl); err != nil {
return "", errors.InternalServerError(fmt.Sprintf("Failed to get parent image layer %s: %s", pl, err))
}
layers = append(layers, lm)
}
ic.ImageLayers = layers
imageConfig, err := ic.CreateImageConfig(layers)
if err != nil {
return "", err
}
imageConfig.Name = config.Repo
// place calculated ImageID in struct
ic.ImageID = imageConfig.ImageID
// cache and persist the image
if err = cache.ImageCache().Add(&imageConfig); err != nil {
return "", fmt.Errorf("error adding image %s to image cache: %s", ic.ImageID, err)
}
if err = cache.ImageCache().Save(); err != nil {
return "", fmt.Errorf("error saving image cache: %s", err)
}
// if repo:tag is specified, update image to repo cache, otherwise, this image will be updated to repo cache while it's tagged
if ic.Reference != nil {
imagec.UpdateRepoCache(ic)
}
ic.Storename = layer.Image.Store
// Write blob to the storage layer
if err = ic.WriteImageBlob(layer, progress.DiscardOutput(), true); err != nil {
return "", err
}
imagec.LayerCache().Commit(layer)
refName := ""
if ic.Reference != nil {
refName = ic.Reference.String()
}
actor := CreateImageEventActorWithAttributes(imageConfig.ImageID, refName, map[string]string{})
EventService().Log("commit", eventtypes.ImageEventType, actor)
return imageConfig.ImageID, nil
}
func getImagec(config *backend.ContainerCommitConfig) (*imagec.ImageC, error) {
var imageRef reference.Named
var err error
if config.Repo != "" {
imageRef, err = reference.WithName(config.Repo)
if err != nil {
return nil, err
}
if config.Tag != "" {
if imageRef, err = reference.WithTag(imageRef, config.Tag); err != nil {
return nil, err
}
}
}
options := imagec.Options{
Destination: os.TempDir(),
Reference: imageRef,
Tag: config.Tag,
}
portLayerServer := PortLayerServer()
if portLayerServer != "" {
options.Host = portLayerServer
}
ic := imagec.NewImageC(options, streamformatter.NewJSONStreamFormatter())
if imageRef != nil {
ic.ParseReference()
}
return ic, nil
}
func setLayerConfig(lm *imagec.ImageWithMeta, container *types.ContainerJSON, config *backend.ContainerCommitConfig, newConfig *containertypes.Config) error {
defer trace.End(trace.Begin(lm.ID))
// 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 {
return errors.InternalServerError(fmt.Sprintf("Failed to get host name: %s", err))
}
if host != "" {
log.Infof("Using UUID (%s) for imagestore name", host)
}
vc := cache.ContainerCache().GetContainer(container.ID)
meta := dockerimage.V1Image{
ID: lm.ID,
Parent: vc.LayerID,
Author: config.Author,
Comment: config.Comment,
Created: time.Now().UTC(),
Container: container.ID,
ContainerConfig: *container.Config,
Architecture: runtime.GOARCH,
OS: runtime.GOOS,
DockerVersion: version.DockerServerVersion,
Config: newConfig,
Size: lm.Size,
}
m, err := json.Marshal(meta)
if err != nil {
return errors.InternalServerError(fmt.Sprintf("Failed to marshal image layer config: %s", err))
}
// layer metadata
lm.Meta = string(m)
lm.Image.Parent = vc.LayerID
lm.Image.Store = host
return nil
}
func downloadDiff(rc io.ReadCloser, containerID string, options imagec.Options) (*imagec.ImageWithMeta, error) {
defer trace.End(trace.Begin(containerID))
// generate random string as layer ID
layerID := stringid.GenerateRandomID()
tmpLayerFileName, diffIDSum, gzSum, err := compressDiffToTmpFile(rc, containerID)
if err != nil {
return nil, err
}
// Cleanup function for the error case
defer func() {
if err != nil {
os.Remove(tmpLayerFileName)
}
}()
blobSum := digest.NewDigestFromBytes(digest.SHA256, gzSum)
log.Debugf("container %s blob sum: %s", containerID, blobSum.String())
layerFile, err := os.Open(string(tmpLayerFileName))
if err != nil {
return nil, err
}
defer layerFile.Close()
decompressed, err := gzip.NewReader(layerFile)
if err != nil {
return nil, err
}
defer decompressed.Close()
// get a tar reader
tr := tar.NewReader(decompressed)
// iterate through tar headers to get file sizes
var size int64
for {
tarHeader, terr := tr.Next()
if terr == io.EOF {
break
}
if terr != nil {
err = terr
return nil, err
}
size += tarHeader.Size
}
diffID := digest.NewDigestFromBytes(digest.SHA256, diffIDSum)
if size == 0 {
diffID = digest.Digest(dockerLayer.DigestSHA256EmptyTar)
}
log.Debugf("container %s diff id: %s, size: %d", containerID, diffID.String(), size)
// Ensure the parent directory exists
destination := path.Join(imagec.DestinationDirectory(options), layerID)
err = os.MkdirAll(destination, 0755) /* #nosec */
if err != nil {
return nil, err
}
// Move(rename) the temporary file to its final destination
err = os.Rename(string(tmpLayerFileName), path.Join(destination, layerID+".tar"))
if err != nil {
return nil, err
}
// layer metadata
lm := &imagec.ImageWithMeta{
Image: &models.Image{
ID: layerID,
},
DiffID: diffID.String(),
Layer: imagec.FSLayer{
BlobSum: blobSum.String(),
},
Size: size,
}
return lm, nil
}
// compressDiffToTmpFile will write stream to temp file, and return temp file name and tar file checksum, compressed file checksum
func compressDiffToTmpFile(rc io.ReadCloser, containerID string) (string, []byte, []byte, error) {
defer trace.End(trace.Begin(containerID))
// Create a temporary file and stream the res.Body into it
var out *os.File
var gzWriter *gzip.Writer
var err error
cleanup := func() {
if gzWriter != nil {
gzWriter.Close()
gzWriter = nil
}
if out != nil {
out.Close()
if err != nil {
os.Remove(out.Name())
}
out = nil
}
}
defer cleanup()
out, err = ioutil.TempFile("", containerID)
if err != nil {
return "", nil, nil, err
}
// compress tar file using gzip and calculate blobsum and diffID all together using multi writer
gzSum := sha256.New()
tarSum := sha256.New()
compressedMW := io.MultiWriter(out, gzSum)
gzWriter = gzip.NewWriter(compressedMW)
tarMW := io.MultiWriter(gzWriter, tarSum)
_, err = io.Copy(tarMW, rc)
if err != nil {
log.Errorf("failed to stream to file: %s", err)
return "", nil, nil, err
}
// close writer before calculate checksum
fileName := out.Name()
err = gzWriter.Flush()
if err != nil {
log.Errorf("failed to flush writer: %s", err)
}
cleanup()
// Return the temporary file name and checksum
return fileName, tarSum.Sum(nil), gzSum.Sum(nil), nil
}
// ***** Code from Docker v17.03.2-ce PullImage to merge two Configs
// merge merges two Config, the image container configuration (defaults values),
// and the user container configuration, either passed by the API or generated
// by the cli.
// It will mutate the specified user configuration (userConf) with the image
// configuration where the user configuration is incomplete.
func merge(userConf, imageConf *containertypes.Config) error {
if userConf.User == "" {
userConf.User = imageConf.User
}
if len(userConf.ExposedPorts) == 0 {
userConf.ExposedPorts = imageConf.ExposedPorts
} else if imageConf.ExposedPorts != nil {
for port := range imageConf.ExposedPorts {
if _, exists := userConf.ExposedPorts[port]; !exists {
userConf.ExposedPorts[port] = struct{}{}
}
}
}
if len(userConf.Env) == 0 {
userConf.Env = imageConf.Env
} else {
for _, imageEnv := range imageConf.Env {
found := false
imageEnvKey := strings.Split(imageEnv, "=")[0]
for _, userEnv := range userConf.Env {
userEnvKey := strings.Split(userEnv, "=")[0]
if runtime.GOOS == "windows" {
// Case insensitive environment variables on Windows
imageEnvKey = strings.ToUpper(imageEnvKey)
userEnvKey = strings.ToUpper(userEnvKey)
}
if imageEnvKey == userEnvKey {
found = true
break
}
}
if !found {
userConf.Env = append(userConf.Env, imageEnv)
}
}
}
if userConf.Labels == nil {
userConf.Labels = map[string]string{}
}
for l, v := range imageConf.Labels {
if _, ok := userConf.Labels[l]; !ok {
userConf.Labels[l] = v
}
}
if len(userConf.Entrypoint) == 0 {
if len(userConf.Cmd) == 0 {
userConf.Cmd = imageConf.Cmd
userConf.ArgsEscaped = imageConf.ArgsEscaped
}
if userConf.Entrypoint == nil {
userConf.Entrypoint = imageConf.Entrypoint
}
}
if imageConf.Healthcheck != nil {
if userConf.Healthcheck == nil {
userConf.Healthcheck = imageConf.Healthcheck
} else {
if len(userConf.Healthcheck.Test) == 0 {
userConf.Healthcheck.Test = imageConf.Healthcheck.Test
}
if userConf.Healthcheck.Interval == 0 {
userConf.Healthcheck.Interval = imageConf.Healthcheck.Interval
}
if userConf.Healthcheck.Timeout == 0 {
userConf.Healthcheck.Timeout = imageConf.Healthcheck.Timeout
}
if userConf.Healthcheck.Retries == 0 {
userConf.Healthcheck.Retries = imageConf.Healthcheck.Retries
}
}
}
if userConf.WorkingDir == "" {
userConf.WorkingDir = imageConf.WorkingDir
}
if len(userConf.Volumes) == 0 {
userConf.Volumes = imageConf.Volumes
} else {
for k, v := range imageConf.Volumes {
userConf.Volumes[k] = v
}
}
if userConf.StopSignal == "" {
userConf.StopSignal = imageConf.StopSignal
}
return nil
}
// *****

View File

@@ -0,0 +1,143 @@
// 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 backends
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"io/ioutil"
"os"
"path"
"testing"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types/backend"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/imagec"
)
func getMockReader(t *testing.T) (io.ReadCloser, error) {
// Create a buffer to write our archive to.
buf := new(bytes.Buffer)
// Create a new tar archive.
tw := tar.NewWriter(buf)
// Add some files to the archive.
var files = []struct {
Name, Body string
}{
{"readme.txt", "This archive contains some text files."},
{"gopher.txt", "Gopher names:\nGeorge\nGeoffrey\nGonzo"},
{"todo.txt", "Get animal handling license."},
}
for _, file := range files {
hdr := &tar.Header{
Name: file.Name,
Mode: 0600,
Size: int64(len(file.Body)),
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
if _, err := tw.Write([]byte(file.Body)); err != nil {
t.Fatal(err)
}
}
// Make sure to check the error on Close.
if err := tw.Close(); err != nil {
log.Fatalln(err)
}
// Open the tar archive for reading.
r := bytes.NewReader(buf.Bytes())
return ioutil.NopCloser(r), nil
}
func TestDownload(t *testing.T) {
log.SetLevel(log.DebugLevel)
tests := []struct {
repo string
tag string
}{
{repo: "registry-1.docker.io", tag: ""},
{repo: "registry-1.docker.io", tag: "mycommit"},
{repo: "myrepo.io", tag: ""},
{repo: "myrepo.io", tag: "mycommit"},
{repo: "", tag: ""},
}
for _, test := range tests {
config := &backend.ContainerCommitConfig{}
config.Tag = test.tag
config.Repo = test.repo
ic, err := getImagec(config)
if err != nil {
t.Errorf("Failed to get imagec: %s", err)
return
}
rc, err := getMockReader(t)
if err != nil {
t.Errorf("Failed to get mocked reader: %s", err)
}
layer, err := downloadDiff(rc, "abcd", ic.Options)
if err != nil {
t.Errorf("Failed to download layer: %s", err)
return
}
t.Logf("layer id: %#v", layer)
destDir := path.Join(imagec.DestinationDirectory(ic.Options), layer.ID)
destination := path.Join(destDir, layer.ID+".tar")
if _, err := os.Stat(destination); err != nil {
t.Errorf("diff file %s is not created", destination)
}
assert.Equal(t, int64(101), layer.Size, "layer size is wrong")
layerFile, err := os.Open(string(destination))
if err != nil {
t.Errorf("Layer file %s is not created: %s", destination, err)
}
defer layerFile.Close()
decompressed, err := gzip.NewReader(layerFile)
if err != nil {
t.Errorf("Failed to create gzip reader: %s", err)
}
defer decompressed.Close()
// get a tar reader
tr := tar.NewReader(decompressed)
// iterate through tar headers to get file sizes
var layerSize int64
for {
tarHeader, terr := tr.Next()
if terr == io.EOF {
break
}
if terr != nil {
t.Errorf("Failed to read layer file: %s", terr)
}
t.Logf("Read file: %s", tarHeader.Name)
layerSize += tarHeader.Size
}
assert.Equal(t, int64(101), layerSize, "tar file size is wrong")
os.RemoveAll(destDir)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
// 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 container
import (
"sync"
"time"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
)
// VicContainer is VIC's abridged version of Docker's container object.
type VicContainer struct {
Name string
ImageID string // maps to the image used by this container
LayerID string // child-most layer ID used to find vmdk for this container
ContainerID string
Config *containertypes.Config //Working copy of config (with overrides from container create)
HostConfig *containertypes.HostConfig
NATMap nat.PortMap // the endpoint NAT mappings only
m sync.RWMutex
execs map[string]struct{}
lockChan chan bool
}
// NewVicContainer returns a reference to a new VicContainer
func NewVicContainer() *VicContainer {
vc := &VicContainer{
Config: &containertypes.Config{},
execs: make(map[string]struct{}),
lockChan: make(chan bool, 1),
}
return vc
}
// Add adds a new exec configuration to the container.
func (v *VicContainer) Add(id string) {
v.m.Lock()
v.execs[id] = struct{}{}
v.m.Unlock()
}
// Delete removes an exec configuration from the container.
func (v *VicContainer) Delete(id string) {
v.m.Lock()
delete(v.execs, id)
v.m.Unlock()
}
// List returns the list of exec ids in the container.
func (v *VicContainer) List() []string {
var IDs []string
v.m.RLock()
for id := range v.execs {
IDs = append(IDs, id)
}
v.m.RUnlock()
return IDs
}
// Tries to lock the container. Timeout argument defines how long the lock
// attempt will be tried. Returns true if locked, false if timed out.
func (v *VicContainer) TryLock(timeout time.Duration) bool {
timeChan := time.After(timeout)
select {
case <-timeChan:
return false
case v.lockChan <- true:
return true
}
}
// Unlocks the container
func (v *VicContainer) Unlock() {
select {
case <-v.lockChan:
default:
panic("Attempt to release container %s's lock that is not locked")
}
}

View File

@@ -0,0 +1,839 @@
// 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 backends
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"testing"
"time"
derr "github.com/docker/docker/api/errors"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
"github.com/docker/docker/api/types/container"
dnetwork "github.com/docker/docker/api/types/network"
"github.com/docker/docker/reference"
"github.com/docker/go-connections/nat"
"github.com/go-openapi/runtime"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
viccontainer "github.com/vmware/vic/lib/apiservers/engine/backends/container"
"github.com/vmware/vic/lib/apiservers/engine/backends/convert"
"github.com/vmware/vic/lib/apiservers/engine/network"
"github.com/vmware/vic/lib/apiservers/engine/proxy"
plclient "github.com/vmware/vic/lib/apiservers/portlayer/client"
plscopes "github.com/vmware/vic/lib/apiservers/portlayer/client/scopes"
plmodels "github.com/vmware/vic/lib/apiservers/portlayer/models"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/metadata"
)
//***********
// Mock proxy
//***********
type CreateHandleMockData struct {
createInputID string
retID string
retHandle string
retErr error
createErrSubstr string
}
type AddToScopeMockData struct {
createInputID string
retHandle string
retErr error
createErrSubstr string
}
type AddVolumesMockData struct {
retHandle string
retErr error
createErrSubstr string
}
type AddInteractionMockData struct {
retHandle string
retErr error
createErrSubstr string
}
type AddLoggingMockData struct {
retHandle string
retErr error
createErrSubstr string
}
type CommitHandleMockData struct {
createInputID string
createErrSubstr string
retErr error
}
type LogMockData struct {
continaerID string
running bool
}
type MockContainerProxy struct {
mockRespIndices []int
mockCreateHandleData []CreateHandleMockData
mockAddToScopeData []AddToScopeMockData
mockAddVolumesData []AddVolumesMockData
mockAddInteractionData []AddInteractionMockData
mockAddLoggingData []AddLoggingMockData
mockCommitData []CommitHandleMockData
}
type MockStorageProxy struct {
}
type MockStreamProxy struct {
}
const (
SUCCESS = 0
dummyContainerID = "abc123"
dummyContainerIDTTY = "tty123"
fakeContainerID = ""
)
var randomNames = []string{
"hello_world",
"hello_world",
"goodbye_world",
"goodbye_world",
"cruel_world",
}
func mockRandomName(retry int) string {
return randomNames[retry%len(randomNames)]
}
var dummyContainers = []string{dummyContainerID, dummyContainerIDTTY}
func NewMockContainerProxy() *MockContainerProxy {
return &MockContainerProxy{
mockRespIndices: make([]int, 6),
mockCreateHandleData: MockCreateHandleData(),
mockAddToScopeData: MockAddToScopeData(),
mockAddVolumesData: MockAddVolumesData(),
mockAddInteractionData: MockAddInteractionData(),
mockAddLoggingData: MockAddLoggingData(),
mockCommitData: MockCommitData(),
}
}
func NewMockStorageProxy() *MockStorageProxy {
return &MockStorageProxy{}
}
func NewMockStreamProxy() *MockStreamProxy {
return &MockStreamProxy{}
}
func MockCreateHandleData() []CreateHandleMockData {
createHandleTimeoutErr := runtime.NewAPIError("unknown error", "context deadline exceeded", http.StatusServiceUnavailable)
mockCreateHandleData := []CreateHandleMockData{
{"busybox", "321cba", "handle", nil, ""},
{"busybox", "", "", derr.NewRequestNotFoundError(fmt.Errorf("No such image: abc123")), "No such image"},
{"busybox", "", "", derr.NewErrorWithStatusCode(createHandleTimeoutErr, http.StatusInternalServerError), "context deadline exceeded"},
}
return mockCreateHandleData
}
func MockAddToScopeData() []AddToScopeMockData {
addToScopeNotFound := plscopes.AddContainerNotFound{
Payload: &plmodels.Error{
Message: "Scope not found",
},
}
addToScopeNotFoundErr := fmt.Errorf("ContainerProxy.AddContainerToScope: Scopes error: %s", addToScopeNotFound.Error())
addToScopeTimeout := plscopes.AddContainerInternalServerError{
Payload: &plmodels.Error{
Message: "context deadline exceeded",
},
}
addToScopeTimeoutErr := fmt.Errorf("ContainerProxy.AddContainerToScope: Scopes error: %s", addToScopeTimeout.Error())
mockAddToScopeData := []AddToScopeMockData{
{"busybox", "handle", nil, ""},
{"busybox", "handle", derr.NewErrorWithStatusCode(fmt.Errorf("container.ContainerCreate failed to create a portlayer client"), http.StatusInternalServerError), "failed to create a portlayer"},
{"busybox", "handle", derr.NewErrorWithStatusCode(addToScopeNotFoundErr, http.StatusInternalServerError), "Scope not found"},
{"busybox", "handle", derr.NewErrorWithStatusCode(addToScopeTimeoutErr, http.StatusInternalServerError), "context deadline exceeded"},
}
return mockAddToScopeData
}
func MockAddVolumesData() []AddVolumesMockData {
return nil
}
func MockAddInteractionData() []AddInteractionMockData {
return nil
}
func MockAddLoggingData() []AddLoggingMockData {
return nil
}
func MockCommitData() []CommitHandleMockData {
noSuchImageErr := fmt.Errorf("No such image: busybox")
mockCommitData := []CommitHandleMockData{
{"buxybox", "", nil},
{"busybox", "failed to create a portlayer", derr.NewErrorWithStatusCode(fmt.Errorf("container.ContainerCreate failed to create a portlayer client"), http.StatusInternalServerError)},
{"busybox", "No such image", derr.NewRequestNotFoundError(noSuchImageErr)},
}
return mockCommitData
}
func (m *MockContainerProxy) GetMockDataCount() (int, int, int, int) {
return len(m.mockCreateHandleData), len(m.mockAddToScopeData), len(m.mockAddVolumesData), len(m.mockCommitData)
}
func (m *MockContainerProxy) SetMockDataResponse(createHandleResp int, addToScopeResp int, addVolumeResp int, addInteractionResp int, addLoggingResp int, commitContainerResp int) {
m.mockRespIndices[0] = createHandleResp
m.mockRespIndices[1] = addToScopeResp
m.mockRespIndices[2] = addVolumeResp
m.mockRespIndices[3] = addInteractionResp
m.mockRespIndices[4] = addLoggingResp
m.mockRespIndices[5] = commitContainerResp
}
func (m *MockContainerProxy) Handle(ctx context.Context, id, name string) (string, error) {
return "", nil
}
func (m *MockContainerProxy) CreateContainerHandle(ctx context.Context, vc *viccontainer.VicContainer, config types.ContainerCreateConfig) (string, string, error) {
respIdx := m.mockRespIndices[0]
if respIdx >= len(m.mockCreateHandleData) {
return "", "", nil
}
return m.mockCreateHandleData[respIdx].retID, m.mockCreateHandleData[respIdx].retHandle, m.mockCreateHandleData[respIdx].retErr
}
func (m *MockContainerProxy) CreateContainerTask(ctx context.Context, handle string, id string, config types.ContainerCreateConfig) (string, error) {
respIdx := m.mockRespIndices[0]
if respIdx >= len(m.mockCreateHandleData) {
return "", nil
}
return m.mockCreateHandleData[respIdx].retHandle, m.mockCreateHandleData[respIdx].retErr
}
func (m *MockContainerProxy) AddContainerToScope(ctx context.Context, handle string, config types.ContainerCreateConfig) (string, error) {
respIdx := m.mockRespIndices[1]
if respIdx >= len(m.mockAddToScopeData) {
return "", nil
}
return m.mockAddToScopeData[respIdx].retHandle, m.mockAddToScopeData[respIdx].retErr
}
func (m *MockContainerProxy) AddVolumesToContainer(ctx context.Context, handle string, config types.ContainerCreateConfig) (string, error) {
respIdx := m.mockRespIndices[2]
if respIdx >= len(m.mockAddVolumesData) {
return "", nil
}
return m.mockAddVolumesData[respIdx].retHandle, m.mockAddVolumesData[respIdx].retErr
}
func (m *MockContainerProxy) AddInteractionToContainer(ctx context.Context, handle string, config types.ContainerCreateConfig) (string, error) {
respIdx := m.mockRespIndices[3]
if respIdx >= len(m.mockAddInteractionData) {
return "", nil
}
return m.mockAddInteractionData[respIdx].retHandle, m.mockAddInteractionData[respIdx].retErr
}
func (m *MockContainerProxy) AddLoggingToContainer(ctx context.Context, handle string, config types.ContainerCreateConfig) (string, error) {
respIdx := m.mockRespIndices[4]
if respIdx >= len(m.mockAddLoggingData) {
return "", nil
}
return m.mockAddLoggingData[respIdx].retHandle, m.mockAddLoggingData[respIdx].retErr
}
func (m *MockContainerProxy) BindInteraction(ctx context.Context, handle string, name string, id string) (string, error) {
return "", nil
}
func (m *MockContainerProxy) CreateExecTask(ctx context.Context, handle string, config *types.ExecConfig) (string, string, error) {
return "", "", nil
}
func (m *MockContainerProxy) UnbindInteraction(ctx context.Context, handle string, name string, id string) (string, error) {
return "", nil
}
func (m *MockContainerProxy) CommitContainerHandle(ctx context.Context, handle, containerID string, waitTime int32) error {
respIdx := m.mockRespIndices[5]
if respIdx >= len(m.mockCommitData) {
return nil
}
return m.mockCommitData[respIdx].retErr
}
func (m *MockContainerProxy) Client() *plclient.PortLayer {
return nil
}
func (m *MockContainerProxy) Stop(ctx context.Context, vc *viccontainer.VicContainer, name string, seconds *int, unbound bool) error {
return nil
}
func (m *MockContainerProxy) State(ctx context.Context, vc *viccontainer.VicContainer) (*types.ContainerState, error) {
// Assume container is running if container in cache. If we need other conditions
// in the future, we can add it, but for now, just assume running.
c := cache.ContainerCache().GetContainer(vc.ContainerID)
if c == nil {
return nil, nil
}
state := &types.ContainerState{
Running: true,
}
return state, nil
}
func (m *MockContainerProxy) Wait(ctx context.Context, vc *viccontainer.VicContainer, timeout time.Duration) (*types.ContainerState, error) {
dockerState := &types.ContainerState{ExitCode: 0}
return dockerState, nil
}
func (m *MockContainerProxy) Signal(ctx context.Context, vc *viccontainer.VicContainer, sig uint64) error {
return nil
}
func (m *MockContainerProxy) Resize(ctx context.Context, id string, height, width int32) error {
return nil
}
func (m *MockContainerProxy) Rename(ctx context.Context, vc *viccontainer.VicContainer, newName string) error {
return nil
}
func (m *MockContainerProxy) Remove(ctx context.Context, vc *viccontainer.VicContainer, config *types.ContainerRmConfig) error {
return nil
}
func (m *MockContainerProxy) StreamContainerStats(ctx context.Context, config *convert.ContainerStatsConfig) error {
return nil
}
func (m *MockContainerProxy) UnbindContainerFromNetwork(ctx context.Context, vc *viccontainer.VicContainer, handle string) (string, error) {
return "", nil
}
func (m *MockContainerProxy) ExitCode(ctx context.Context, vc *viccontainer.VicContainer) (string, error) {
return "", nil
}
func AddMockImageToCache() {
mockImage := &metadata.ImageConfig{
ImageID: "e732471cb81a564575aad46b9510161c5945deaf18e9be3db344333d72f0b4b2",
Name: "busybox",
Tags: []string{"latest"},
Reference: "busybox:latest",
}
mockImage.Config = &container.Config{
Hostname: "55cd1f8f6e5b",
Domainname: "",
User: "",
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Tty: false,
OpenStdin: false,
StdinOnce: false,
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Cmd: []string{"sh"},
Image: "sha256:e732471cb81a564575aad46b9510161c5945deaf18e9be3db344333d72f0b4b2",
Volumes: nil,
WorkingDir: "",
Entrypoint: nil,
OnBuild: nil,
}
cache.ImageCache().Add(mockImage)
ref, _ := reference.ParseNamed(mockImage.Reference)
cache.RepositoryCache().AddReference(ref, mockImage.ImageID, false, mockImage.ImageID, false)
}
func AddMockContainerToCache() {
AddMockImageToCache()
image, err := cache.ImageCache().Get("e732471cb81a564575aad46b9510161c5945deaf18e9be3db344333d72f0b4b2")
if err == nil {
vc := viccontainer.NewVicContainer()
vc.ImageID = image.ID
vc.Config = image.Config //Set defaults. Overrides will get copied below.
vc.Config.Tty = false
vc.ContainerID = dummyContainerID
cache.ContainerCache().AddContainer(vc)
vc = viccontainer.NewVicContainer()
vc.ImageID = image.ID
vc.Config = image.Config
vc.Config.Tty = true
vc.ContainerID = dummyContainerIDTTY
cache.ContainerCache().AddContainer(vc)
vc = viccontainer.NewVicContainer()
vc.ImageID = image.ID
vc.Config = image.Config
vc.Config.Tty = false
vc.ContainerID = fakeContainerID
cache.ContainerCache().AddContainer(vc)
}
}
func (s *MockStorageProxy) Create(ctx context.Context, name, driverName string, volumeData, labels map[string]string) (*types.Volume, error) {
return nil, nil
}
func (s *MockStorageProxy) VolumeList(ctx context.Context, filter string) ([]*plmodels.VolumeResponse, error) {
return nil, nil
}
func (s *MockStorageProxy) VolumeInfo(ctx context.Context, name string) (*plmodels.VolumeResponse, error) {
return nil, nil
}
func (s *MockStorageProxy) Remove(ctx context.Context, name string) error {
return nil
}
func (s *MockStorageProxy) AddVolumesToContainer(ctx context.Context, handle string, config types.ContainerCreateConfig) (string, error) {
return "", nil
}
func (sp *MockStreamProxy) AttachStreams(ctx context.Context, ac *proxy.AttachConfig, stdin io.ReadCloser, stdout, stderr io.Writer) error {
return nil
}
func (sp *MockStreamProxy) StreamContainerLogs(_ context.Context, name string, out io.Writer, started chan struct{}, showTimestamps bool, followLogs bool, since int64, tailLines int64) error {
if name == "" {
return fmt.Errorf("sample error message")
}
var lineCount int64 = 10
close(started)
for i := int64(0); i < lineCount; i++ {
if !followLogs && i > tailLines {
break
}
if followLogs && i > tailLines {
time.Sleep(500 * time.Millisecond)
}
fmt.Fprintf(out, "line %d\n", i)
}
return nil
}
func (sp *MockStreamProxy) StreamContainerStats(ctx context.Context, config *convert.ContainerStatsConfig) error {
return nil
}
//***********
// Tests
//***********
// TestContainerCreateEmptyImageCache() attempts a ContainerCreate() with an empty image
// cache
func TestContainerCreateEmptyImageCache(t *testing.T) {
mockContainerProxy := NewMockContainerProxy()
// Create our personality Container backend
cb := &ContainerBackend{
containerProxy: mockContainerProxy,
}
// mock a container create config
var config types.ContainerCreateConfig
config.HostConfig = &container.HostConfig{}
config.Config = &container.Config{}
config.NetworkingConfig = &dnetwork.NetworkingConfig{}
config.Config.Image = "busybox"
_, err := cb.ContainerCreate(config)
assert.Contains(t, err.Error(), "No such image", "Error (%s) should have 'No such image' for an empty image cache", err.Error())
}
// TestCreateHandle() cycles through all possible input/outputs for creating a handle
// and calls vicbackends.ContainerCreate(). The idea is that if creating handle fails
// then vicbackends.ContainerCreate() should return errors from that.
func TestCreateHandle(t *testing.T) {
mockContainerProxy := NewMockContainerProxy()
// Create our personality Container backend
cb := &ContainerBackend{
containerProxy: mockContainerProxy,
}
AddMockImageToCache()
// configure mock naming for just this test
defer func(fn func(int) string) {
randomName = fn
}(randomName)
randomName = mockRandomName
// mock a container create config
var config types.ContainerCreateConfig
config.HostConfig = &container.HostConfig{}
config.Config = &container.Config{}
config.NetworkingConfig = &dnetwork.NetworkingConfig{}
mockCreateHandleData := MockCreateHandleData()
// Iterate over create handler responses and see what the composite ContainerCreate()
// returns. Since the handle is the first operation, we expect to receive a create handle
// error.
count, _, _, _ := mockContainerProxy.GetMockDataCount()
for i := 0; i < count; i++ {
if i == SUCCESS { //skip success case
continue
}
mockContainerProxy.SetMockDataResponse(i, 0, 0, 0, 0, 0)
config.Config.Image = mockCreateHandleData[i].createInputID
_, err := cb.ContainerCreate(config)
assert.Contains(t, err.Error(), mockCreateHandleData[i].createErrSubstr)
}
}
// TestContainerAddToScope() assumes container handle create succeeded and cycles through all
// possible input/outputs for adding container to scope and calls vicbackends.ContainerCreate()
func TestContainerAddToScope(t *testing.T) {
mockContainerProxy := NewMockContainerProxy()
// Create our personality Container backend
cb := &ContainerBackend{
containerProxy: mockContainerProxy,
}
AddMockImageToCache()
// mock a container create config
var config types.ContainerCreateConfig
config.HostConfig = &container.HostConfig{}
config.Config = &container.Config{}
config.NetworkingConfig = &dnetwork.NetworkingConfig{}
mockAddToScopeData := MockAddToScopeData()
// Iterate over create handler responses and see what the composite ContainerCreate()
// returns. Since the handle is the first operation, we expect to receive a create handle
// error.
_, count, _, _ := mockContainerProxy.GetMockDataCount()
for i := 0; i < count; i++ {
if i == SUCCESS { //skip success case
continue
}
mockContainerProxy.SetMockDataResponse(0, i, 0, 0, 0, 0)
config.Config.Image = mockAddToScopeData[i].createInputID
_, err := cb.ContainerCreate(config)
assert.Contains(t, err.Error(), mockAddToScopeData[i].createErrSubstr)
}
}
// TestContainerAddVolumes() assumes container handle create succeeded and cycles through all
// possible input/outputs for committing the handle and calls vicbackends.ContainerCreate()
func TestCommitHandle(t *testing.T) {
mockContainerProxy := NewMockContainerProxy()
mockStorageProxy := NewMockStorageProxy()
// Create our personality Container backend
cb := &ContainerBackend{
containerProxy: mockContainerProxy,
storageProxy: mockStorageProxy,
}
AddMockImageToCache()
// mock a container create config
var config types.ContainerCreateConfig
config.HostConfig = &container.HostConfig{}
config.Config = &container.Config{}
config.NetworkingConfig = &dnetwork.NetworkingConfig{}
mockCommitHandleData := MockCommitData()
// Iterate over create handler responses and see what the composite ContainerCreate()
// returns. Since the handle is the first operation, we expect to receive a create handle
// error.
_, _, _, count := mockContainerProxy.GetMockDataCount()
for i := 0; i < count; i++ {
if i == SUCCESS { //skip success case
continue
}
mockContainerProxy.SetMockDataResponse(0, 0, 0, 0, 0, i)
config.Config.Image = mockCommitHandleData[i].createInputID
_, err := cb.ContainerCreate(config)
assert.Contains(t, err.Error(), mockCommitHandleData[i].createErrSubstr)
}
}
// TestContainerLogs() tests the docker logs api when user asks for entire log
func TestContainerLogs(t *testing.T) {
// Create our personality Container backend
cb := &ContainerBackend{
containerProxy: NewMockContainerProxy(),
streamProxy: NewMockStreamProxy(),
}
// Prepopulate our image and container cache with dummy data
AddMockContainerToCache()
// Create a buffer io.writer
var writer bytes.Buffer
successDuration := 1 * time.Second
// Create our mock table
mockData := []struct {
Config backend.ContainerLogsConfig
ExpectedSuccess bool
ExpectedFollow bool
}{
{
Config: backend.ContainerLogsConfig{
ContainerLogsOptions: types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Tail: "all",
},
OutStream: &writer,
},
ExpectedSuccess: true,
ExpectedFollow: false,
},
{
Config: backend.ContainerLogsConfig{
ContainerLogsOptions: types.ContainerLogsOptions{
ShowStdout: false,
ShowStderr: false,
},
OutStream: &writer,
},
ExpectedSuccess: false,
ExpectedFollow: false,
},
{
Config: backend.ContainerLogsConfig{
ContainerLogsOptions: types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
},
OutStream: &writer,
},
ExpectedSuccess: true,
ExpectedFollow: true,
},
}
for _, containerID := range dummyContainers {
for _, data := range mockData {
started := make(chan struct{})
start := time.Now()
err := cb.ContainerLogs(context.TODO(), containerID, &data.Config, started)
end := time.Now()
select {
case <-started:
default:
close(started)
}
if data.ExpectedSuccess {
assert.Nil(t, err, "Expected success, but got error, config: %#v", data.Config)
} else {
assert.NotEqual(t, err, nil, "Expected error but received nil, config: %#v", data.Config)
}
immediate := start.Add(successDuration)
didFollow := immediate.Before(end) //determines if logs continued to stream
if data.ExpectedFollow {
assert.True(t, didFollow, "Expected logs to follow but didn't (%s, %s), config: %#v", start.String(), end.String(), data.Config)
} else {
assert.False(t, didFollow, "Expected logs to NOT follow but it did, config: %#v", data.Config)
}
}
}
// Check that ContainerLogs *does not* return an error if StreamContainerLogs
// returns an error. Here, the config is valid and the container is in the
// cache, so the only error will come from StreamContainerLogs. Since the
// containerID = "", StreamContainerLogs will return an error.
started := make(chan struct{})
err := cb.ContainerLogs(context.TODO(), fakeContainerID, &mockData[0].Config, started)
assert.NoError(t, err)
}
func TestPortInformation(t *testing.T) {
mockContainerInfo := &plmodels.ContainerInfo{}
mockContainerConfig := &plmodels.ContainerConfig{}
containerID := "foo"
mockContainerConfig.ContainerID = containerID
mockHostConfig := &container.HostConfig{}
portMap := nat.PortMap{}
port, _ := nat.NewPort("tcp", "80")
portBinding := nat.PortBinding{
HostIP: "127.0.0.1",
HostPort: "8000",
}
portBindings := []nat.PortBinding{portBinding}
portMap[port] = portBindings
mockHostConfig.PortBindings = portMap
mockContainerInfo.ContainerConfig = mockContainerConfig
mockContainerInfo.Endpoints = []*plmodels.EndpointConfig{
{
Direct: true,
Trust: executor.Published.String(),
Ports: []string{"8000/tcp"},
},
}
ips := []string{"192.168.1.1"}
co := viccontainer.NewVicContainer()
co.HostConfig = mockHostConfig
co.NATMap = portMap
co.ContainerID = containerID
co.Name = "bar"
cache.ContainerCache().AddContainer(co)
// unless there are entries in vicnetwork.ContainerByPort we won't report them as bound
ports := network.PortForwardingInformation(co, ips)
assert.Empty(t, ports, "There should be no bound IPs at this point for forwarding")
// the current port binding should show up as a direct port
ports = network.DirectPortInformation(mockContainerInfo)
assert.NotEmpty(t, ports, "There should be a direct port")
network.ContainerByPort["8000"] = containerID
ports = network.PortForwardingInformation(co, ips)
assert.NotEmpty(t, ports, "There should be bound IPs")
assert.Equal(t, 1, len(ports), "Expected 1 port binding, found %d", len(ports))
// now that this port presents as a forwarded port it should NOT present as a direct port
ports = network.DirectPortInformation(mockContainerInfo)
assert.Empty(t, ports, "There should not be a direct port")
port, _ = nat.NewPort("tcp", "80")
portBinding = nat.PortBinding{
HostIP: "127.0.0.1",
HostPort: "00",
}
portMap[port] = portBindings
// forwarding of 00 should never happen, but this is allowing us to confirm that
// it's kicked out by the function even if present in the map
network.ContainerByPort["00"] = containerID
ports = network.PortForwardingInformation(co, ips)
assert.NotEmpty(t, ports, "There should be 1 bound IP")
assert.Equal(t, 1, len(ports), "Expected 1 port binding, found %d", len(ports))
port, _ = nat.NewPort("tcp", "800")
portBinding = nat.PortBinding{
HostIP: "127.0.0.1",
HostPort: "800",
}
portMap[port] = portBindings
network.ContainerByPort["800"] = containerID
ports = network.PortForwardingInformation(co, ips)
assert.Equal(t, 2, len(ports), "Expected 2 port binding, found %d", len(ports))
}
// TestCreateConfigNetowrkMode() whether the HostConfig.NetworkMode is set correctly in ValidateCreateConfig()
func TestCreateConfigNetworkMode(t *testing.T) {
// mock a container create config
mockConfig := types.ContainerCreateConfig{
HostConfig: &container.HostConfig{},
Config: &container.Config{
Image: "busybox",
},
NetworkingConfig: &dnetwork.NetworkingConfig{
EndpointsConfig: map[string]*dnetwork.EndpointSettings{
"net1": {},
},
},
}
validateCreateConfig(&mockConfig)
assert.Equal(t, mockConfig.HostConfig.NetworkMode.NetworkName(), "net1", "expected NetworkMode is net1, found %s", mockConfig.HostConfig.NetworkMode)
// container connects to two vicnetwork endpoints; check for NetworkMode error
mockConfig.NetworkingConfig.EndpointsConfig["net2"] = &dnetwork.EndpointSettings{}
err := validateCreateConfig(&mockConfig)
assert.Contains(t, err.Error(), "NetworkMode error", "error (%s) should have 'NetworkMode error'", err.Error())
}

View File

@@ -0,0 +1,81 @@
// 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 convert
import (
"encoding/base64"
"encoding/json"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
)
const (
AnnotationKeyLabels = "docker.labels"
AnnotationKeyAutoRemove = "docker.autoremove"
)
// SetContainerAnnotation encodes a docker specific attribute into a vSphere annotation. These vSphere
// annotations are stored in the VM vmx file
func SetContainerAnnotation(config *models.ContainerCreateConfig, key string, value interface{}) error {
var err error
if config == nil || value == nil {
return nil
}
if config.Annotations == nil {
config.Annotations = make(map[string]string)
}
// Encoding the labels map into a blob that can be stored as ansi regardless
// of what encoding the input labels are. We do this by first marshaling to
// to a json byte array to get a self describing encoding and then encoding
// to base64. We could use another encoding for the self describing part,
// such as Golang GOB, but this data will be pushed over to a standard REST
// server so we use standard web standards instead.
if valueBytes, merr := json.Marshal(value); merr == nil {
blob := base64.StdEncoding.EncodeToString(valueBytes)
config.Annotations[key] = blob
} else {
err = merr
log.Errorf("Unable to marshal annotation %s to json: %s", key, err)
}
return err
}
// ContainerAnnotation will convert a vSphere annotation into a docker specific attribute
func ContainerAnnotation(annotations map[string]string, key string, value interface{}) error {
var err error
if len(annotations) == 0 || value == nil {
return nil
}
if blob, ok := annotations[key]; ok {
if annotationBytes, decodeErr := base64.StdEncoding.DecodeString(blob); decodeErr == nil {
if err = json.Unmarshal(annotationBytes, value); err != nil {
log.Errorf("Unable to unmarshal %s: %s", key, err)
}
} else {
err = decodeErr
log.Errorf("Unable to decode container annotations: %s", err)
}
}
return err
}

View File

@@ -0,0 +1,48 @@
// 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 convert
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
)
func TestSetContainerAnnotation(t *testing.T) {
config := &models.ContainerCreateConfig{}
labels := make(map[string]string)
labels["environment"] = "dev"
err := SetContainerAnnotation(nil, AnnotationKeyLabels, labels)
assert.NoError(t, err)
err = SetContainerAnnotation(config, AnnotationKeyLabels, &labels)
assert.NoError(t, err)
var myLabels map[string]string
err = ContainerAnnotation(myLabels, AnnotationKeyLabels, &myLabels)
assert.NoError(t, err)
err = ContainerAnnotation(config.Annotations, AnnotationKeyLabels, &myLabels)
assert.NoError(t, err)
assert.Equal(t, 1, len(myLabels))
err = ContainerAnnotation(config.Annotations, AnnotationKeyLabels, myLabels)
assert.Error(t, err)
}

View File

@@ -0,0 +1,93 @@
// 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 convert
import (
"fmt"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/go-units"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
)
// State will create and return a docker ContainerState object
// from the passed vic ContainerInfo object
func State(info *models.ContainerInfo) *types.ContainerState {
// ensure we have the data we need
if info == nil || info.ProcessConfig == nil || info.ContainerConfig == nil {
return nil
}
dockerState := &types.ContainerState{}
// convert start / stop times
var started time.Time
var finished time.Time
if info.ProcessConfig.StartTime > 0 {
started = time.Unix(info.ProcessConfig.StartTime, 0)
dockerState.StartedAt = time.Unix(info.ProcessConfig.StartTime, 0).Format(time.RFC3339Nano)
}
if info.ProcessConfig.StopTime > 0 {
finished = time.Unix(info.ProcessConfig.StopTime, 0)
dockerState.FinishedAt = time.Unix(info.ProcessConfig.StopTime, 0).Format(time.RFC3339Nano)
}
// set docker status to state and we'll change if needed
dockStatus := info.ContainerConfig.State
// set exitCode and change if needed
exitCode := int(info.ProcessConfig.ExitCode)
switch info.ContainerConfig.State {
case "Running":
// if we don't have a start date leave the status as the state
if !started.IsZero() {
dockStatus = fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(started)))
dockerState.Running = true
}
case "Stopped":
// if we don't have a finished date then don't process exitCode and return "Stopped" for the status
if !finished.IsZero() {
// interrogate the process status returned from the portlayer
// and based on status text and exit codes set the appropriate
// docker exit code
if strings.Contains(info.ProcessConfig.Status, "permission denied") {
exitCode = 126
} else if strings.Contains(info.ProcessConfig.Status, "no such") {
exitCode = 127
} else if info.ProcessConfig.Status == "true" && exitCode == -1 {
// most likely the process was killed via the cli
// or received a sigkill
exitCode = 137
} else if info.ProcessConfig.Status == "" && exitCode == -1 {
// the process was stopped via the cli
// or received a sigterm
exitCode = 143
}
dockStatus = fmt.Sprintf("Exited (%d) %s ago", exitCode, units.HumanDuration(time.Now().UTC().Sub(finished)))
}
}
dockerState.Status = dockStatus
dockerState.ExitCode = exitCode
dockerState.Pid = int(info.ProcessConfig.Pid)
dockerState.Error = info.ProcessConfig.ErrorMsg
return dockerState
}

View File

@@ -0,0 +1,453 @@
// 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 convert
import (
"context"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/vmware/vic/pkg/vsphere/performance"
)
// ContainerStats encapsulates the conversion of VMMetrics to
// docker specific metrics
type ContainerStats struct {
config *ContainerStatsConfig
totalVCHMhz uint64
dblVCHMhz uint64
preTotalMhz uint64
preDockerStat *types.StatsJSON
curDockerStat *types.StatsJSON
currentMetric *performance.VMMetrics
// disk & net stats are accumulated during the life of the
// subscription. These maps will assist in that accumulation.
diskStats map[string]performance.VirtualDisk
netStats map[string]performance.Network
mu sync.Mutex
reader *io.PipeReader
writer *io.PipeWriter
listening bool
}
type ContainerStatsConfig struct {
Ctx context.Context
Cancel context.CancelFunc
Out io.Writer
ContainerID string
ContainerState *types.ContainerState
Memory int64
Stream bool
VchMhz int64
}
type InvalidOrderError struct {
current time.Time
previous time.Time
}
func (iso InvalidOrderError) Error() string {
return fmt.Sprintf("The current sample time (%s) is before the previous time (%s)", iso.current, iso.previous)
}
// NewContainerStats will return a new instance of ContainerStats
func NewContainerStats(config *ContainerStatsConfig) *ContainerStats {
return &ContainerStats{
config: config,
curDockerStat: &types.StatsJSON{},
totalVCHMhz: uint64(config.VchMhz),
dblVCHMhz: uint64(config.VchMhz * 2),
diskStats: make(map[string]performance.VirtualDisk),
netStats: make(map[string]performance.Network),
}
}
// IsListening returns the listening flag
func (cs *ContainerStats) IsListening() bool {
cs.mu.Lock()
defer cs.mu.Unlock()
return cs.listening
}
// Stop will clean up the pipe and flip listening flag
func (cs *ContainerStats) Stop() {
cs.mu.Lock()
defer cs.mu.Unlock()
if cs.listening {
// #nosec: Errors unhandled.
cs.reader.Close()
// #nosec: Errors unhandled.
cs.writer.Close()
cs.listening = false
}
}
// newPipe will initialize the pipe for encoding / decoding and
// set the listening flag
func (cs *ContainerStats) newPipe() {
cs.mu.Lock()
defer cs.mu.Unlock()
// create a new reader / writer
cs.reader, cs.writer = io.Pipe()
cs.listening = true
}
// Listen for new metrics from the portLayer, convert to docker format
// and encode to the configured Writer.
func (cs *ContainerStats) Listen() *io.PipeWriter {
// Are we already listening?
if cs.IsListening() {
return nil
}
// create pipe for encode/decode
cs.newPipe()
dec := json.NewDecoder(cs.reader)
doc := json.NewEncoder(cs.config.Out)
// channel to transfer metric from decoder to encoder
metric := make(chan performance.VMMetrics)
// if we aren't streaming and the container is not running, then create an empty
// docker stat to return
if !cs.config.Stream && !cs.config.ContainerState.Running {
cs.preDockerStat = &types.StatsJSON{}
}
// go routine to stop on Context.Cancel
go func() {
<-cs.config.Ctx.Done()
close(metric)
cs.Stop()
}()
// go routine will decode metrics received from the portLayer and
// send them to the encoding routine
go func() {
for {
select {
case <-cs.config.Ctx.Done():
return
default:
for dec.More() {
var vmm performance.VMMetrics
err := dec.Decode(&vmm)
if err != nil {
log.Errorf("container metric decoding error for container(%s): %s", cs.config.ContainerID, err)
cs.config.Cancel()
}
// send the decoded metric for transform and encoding
if cs.IsListening() {
metric <- vmm
}
}
}
}
}()
// go routine will convert incoming metrics to docker specific stats and encode for the docker client.
go func() {
// docker needs updates quicker than vSphere can produce metrics, so we'll send a minimum of 1 metric/sec
ticker := time.NewTicker(time.Millisecond * 500)
for range ticker.C {
select {
case <-cs.config.Ctx.Done():
ticker.Stop()
return
case nm := <-metric:
// convert the Stat to docker struct
stat, err := cs.ToContainerStats(&nm)
if err != nil {
log.Errorf("container metric conversion error for container(%s): %s", cs.config.ContainerID, err)
cs.config.Cancel()
}
if stat != nil {
cs.preDockerStat = stat
}
default:
if cs.IsListening() && cs.preDockerStat != nil {
// send docker stat to client
err := doc.Encode(cs.preDockerStat)
if err != nil {
log.Warnf("container metric encoding error for container(%s): %s", cs.config.ContainerID, err)
cs.config.Cancel()
}
// if we aren't streaming then cancel
if !cs.config.Stream {
cs.config.Cancel()
}
}
}
}
}()
return cs.writer
}
// ToContainerStats will convert the vic VMMetrics to a docker stats struct -- a complete docker stats
// struct requires two samples. Func will return nil until a complete stat is available
func (cs *ContainerStats) ToContainerStats(current *performance.VMMetrics) (*types.StatsJSON, error) {
// if we have a current metric then validate and transform
if cs.currentMetric != nil {
// do we have the same metric or has the metric not been initialized?
if cs.currentMetric.SampleTime.Equal(current.SampleTime) || current.SampleTime.IsZero() {
return nil, nil
}
// we have new current stats so need to move the previous CPU
err := cs.previousCPU(current)
if err != nil {
return nil, err
}
}
cs.currentMetric = current
// create the current CPU stats
cs.currentCPU()
// create memory stats
cs.memory()
// create network stats
cs.network()
// create storage stats
cs.disk()
// set sample time
cs.curDockerStat.Read = cs.currentMetric.SampleTime
// PreRead will be zero if we don't have two samples
if cs.curDockerStat.PreRead.IsZero() {
return nil, nil
}
return cs.curDockerStat, nil
}
// network will calculate stats by network device. The stats presented will be the
// network stats accumulated during the stats subscription. This differs from vanilla
// docker as it provides the network stats for the lifetime of the container.
//
// TODO: Errors from either Tx or Rx are not currently supported (July 9th 2017)
func (cs *ContainerStats) network() {
cs.curDockerStat.Networks = make(map[string]types.NetworkStats)
for _, net := range cs.currentMetric.Networks {
// get the previous network stats
if preNet, exists := cs.netStats[net.Name]; exists {
net.Rx.Bytes += preNet.Rx.Bytes
net.Rx.Packets += preNet.Rx.Packets
net.Rx.Dropped += preNet.Rx.Dropped
net.Tx.Bytes += preNet.Tx.Bytes
net.Tx.Packets += preNet.Tx.Packets
net.Tx.Dropped += preNet.Tx.Dropped
cs.netStats[net.Name] = net
} else {
// initial iteration
cs.netStats[net.Name] = net
}
cs.curDockerStat.Networks[net.Name] = types.NetworkStats{
RxBytes: net.Rx.Bytes,
RxPackets: uint64(net.Rx.Packets),
RxDropped: uint64(net.Rx.Dropped),
TxBytes: net.Tx.Bytes,
TxPackets: uint64(net.Tx.Packets),
TxDropped: uint64(net.Tx.Dropped),
}
}
}
// disk will calculate supported stats by disk device. The stats presented will be the
// disk stats accumulated during the stats subscription. This differs from vanilla
// docker as it provides the disk stats for the lifetime of the container.
//
// Supported stats are io_service_bytes_recursive and io_serviced_recursive, so bytes and iops
// during the stats subscription
//
// TODO: Currently disk assumes a single scsi controller. Multiple scsi controllers will need
// to be supported in a future release (July 9th 2017)
func (cs *ContainerStats) disk() {
// docker storage stats to populate
storage := types.BlkioStats{
IoServiceBytesRecursive: []types.BlkioStatEntry{},
IoServicedRecursive: []types.BlkioStatEntry{},
}
for _, disk := range cs.currentMetric.Disks {
// disk stats accumulate for the life of subscription, so
// either add previous stats or store initial stats
if preDisk, exists := cs.diskStats[disk.Name]; exists {
// add previous values to current value
disk.Read.Bytes += preDisk.Read.Bytes
disk.Read.Op += preDisk.Read.Op
disk.Write.Bytes += preDisk.Write.Bytes
disk.Write.Op += preDisk.Write.Op
cs.diskStats[disk.Name] = disk
} else {
// initial iteration
cs.diskStats[disk.Name] = disk
}
// get the minor number for the disk device
deviceMinor := diskMinor(cs.config.ContainerID, disk.Name)
// need to update read, write & total for supported stats (bytes & iops)
storage.IoServiceBytesRecursive = append(storage.IoServiceBytesRecursive,
createBlkioStatsEntry(deviceMinor, "Read", cs.diskStats[disk.Name].Read.Bytes))
storage.IoServiceBytesRecursive = append(storage.IoServiceBytesRecursive,
createBlkioStatsEntry(deviceMinor, "Write", cs.diskStats[disk.Name].Write.Bytes))
storage.IoServiceBytesRecursive = append(storage.IoServiceBytesRecursive,
createBlkioStatsEntry(deviceMinor, "Total", cs.diskStats[disk.Name].Read.Bytes+cs.diskStats[disk.Name].Write.Bytes))
// Ops
storage.IoServicedRecursive = append(storage.IoServicedRecursive,
createBlkioStatsEntry(deviceMinor, "Read", cs.diskStats[disk.Name].Read.Op))
storage.IoServicedRecursive = append(storage.IoServicedRecursive,
createBlkioStatsEntry(deviceMinor, "Write", cs.diskStats[disk.Name].Write.Op))
storage.IoServicedRecursive = append(storage.IoServicedRecursive,
createBlkioStatsEntry(deviceMinor, "Total", cs.diskStats[disk.Name].Read.Op+cs.diskStats[disk.Name].Write.Op))
}
// add the block stats to the docker stat
cs.curDockerStat.BlkioStats = storage
}
func (cs *ContainerStats) memory() {
// given MB (i.e. 2048) convert to GB
cs.curDockerStat.MemoryStats.Limit = uint64(cs.config.Memory * 1024 * 1024)
// given KB (i.e. 384.5) convert to Bytes
cs.curDockerStat.MemoryStats.Usage = uint64(cs.currentMetric.Memory.Active * 1024)
}
// previousCPU will move the current stats to the previous CPU location
func (cs *ContainerStats) previousCPU(current *performance.VMMetrics) error {
// validate that the sampling is in the correct order
if current.SampleTime.Before(cs.curDockerStat.Read) {
err := InvalidOrderError{
current: current.SampleTime,
previous: cs.curDockerStat.Read,
}
return err
}
// move the stats
cs.curDockerStat.PreCPUStats = cs.curDockerStat.CPUStats
// set the previousTotal -- this will be added to the current CPU
cs.preTotalMhz = cs.curDockerStat.PreCPUStats.CPUUsage.TotalUsage
cs.curDockerStat.PreRead = cs.curDockerStat.Read
// previous systemUsage will always be the VCH total
// see note in func currentCPU() for detail
cs.curDockerStat.PreCPUStats.SystemUsage = cs.totalVCHMhz
return nil
}
// currentCPU will convert the VM CPU metrics to docker CPU stats
func (cs *ContainerStats) currentCPU() {
cpuCount := len(cs.currentMetric.CPU.CPUs)
dockerCPU := types.CPUStats{
CPUUsage: types.CPUUsage{
PercpuUsage: make([]uint64, cpuCount, cpuCount),
},
}
// collect the current CPU Metrics
for ci, current := range cs.currentMetric.CPU.CPUs {
dockerCPU.CPUUsage.PercpuUsage[ci] = uint64(current.MhzUsage)
dockerCPU.CPUUsage.TotalUsage += uint64(current.MhzUsage)
}
// vSphere will report negative usage for a starting VM, lets
// set to zero
if dockerCPU.CPUUsage.TotalUsage < 0 {
dockerCPU.CPUUsage.TotalUsage = 0
}
// The first stat available for a VM will be missing detail
if cpuCount > 0 {
// TotalUsage is the sum of the individual vCPUs Mhz
// consumption this reading. We must divide that by the
// number of vCPUs to get the average across both, since
// the cpuUsage calc (explained below) will multiply by
// the number of CPUs to get the cpuUsage percent
dockerCPU.CPUUsage.TotalUsage /= uint64(cpuCount)
}
// Set the current systemUsage to double the VCH as the
// previous systemUsage is the VCH total. The docker
// client formula creates a SystemDelta which is the following:
// systemDelta = currentSystemUsage - previousSystemUsage
// We always need systemDelta to equal the total amount of
// VCH Mhz thus the doubling here.
dockerCPU.SystemUsage = cs.dblVCHMhz
// Much like systemUsage (above) totalCPUUsage and previous
// totalCPUUsage will be used to create a CPUUsage delta as such:
// CPUDelta = currentTotalCPUUsage - previousTotalCPUUsage
// This amount will then be divided by the systemDelta
// (explained above) as part of the CPU % Usage calculation
// cpuUsage = (CPUDelta / SystemDelta) * cpuCount * 100
// This will require the addition of the previous total usage
dockerCPU.CPUUsage.TotalUsage += cs.preTotalMhz
cs.curDockerStat.CPUStats = dockerCPU
}
// diskMinor will parse the disk name and return the minor id of
// the disk device. The func assumes that minor identifiers are multiples
// of 16 (0,16,32,48,etc).
func diskMinor(containerID string, name string) uint64 {
// disks are named scsi0:0, scsi0:1
// i.e. controller+controller number:device number
device := strings.Split(name, ":")
// convert to an int
minor, err := strconv.Atoi(device[len(device)-1])
if err != nil {
// log error, but continue and return a minor number of zero
// unlikely this would happen, but if it does and there is more than one disk on the vm
// then it could go undetected
log.Errorf("stats error generating container(%s) disk(%s) minor: %s", containerID, name, err)
}
// minor identifiers are multiples of 16
minor *= 16
return uint64(minor)
}
func createBlkioStatsEntry(minor uint64, op string, value uint64) types.BlkioStatEntry {
return types.BlkioStatEntry{
Major: 8,
Minor: minor,
Op: op,
Value: value,
}
}

View File

@@ -0,0 +1,495 @@
// 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 convert
import (
"context"
"encoding/json"
"fmt"
"io"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/pkg/retry"
"github.com/vmware/vic/pkg/vsphere/performance"
)
const (
vcpuMhz = 3300
vcpuCount = 1
vchMhzTotal = 3300
memConsumed = 1024 * 1024 * 500
memProvisioned = 1024 * 1024 * 1024
)
func TestContainerConverter(t *testing.T) {
plumb := setup()
defer teardown(plumb)
// grab a config object
config := ccConfig(plumb)
cStats := NewContainerStats(config)
assert.NotNil(t, cStats)
// returned writer is given to PL
writer := cStats.Listen()
assert.NotNil(t, writer)
// second call should result in nil writer as
// we are already listening
w2 := cStats.Listen()
assert.Nil(t, w2)
// // ensure stop closes reader / writer
cStats.Stop()
// verify we stopped listening
assert.False(t, cStats.IsListening())
}
func TestToContainerStats(t *testing.T) {
plumb := setup()
defer teardown(plumb)
// grab a config object
config := ccConfig(plumb)
cStats := NewContainerStats(config)
assert.NotNil(t, cStats)
initCPU := 1000
vmBefore := vmMetrics(vcpuCount, initCPU)
vmm := vmMetrics(vcpuCount, initCPU)
// ensure we are after the initial metric
vmm.SampleTime.Add(time.Second * 1)
// first metric sent, should return nil
js, err := cStats.ToContainerStats(vmm)
assert.NoError(t, err)
assert.Nil(t, js)
// send the same stat should return nil
js, err = cStats.ToContainerStats(vmm)
assert.Nil(t, err)
assert.Nil(t, js)
// send out of order stat
js, err = cStats.ToContainerStats(vmBefore)
assert.NotNil(t, err)
assert.Nil(t, js)
secondCPU := 250
// create a new metric
vmmm := vmMetrics(vcpuCount, secondCPU)
// sample will be 20 seconds apart..
vmmm.SampleTime = vmm.SampleTime.Add(time.Second * 20)
js, err = cStats.ToContainerStats(vmmm)
assert.NoError(t, err)
assert.NotZero(t, js.Read, js.PreRead)
assert.Equal(t, uint64(vchMhzTotal*2), js.CPUStats.SystemUsage)
assert.Equal(t, uint64(secondCPU+initCPU), js.CPUStats.CPUUsage.TotalUsage)
assert.Equal(t, uint64(initCPU), js.PreCPUStats.CPUUsage.TotalUsage)
assert.Equal(t, uint64(vchMhzTotal), js.PreCPUStats.SystemUsage)
// this reading should show 250mhz of 3300mhz used -- 7.58%
cpuPercent := fmt.Sprintf("%2.2f", calculateCPUPercentUnix(js.PreCPUStats.CPUUsage.TotalUsage, js.PreCPUStats.SystemUsage, js))
assert.Equal(t, "7.58", cpuPercent)
config.Cancel()
<-config.Ctx.Done()
// verify we stopped listening
assert.True(t, success(cStats))
}
func TestContainerStatsListener(t *testing.T) {
plumb := setup()
defer teardown(plumb)
// grab a config object
config := ccConfig(plumb)
cStats := NewContainerStats(config)
assert.NotNil(t, cStats)
// start the listener
writer := cStats.Listen()
assert.NotNil(t, writer)
// create an initial metric
initCPU := 1000
vm := vmMetrics(vcpuCount, initCPU)
err := plumb.mockPLMetrics(vm, writer)
assert.NoError(t, err)
// send second metric
vmm := vmMetrics(vcpuCount, initCPU+100)
vmm.SampleTime = vm.SampleTime.Add(time.Second * 20)
err = plumb.mockPLMetrics(vmm, writer)
assert.NoError(t, err)
// did client receive metric??
ds, err := plumb.mockDockerClient()
assert.NoError(t, err)
assert.NotNil(t, ds)
assert.Equal(t, uint64((initCPU*2+100)/vcpuCount), ds.CPUStats.CPUUsage.TotalUsage)
// docker expects data quicker than vSphere can produce -- sleep for just over 1 sec
// and ensure the previous docker stat is returned to client
time.Sleep(time.Millisecond * 1100)
same, err := plumb.mockDockerClient()
assert.NoError(t, err)
assert.NotNil(t, same)
assert.Equal(t, ds.CPUStats.CPUUsage.TotalUsage, same.CPUStats.CPUUsage.TotalUsage)
config.Cancel()
<-config.Ctx.Done()
// verify we stopped listening
assert.True(t, success(cStats))
}
func TestContainerConvertCtxCancel(t *testing.T) {
plumb := setup()
defer teardown(plumb)
// grab a config object
config := ccConfig(plumb)
cStats := NewContainerStats(config)
assert.NotNil(t, cStats)
// start the listener
writer := cStats.Listen()
assert.NotNil(t, writer)
// cancel the context
config.Cancel()
<-config.Ctx.Done()
// verify we stopped listening
assert.True(t, success(cStats))
}
func TestContainerConvertNoStream(t *testing.T) {
plumb := setup()
defer teardown(plumb)
// grab a config object
config := ccConfig(plumb)
config.Stream = false
cStats := NewContainerStats(config)
assert.NotNil(t, cStats)
// start the listener
writer := cStats.Listen()
assert.NotNil(t, writer)
// create an initial metric
initCPU := 1000
vm := vmMetrics(vcpuCount, initCPU)
err := plumb.mockPLMetrics(vm, writer)
assert.NoError(t, err)
// send second metric
vmm := vmMetrics(vcpuCount, initCPU+100)
vmm.SampleTime = vm.SampleTime.Add(time.Second * 20)
err = plumb.mockPLMetrics(vmm, writer)
assert.NoError(t, err)
ds, err := plumb.mockDockerClient()
assert.NoError(t, err)
assert.NotNil(t, ds)
// converter canceled the context
<-config.Ctx.Done()
// verify we stopped listening
assert.True(t, success(cStats))
}
func TestContainerNotRunningNoStream(t *testing.T) {
plumb := setup()
defer teardown(plumb)
// grab a config object
config := ccConfig(plumb)
config.Stream = false
config.ContainerState.Running = false
cStats := NewContainerStats(config)
assert.NotNil(t, cStats)
// start the listener
writer := cStats.Listen()
assert.NotNil(t, writer)
ds, err := plumb.mockDockerClient()
assert.NoError(t, err)
assert.NotNil(t, ds)
// converter canceled the context
<-config.Ctx.Done()
// verify we stopped listening
assert.True(t, success(cStats))
}
func TestDiskMinor(t *testing.T) {
containerID := "12345"
for i := 0; i <= 15; i++ {
name := fmt.Sprintf("scsi0:%d", i)
assert.Equal(t, uint64(i*16), diskMinor(containerID, name))
}
minor := uint64(0)
// test with invalid disk names to ensure no panic, etc
assert.Equal(t, minor, diskMinor(containerID, "foo:bar:0"))
assert.Equal(t, minor, diskMinor(containerID, "foo"))
assert.Equal(t, minor, diskMinor(containerID, "foo:"))
}
func TestCreateBlkioStatsEntry(t *testing.T) {
minor := uint64(0)
val := uint64(12)
maj := uint64(8)
entry := createBlkioStatsEntry(minor, "Read", val)
assert.Equal(t, "Read", entry.Op)
assert.Equal(t, val, entry.Value)
assert.Equal(t, minor, entry.Minor)
assert.Equal(t, maj, entry.Major)
}
func TestDiskStats(t *testing.T) {
plumb := setup()
defer teardown(plumb)
// grab a config object
config := ccConfig(plumb)
cStats := NewContainerStats(config)
assert.NotNil(t, cStats)
// create metric
initCPU := 1000
vm := vmMetrics(vcpuCount, initCPU)
cStats.currentMetric = vm
// update disk
cStats.disk()
assert.Equal(t, 3, len(cStats.curDockerStat.BlkioStats.IoServiceBytesRecursive))
assert.Equal(t, 1, len(cStats.diskStats))
// update again -- this should accumulate the totals
cStats.disk()
assert.Equal(t, 3, len(cStats.curDockerStat.BlkioStats.IoServiceBytesRecursive))
assert.Equal(t, 1, len(cStats.diskStats))
for _, disk := range cStats.curDockerStat.BlkioStats.IoServiceBytesRecursive {
switch disk.Op {
case "Write":
assert.Equal(t, uint64(vm.Disks[0].Write.Bytes*2), disk.Value)
}
}
}
func TestNetworkStats(t *testing.T) {
plumb := setup()
defer teardown(plumb)
// grab a config object
config := ccConfig(plumb)
cStats := NewContainerStats(config)
assert.NotNil(t, cStats)
// create metric
initCPU := 1000
vm := vmMetrics(vcpuCount, initCPU)
cStats.currentMetric = vm
// update network
cStats.network()
assert.Equal(t, 1, len(cStats.curDockerStat.Networks))
assert.Equal(t, 1, len(cStats.netStats))
// update again -- this should accumulate the totals
cStats.network()
assert.Equal(t, 1, len(cStats.curDockerStat.Networks))
assert.Equal(t, 1, len(cStats.netStats))
for network, usage := range cStats.curDockerStat.Networks {
switch network {
case "eth0":
assert.Equal(t, uint64(200), usage.RxBytes)
}
}
}
// Test Helpers
type plumbing struct {
r *io.PipeReader
w *io.PipeWriter
out io.Writer
// mock portlayer
mockPL *json.Encoder
// mock docker client decoder
mockDoc *json.Decoder
}
func setup() *plumbing {
r, o := io.Pipe()
out := io.Writer(o)
return &plumbing{
r: r,
w: o,
out: out,
mockDoc: json.NewDecoder(r),
}
}
// success is a helper to check the listening status of the
// converter
func success(converter *ContainerStats) bool {
op := func() error {
listen := converter.IsListening()
if listen {
return fmt.Errorf("still listening: %t", listen)
}
return nil
}
wait := func(err error) bool {
if err != nil {
return true
}
return false
}
// use the retry package and keep retrying until we've hit the limit
if err := retry.Do(op, wait); err != nil {
return false
}
return true
}
func teardown(p *plumbing) {
// close the reader / writer
p.r.Close()
p.w.Close()
}
func (p *plumbing) mockPLMetrics(metric *performance.VMMetrics, writer io.Writer) error {
if p.mockPL == nil {
p.mockPL = json.NewEncoder(writer)
}
return p.mockPL.Encode(metric)
}
func (p *plumbing) mockDockerClient() (*types.StatsJSON, error) {
docStats := &types.StatsJSON{}
err := p.mockDoc.Decode(docStats)
if err != nil {
return nil, err
}
return docStats, nil
}
func ccConfig(p *plumbing) *ContainerStatsConfig {
// test config
ctx, cancel := context.WithCancel(context.Background())
config := &ContainerStatsConfig{
VchMhz: int64(vchMhzTotal),
Ctx: ctx,
Cancel: cancel,
ContainerID: "1234",
Out: p.out,
Stream: true,
Memory: 2048,
ContainerState: &types.ContainerState{
Running: true,
},
}
return config
}
func vmMetrics(count int, vcpuMhz int) *performance.VMMetrics {
vmm := &performance.VMMetrics{}
vmm.SampleTime = time.Now()
vmm.CPU = cpuUsageMetrics(count, vcpuMhz)
vmm.Memory = performance.MemoryMetrics{
Consumed: int64(memConsumed),
Provisioned: int64(memProvisioned),
}
disk := performance.VirtualDisk{
Name: "scsi0:0",
Write: performance.DiskUsage{
Bytes: uint64(100),
Kbps: 5,
Op: uint64(5),
Ops: 5,
},
Read: performance.DiskUsage{
Bytes: uint64(10),
Kbps: 5,
Op: uint64(5),
Ops: 5,
},
}
vmm.Disks = append(vmm.Disks, disk)
network := performance.Network{
Name: "eth0",
Rx: performance.NetworkUsage{
Bytes: uint64(100),
Kbps: 5,
Packets: 1,
},
Tx: performance.NetworkUsage{
Bytes: uint64(10),
Packets: 1,
},
}
vmm.Networks = append(vmm.Networks, network)
return vmm
}
// cpuUsageMetrics will return a populated CPUMetrics struct
func cpuUsageMetrics(count int, cpuMhz int) performance.CPUMetrics {
vmCPUs := make([]performance.CPUUsage, count, count)
total := count * cpuMhz
for i := range vmCPUs {
vmCPUs[i] = performance.CPUUsage{
ID: i,
MhzUsage: int64(cpuMhz),
}
}
return performance.CPUMetrics{
CPUs: vmCPUs,
Usage: calcVCPUUsage(total),
}
}
// calcUsage is a helper function that will take the total provdied usage
// and convert to percentage of total vCPU usage
func calcVCPUUsage(total int) float32 {
return float32(total) / (vcpuMhz * vcpuCount)
}
// calculateCPUPercentUnix is a copy from docker to test the percentage calculations
func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 {
var (
cpuPercent = 0.0
// calculate the change for the cpu usage of the container in between readings
cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU)
// calculate the change for the entire system between readings
systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem)
)
if systemDelta > 0.0 && cpuDelta > 0.0 {
cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0
}
return cpuPercent
}

View File

@@ -0,0 +1,135 @@
// 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 backends
import (
"fmt"
"net"
"net/http"
derr "github.com/docker/docker/api/errors"
"github.com/docker/libnetwork"
"github.com/docker/libnetwork/types"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
)
var notImplementedError = derr.NewErrorWithStatusCode(fmt.Errorf("not implemented"), http.StatusInternalServerError)
type endpoint struct {
ep *models.EndpointConfig
sc *models.ScopeConfig
}
// A system generated id for this endpoint.
func (e *endpoint) ID() string {
return e.ep.ID
}
// Name returns the name of this endpoint.
func (e *endpoint) Name() string {
return e.ep.Name
}
// Network returns the name of the vicnetwork to which this endpoint is attached.
func (e *endpoint) Network() string {
return e.ep.Scope
}
// Join joins the sandbox to the endpoint and populates into the sandbox
// the vicnetwork resources allocated for the endpoint.
func (e *endpoint) Join(sandbox libnetwork.Sandbox, options ...libnetwork.EndpointOption) error {
return notImplementedError
}
// Leave detaches the vicnetwork resources populated in the sandbox.
func (e *endpoint) Leave(sandbox libnetwork.Sandbox, options ...libnetwork.EndpointOption) error {
return notImplementedError
}
// Return certain operational data belonging to this endpoint
func (e *endpoint) Info() libnetwork.EndpointInfo {
return e
}
// DriverInfo returns a collection of driver operational data related to this endpoint retrieved from the driver
func (e *endpoint) DriverInfo() (map[string]interface{}, error) {
return nil, notImplementedError
}
// Delete and detaches this endpoint from the vicnetwork.
func (e *endpoint) Delete(force bool) error {
return notImplementedError
}
// Iface returns InterfaceInfo, go interface that can be used
// to get more information on the interface which was assigned to
// the endpoint by the driver. This can be used after the
// endpoint has been created.
func (e *endpoint) Iface() libnetwork.InterfaceInfo {
return e
}
// Gateway returns the IPv4 gateway assigned by the driver.
// This will only return a valid value if a container has joined the endpoint.
func (e *endpoint) Gateway() net.IP {
return net.ParseIP(e.sc.Gateway)
}
// GatewayIPv6 returns the IPv6 gateway assigned by the driver.
// This will only return a valid value if a container has joined the endpoint.
func (e *endpoint) GatewayIPv6() net.IP {
return nil
}
// StaticRoutes returns the list of static routes configured by the vicnetwork
// driver when the container joins a vicnetwork
func (e *endpoint) StaticRoutes() []*types.StaticRoute {
return nil
}
// Sandbox returns the attached sandbox if there, nil otherwise.
func (e *endpoint) Sandbox() libnetwork.Sandbox {
return newSandbox(e.ep.Container)
}
// MacAddress returns the MAC address assigned to the endpoint.
func (e *endpoint) MacAddress() net.HardwareAddr {
return nil
}
// Address returns the IPv4 address assigned to the endpoint.
func (e *endpoint) Address() *net.IPNet {
ip := net.ParseIP(e.ep.Address)
if ip == nil {
return nil
}
_, snet, err := net.ParseCIDR(e.sc.Subnet)
if err != nil {
return nil
}
return &net.IPNet{IP: ip, Mask: snet.Mask}
}
// AddressIPv6 returns the IPv6 address assigned to the endpoint.
func (e *endpoint) AddressIPv6() *net.IPNet {
return nil
}
func (e *endpoint) LinkLocalAddresses() []*net.IPNet {
return nil
}

View File

@@ -0,0 +1,38 @@
// 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 endpoint
import (
"fmt"
log "github.com/Sirupsen/logrus"
apinet "github.com/docker/docker/api/types/network"
)
func Alias(endpointConfig *apinet.EndpointSettings) []string {
var aliases []string
log.Debugf("EndpointsConfig: %#v", endpointConfig)
log.Debugf("Aliases: %s", endpointConfig.Aliases)
log.Debugf("Links: %s", endpointConfig.Links)
// Links are already in CONTAINERNAME:ALIAS format
aliases = endpointConfig.Links
// Converts aliases to ":ALIAS" format
for i := range endpointConfig.Aliases {
aliases = append(aliases, fmt.Sprintf(":%s", endpointConfig.Aliases[i]))
}
return aliases
}

View File

@@ -0,0 +1,291 @@
// Copyright 2017-2018 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 backends
//**** eventmonitor.go
//
// Handles monitoring of events from the portlayer. Events that are applicable to
// Docker events are then translated and published to the Docker event subscribers.
// NOTE: This does not handle all Docker events. In fact, most docker events are
// passively handled by API calls in the backend routers, with no feedback from
// the portlayer.
import (
"encoding/json"
"fmt"
"io"
"strings"
"sync"
log "github.com/Sirupsen/logrus"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
eventtypes "github.com/docker/docker/api/types/events"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"github.com/vmware/vic/lib/apiservers/engine/network"
"github.com/vmware/vic/lib/apiservers/engine/proxy"
"github.com/vmware/vic/lib/apiservers/portlayer/client/events"
plevents "github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/uid"
)
const (
containerDieEvent = "die"
containerDestroyEvent = "destroy"
containerStopEvent = "stop"
containerStartEvent = "start"
containerCreateEvent = "create"
containerRestartEvent = "restart"
containerAttachEvent = "attach"
containerDetachEvent = "detach"
containerKillEvent = "kill"
containerResizeEvent = "resize"
)
// for unit testing purposes
type eventproxy interface {
StreamEvents(ctx context.Context, out io.Writer) error
}
type eventpublisher interface {
PublishEvent(event plevents.BaseEvent)
}
type PlEventProxy struct {
}
type DockerEventPublisher struct {
}
type PortlayerEventMonitor struct {
stop chan struct{}
proxy eventproxy
publisher eventpublisher
}
// StreamEvents() handles all swagger interaction to the Portlayer's event manager
//
// Input:
// context and a io.Writer
func (ep PlEventProxy) StreamEvents(ctx context.Context, out io.Writer) error {
defer trace.End(trace.Begin(""))
plClient := PortLayerClient()
if plClient == nil {
return errors.InternalServerError("eventproxy.StreamEvents failed to get a portlayer client")
}
params := events.NewGetEventsParamsWithContext(ctx)
if _, err := plClient.Events.GetEvents(params, out); err != nil {
switch err := err.(type) {
case *events.GetEventsInternalServerError:
return errors.InternalServerError("Server error from the events port layer")
default:
//Check for EOF. Since the connection, transport, and data handling are
//encapsulated inside of Swagger, we can only detect EOF by checking the
//error string
if strings.Contains(err.Error(), proxy.SwaggerSubstringEOF) {
return nil
}
return errors.InternalServerError(fmt.Sprintf("Unknown error from the interaction port layer: %s", err))
}
}
return nil
}
func NewPortlayerEventMonitor(proxy eventproxy, publisher eventpublisher) *PortlayerEventMonitor {
return &PortlayerEventMonitor{proxy: proxy, publisher: publisher}
}
// Start() starts the portlayer event monitoring
func (m *PortlayerEventMonitor) Start() error {
defer trace.End(trace.Begin(""))
if m.stop != nil {
return fmt.Errorf("Portlayer event monitor: Already started")
}
m.stop = make(chan struct{})
go m.monitor()
return nil
}
// Stop() stops the portlayer event monitoring
func (m *PortlayerEventMonitor) Stop() {
defer trace.End(trace.Begin(""))
if m.stop != nil {
close(m.stop)
}
}
// monitor() establishes a streaming connection to the portlayer's event
// endpoint, decodes the results, translate it to Docker events if needed,
// and publishes the event to Docker event subscribers.
func (m *PortlayerEventMonitor) monitor() error {
defer trace.End(trace.Begin(""))
var wg sync.WaitGroup
errors := make(chan error, 2)
reader, writer := io.Pipe()
ctx, cancel := context.WithCancel(context.TODO())
// Start streaming events
wg.Add(1)
go func() {
var err error
defer wg.Done()
if err = m.proxy.StreamEvents(ctx, writer); err != nil {
if ctx.Err() != context.Canceled {
log.Warnf("Event streaming from portlayer returned: %#v", err)
}
}
if ctx.Err() == context.Canceled {
log.Infof("Event streaming from portlayer was cancelled")
return
}
errors <- err
writer.Close()
reader.Close()
}()
// Start decoding event stream json
wg.Add(1)
go func() {
var err error
var event plevents.BaseEvent
defer wg.Done()
decoder := json.NewDecoder(reader)
for decoder.More() {
if err = decoder.Decode(&event); err == nil {
m.publisher.PublishEvent(event)
}
}
errors <- err
reader.Close()
writer.Close()
}()
// Create a channel signaling when the waitgroup finishes
done := make(chan struct{})
go func() {
wg.Wait()
close(errors)
close(done)
}()
select {
case <-done:
for err := range errors {
if err != nil {
log.Warnf("Exiting Events Monitor: %#v", err)
return err
}
}
case <-m.stop:
cancel()
writer.Close()
reader.Close()
}
return nil
}
// PublishEvent translates select portlayer container events into Docker events
// and publishes to subscribers
func (p DockerEventPublisher) PublishEvent(event plevents.BaseEvent) {
// create a shortID for the container for logging purposes
containerShortID := uid.Parse(event.Ref).Truncate()
defer trace.End(trace.Begin(fmt.Sprintf("Event Monitor received eventID(%s) for container(%s) - %s", event.ID, containerShortID, event.Event)))
vc := cache.ContainerCache().GetContainer(event.Ref)
if vc == nil && event.Event != plevents.ContainerCreated {
log.Warnf("Event Monitor received eventID(%s) but container(%s) not in cache", event.ID, containerShortID)
return
}
// docker event attributes
var attrs map[string]string
switch event.Event {
case plevents.ContainerCreated:
syncContainerCache()
case plevents.ContainerStarted:
attrs = make(map[string]string)
actor := CreateContainerEventActorWithAttributes(vc, attrs)
EventService().Log(containerStartEvent, eventtypes.ContainerEventType, actor)
case plevents.ContainerStopped,
plevents.ContainerPoweredOff,
plevents.ContainerFailed:
// since we are going to make a call to the portLayer lets execute this in a go routine
go func() {
attrs = make(map[string]string)
// get the containerEngine
code, _ := NewContainerBackend().containerProxy.ExitCode(context.Background(), vc)
log.Infof("Sending die event for container(%s) with exitCode[%s] - eventID(%s)", containerShortID, code, event.ID)
// if the docker client is unable to convert the code to an int the client will return 125
attrs["exitCode"] = code
actor := CreateContainerEventActorWithAttributes(vc, attrs)
EventService().Log(containerDieEvent, eventtypes.ContainerEventType, actor)
// TODO: this really, really shouldn't be in the event publishing code - it's fine to have multiple consumers of events
// and this should be registered as a callback by the logic responsible for the MapPorts portion.
if err := network.UnmapPorts(vc.ContainerID, vc); err != nil {
log.Errorf("Event Monitor failed to unmap ports for container(%s): %s - eventID(%s)", containerShortID, err, event.ID)
}
// auto-remove if required
// TODO: this should be a separate event hook registered by logic outside of the publish events loop.
if vc.HostConfig.AutoRemove {
config := &types.ContainerRmConfig{
ForceRemove: true,
RemoveVolume: true,
}
err := NewContainerBackend().ContainerRm(vc.Name, config)
if err != nil {
log.Errorf("Event Monitor failed to remove container(%s) - eventID(%s): %s", containerShortID, event.ID, err)
}
}
}()
case plevents.ContainerRemoved:
attrs = make(map[string]string)
// pop the destroy event...
actor := CreateContainerEventActorWithAttributes(vc, attrs)
EventService().Log(containerDestroyEvent, eventtypes.ContainerEventType, actor)
if err := network.UnmapPorts(vc.ContainerID, vc); err != nil {
log.Errorf("Event Monitor failed to unmap ports for container(%s): %s - eventID(%s)", containerShortID, err, event.ID)
}
// remove from the container cache...
cache.ContainerCache().DeleteContainer(vc.ContainerID)
default:
// let everything else slide on by...
}
}

View File

@@ -0,0 +1,137 @@
// 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 backends
import (
"encoding/json"
"fmt"
"io"
"testing"
"time"
"golang.org/x/net/context"
"github.com/stretchr/testify/assert"
plevents "github.com/vmware/vic/lib/portlayer/event/events"
)
type MockEventProxy struct {
MockEvents []plevents.BaseEvent
Delay time.Duration
}
type MockEventPublisher struct {
MockEventChan chan plevents.BaseEvent
}
func (ep *MockEventProxy) StreamEvents(ctx context.Context, out io.Writer) error {
encoder := json.NewEncoder(out)
if encoder == nil {
return fmt.Errorf("Failed to create a json encoder")
}
for _, event := range ep.MockEvents {
if err := encoder.Encode(event); err != nil {
return err
}
time.Sleep(ep.Delay)
}
return nil
}
func (p *MockEventPublisher) PublishEvent(event plevents.BaseEvent) {
if p.MockEventChan != nil {
p.MockEventChan <- event
}
}
func TestStartStopMonitor(t *testing.T) {
proxy := MockEventProxy{
MockEvents: []plevents.BaseEvent{
{
Event: plevents.ContainerCreated,
Ref: "abc",
},
{
Event: plevents.ContainerStarted,
Ref: "abc",
},
{
Event: plevents.ContainerStopped,
Ref: "abc",
},
},
Delay: 1 * time.Second,
}
publisher := MockEventPublisher{
MockEventChan: make(chan plevents.BaseEvent, 3),
}
monitor := NewPortlayerEventMonitor(&proxy, &publisher)
var err error
// The actual tests
err = monitor.Start()
assert.Nil(t, err, "Expected monitor start to succeed, but received: %#v", err)
err = monitor.Start()
assert.NotEqual(t, err, nil, "Expected error but received nil on double start")
if err != nil {
assert.Contains(t, err.Error(), "Already started", "Expected already started error but received: %s", err)
}
monitor.Stop()
}
func TestEventMonitor(t *testing.T) {
proxy := MockEventProxy{
MockEvents: []plevents.BaseEvent{
{
Event: plevents.ContainerCreated,
Ref: "abc",
},
{
Event: plevents.ContainerStarted,
Ref: "abc",
},
{
Event: plevents.ContainerStopped,
Ref: "abc",
},
},
Delay: 0,
}
publisher := MockEventPublisher{
MockEventChan: make(chan plevents.BaseEvent, 3),
}
monitor := NewPortlayerEventMonitor(&proxy, &publisher)
var err error
// The actual tests
err = monitor.Start()
assert.Nil(t, err, "Expected monitor start to succeed, but received: %#v", err)
time.Sleep(1 * time.Second)
count := len(publisher.MockEventChan)
for i := 0; i < count; i++ {
event := <-publisher.MockEventChan
assert.Equal(t, event.Event, proxy.MockEvents[i].Event, "Expected to find event %s but found %s", proxy.MockEvents[i].Event, event.Event)
}
}

View File

@@ -0,0 +1,171 @@
// 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 executor
import (
"io"
"time"
"golang.org/x/net/context"
"github.com/docker/distribution"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
swarmtypes "github.com/docker/docker/api/types/swarm"
clustertypes "github.com/docker/docker/daemon/cluster/provider"
"github.com/docker/docker/plugin"
"github.com/docker/docker/reference"
"github.com/docker/libnetwork"
"github.com/docker/libnetwork/cluster"
networktypes "github.com/docker/libnetwork/types"
"github.com/docker/swarmkit/agent/exec"
"github.com/vmware/vic/lib/apiservers/engine/errors"
)
type SwarmBackend struct {
}
func (b SwarmBackend) CreateManagedNetwork(clustertypes.NetworkCreateRequest) error {
return nil
}
func (b SwarmBackend) DeleteManagedNetwork(name string) error {
return nil
}
func (b SwarmBackend) FindNetwork(idName string) (libnetwork.Network, error) {
return nil, nil
}
func (b SwarmBackend) SetupIngress(req clustertypes.NetworkCreateRequest, nodeIP string) error {
return nil
}
func (b SwarmBackend) PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
return nil
}
func (b SwarmBackend) CreateManagedContainer(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error) {
return container.ContainerCreateCreatedBody{}, nil
}
func (b SwarmBackend) ContainerStart(name string, hostConfig *container.HostConfig, checkpoint string, checkpointDir string) error {
return nil
}
func (b SwarmBackend) ContainerStop(name string, seconds *int) error {
return nil
}
// ContainerLogs hooks up a container's stdout and stderr streams
// configured with the given struct.
func (b SwarmBackend) ContainerLogs(ctx context.Context, containerName string, config *backend.ContainerLogsConfig, started chan struct{}) error {
return nil
}
func (b SwarmBackend) ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error {
return nil
}
func (b SwarmBackend) ActivateContainerServiceBinding(containerName string) error {
return nil
}
func (b SwarmBackend) DeactivateContainerServiceBinding(containerName string) error {
return nil
}
func (b SwarmBackend) UpdateContainerServiceConfig(containerName string, serviceConfig *clustertypes.ServiceConfig) error {
return nil
}
func (b SwarmBackend) ContainerInspectCurrent(name string, size bool) (*types.ContainerJSON, error) {
return nil, nil
}
func (b SwarmBackend) ContainerWaitWithContext(ctx context.Context, name string) error {
return nil
}
func (b SwarmBackend) ContainerRm(name string, config *types.ContainerRmConfig) error {
return nil
}
func (b SwarmBackend) ContainerKill(name string, sig uint64) error {
return nil
}
func (b SwarmBackend) SetContainerSecretStore(name string, store exec.SecretGetter) error {
return nil
}
func (b SwarmBackend) SetContainerSecretReferences(name string, refs []*swarmtypes.SecretReference) error {
return nil
}
func (b SwarmBackend) SystemInfo() (*types.Info, error) {
return nil, nil
}
func (b SwarmBackend) VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error) {
return nil, nil
}
func (b SwarmBackend) Containers(config *types.ContainerListOptions) ([]*types.Container, error) {
return nil, nil
}
func (b SwarmBackend) SetNetworkBootstrapKeys([]*networktypes.EncryptionKey) error {
return nil
}
func (b SwarmBackend) SetClusterProvider(provider cluster.Provider) {
}
func (b SwarmBackend) IsSwarmCompatible() error {
return errors.SwarmNotSupportedError()
}
func (b SwarmBackend) SubscribeToEvents(since, until time.Time, filter filters.Args) ([]events.Message, chan interface{}) {
return nil, nil
}
func (b SwarmBackend) UnsubscribeFromEvents(listener chan interface{}) {
}
func (b SwarmBackend) UpdateAttachment(string, string, string, *network.NetworkingConfig) error {
return nil
}
func (b SwarmBackend) WaitForDetachment(context.Context, string, string, string, string) error {
return nil
}
func (b SwarmBackend) GetRepository(context.Context, reference.NamedTagged, *types.AuthConfig) (distribution.Repository, bool, error) {
return nil, false, nil
}
func (b SwarmBackend) LookupImage(name string) (*types.ImageInspect, error) {
return nil, nil
}
func (b SwarmBackend) PluginManager() *plugin.Manager {
return nil
}

View File

@@ -0,0 +1,242 @@
// 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 filter
import (
"fmt"
"strconv"
"github.com/docker/docker/api/types"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
)
// reused from docker/docker/daemon/list.go
type ContainerListContext struct {
FilterContext
// Counter is the container iteration index for this context
Counter int
// ExitCode for the passed container
ExitCode int
// exitAllowed is a list of exit codes allowed to filter with
exitAllowed map[int]struct{}
// ContainerListOptions is the filters set by the user
*types.ContainerListOptions
}
// IncludeContainer will evaluate the filter criteria in listContext against the provided
// container and determine what action to take. There are three options:
// * IncludeAction
// * ExcludeAction
// * StopAction
func IncludeContainer(listContext *ContainerListContext, container *models.ContainerInfo) FilterAction {
// if we need to filter on name add to the listContext
if listContext.Filters.Include("name") {
// containerConfig allows for multiple names, but only 1 ever
// assigned
listContext.Name = container.ContainerConfig.Names[0]
}
// filter common requirements
act := filterCommon(&listContext.FilterContext, listContext.Filters)
if act != IncludeAction {
return act
}
// Stop iteration when the index is over the limit
if listContext.Limit > 0 && listContext.Counter == listContext.Limit {
return StopAction
}
// Do we have exit codes to evaluate
if len(listContext.exitAllowed) > 0 {
// Is the containers exitCode in the validatedList?
_, ok := listContext.exitAllowed[listContext.ExitCode]
// only include container whose exit code is in the list and that's
// not currently running and has been started previously
// note "Running" state is congruent with PortLayer and not docker
if !ok || container.ContainerConfig.State == "Running" || container.ProcessConfig.StartTime == 0 {
return ExcludeAction
}
}
state := DockerState(container.ContainerConfig.State)
// Do not include container if its status doesn't match the filter
if !listContext.Filters.Match("status", state) {
return ExcludeAction
}
// Filter on network name
if listContext.Filters.Include("network") {
netFilterValues := listContext.Filters.Get("network")
// Exclude the container if its network(s) match no supplied filter values
exists := false
for i := range netFilterValues {
for j := range container.Endpoints {
if netFilterValues[i] == container.Endpoints[j].Scope {
exists = true
break
}
}
}
if !exists {
return ExcludeAction
}
}
// Filter on volume name
if listContext.Filters.Include("volume") {
volFilterValues := listContext.Filters.Get("volume")
// Exclude the container if its volume(s) match no supplied filter values
exists := false
for i := range volFilterValues {
for j := range container.VolumeConfig {
if volFilterValues[i] == container.VolumeConfig[j].Name {
exists = true
break
}
}
}
if !exists {
return ExcludeAction
}
}
return IncludeAction
}
// ValidateContainerFilters validates that the container filters are
// valid docker filters / values and supported by VIC.
// The function reuses Docker's filter validation.
func ValidateContainerFilters(options *types.ContainerListOptions, acceptedFilters map[string]bool, unSupportedFilters map[string]bool) (*ContainerListContext, error) {
containerFilters := options.Filters
// ensure filter options are valid and supported by vic
if err := ValidateFilters(containerFilters, acceptedFilters, unSupportedFilters); err != nil {
return nil, err
}
// we need all containers for these options, so set the All flag
if options.Limit > 0 || options.Latest {
options.All = true
}
var s struct{}
filtExited := make(map[int]struct{})
err := containerFilters.WalkValues("exited", func(value string) error {
code, err := strconv.Atoi(value)
if err != nil {
return err
}
// add valid exit code to map
filtExited[code] = s
return nil
})
if err != nil {
return nil, err
}
err = containerFilters.WalkValues("status", func(value string) error {
if !IsValidDockerState(value) {
return fmt.Errorf("Unrecognised filter value for status: %s", value)
}
options.All = true
return nil
})
if err != nil {
return nil, err
}
// return value
listContext := &ContainerListContext{
FilterContext: FilterContext{},
exitAllowed: filtExited,
ContainerListOptions: options,
}
err = containerFilters.WalkValues("before", func(value string) error {
var err error
before := cache.ContainerCache().GetContainer(value)
if before == nil {
err = fmt.Errorf("No such container: %s", value)
} else {
listContext.BeforeID = &before.ContainerID
}
return err
})
if err != nil {
return nil, err
}
err = containerFilters.WalkValues("since", func(value string) error {
var err error
since := cache.ContainerCache().GetContainer(value)
if since == nil {
err = fmt.Errorf("No such container: %s", value)
} else {
listContext.SinceID = &since.ContainerID
}
return err
})
if err != nil {
return nil, err
}
return listContext, nil
}
// DockerState will attempt to transform the passed state
// to a valid docker state
// valid states are listed in the func IsValidContainerState
func DockerState(containerState string) string {
var state string
switch containerState {
case "Stopped":
state = "exited"
case "Running":
state = "running"
case "Created":
state = "created"
default:
// not sure what to do, so just return
// what was given
state = containerState
}
return state
}
// IsValidDockerState will verify the provided state is
// a valid docker container state
func IsValidDockerState(s string) bool {
if s != "paused" &&
s != "restarting" &&
s != "removing" &&
s != "running" &&
s != "dead" &&
s != "created" &&
s != "exited" {
return false
}
return true
}

View File

@@ -0,0 +1,213 @@
// 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 filter
import (
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
viccontainer "github.com/vmware/vic/lib/apiservers/engine/backends/container"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
)
func TestValidateContainerFilters(t *testing.T) {
options := &types.ContainerListOptions{
Filters: filters.NewArgs(),
}
options.Filters.Add("id", "12345")
options.Filters.Add("status", "running")
options.Filters.Add("exited", "143")
options.Filters.Add("exited", "127")
// valid status & exit
listContext, err := ValidateContainerFilters(options, acceptedPsFilterTags, unSupportedPsFilters)
assert.NoError(t, err)
// we should have two exit codes added to the list
// context
assert.Equal(t, 2, len(listContext.exitAllowed))
assert.Equal(t, options.Filters, listContext.Filters)
// remove valid status and replace w/invalid
options.Filters.Del("status", "running")
options.Filters.Add("status", "jackedup")
// invalid status
_, err = ValidateContainerFilters(options, acceptedPsFilterTags, unSupportedPsFilters)
assert.Error(t, err)
// remove valid exit code and replace w/invalid
options.Filters.Del("exited", "143")
options.Filters.Add("exited", "abc")
// invalid exit code
_, err = ValidateContainerFilters(options, acceptedPsFilterTags, unSupportedPsFilters)
assert.Error(t, err)
// add an invalid container option
options.Filters.Add("jojo", "jojo")
// invalid container filter option
_, err = ValidateContainerFilters(options, acceptedPsFilterTags, unSupportedPsFilters)
assert.Error(t, err)
options.Filters.Del("jojo", "jojo")
// add before filter
options.Filters = filters.NewArgs()
options.Filters.Add("before", "1234")
// fail because the before container isn't present
_, err = ValidateContainerFilters(options, acceptedPsFilterTags, unSupportedPsFilters)
assert.Error(t, err)
assert.Contains(t, err.Error(), "No such container:")
// add the container to the cache
containerBefore := &viccontainer.VicContainer{
ContainerID: "12345",
Name: "fuzzy",
}
cache.ContainerCache().AddContainer(containerBefore)
// successful before validation
_, err = ValidateContainerFilters(options, acceptedPsFilterTags, unSupportedPsFilters)
assert.NoError(t, err)
options.Filters.Add("since", "8888")
// fail because the since container isn't present
_, err = ValidateContainerFilters(options, acceptedPsFilterTags, unSupportedPsFilters)
assert.Error(t, err)
assert.Contains(t, err.Error(), "No such container:")
}
func TestDockerState(t *testing.T) {
vicState := make([]string, 4, 4)
vicState[0] = "Running"
vicState[1] = "Stopped"
vicState[2] = "Created"
vicState[3] = "sammy"
docker := make(map[string]bool)
docker["created"] = true
docker["running"] = true
docker["exited"] = true
// This is not a docker state, but is used to validate the
// default switch in the tested function
docker["sammy"] = false
for i := range vicState {
if _, ok := docker[DockerState(vicState[i])]; !ok {
t.Errorf("vicState doesn't map to docker state: %s", vicState[i])
}
}
}
func TestIncludeContainer(t *testing.T) {
ep := &models.EndpointConfig{
Scope: "bridge",
}
eps := make([]*models.EndpointConfig, 0)
vol := &models.VolumeConfig{
Name: "fooVol",
}
vols := make([]*models.VolumeConfig, 0)
contain := &models.ContainerInfo{
ContainerConfig: &models.ContainerConfig{
Names: []string{"jojo"},
},
ProcessConfig: &models.ProcessConfig{},
VolumeConfig: append(vols, vol),
Endpoints: append(eps, ep),
}
listCtx := &ContainerListContext{
ContainerListOptions: &types.ContainerListOptions{
Filters: filters.NewArgs()},
}
listCtx.Filters.Add("name", "jojo")
assert.Equal(t, IncludeAction, IncludeContainer(listCtx, contain))
listCtx.Limit = 1
listCtx.Counter = listCtx.Limit
assert.Equal(t, StopAction, IncludeContainer(listCtx, contain))
// reset counter
listCtx.Counter = 0
// create exited map
var s struct{}
listCtx.exitAllowed = make(map[int]struct{})
listCtx.exitAllowed[137] = s
// exclude since no container exit code
assert.Equal(t, ExcludeAction, IncludeContainer(listCtx, contain))
startTime := int64(4444)
contain.ProcessConfig.StartTime = startTime
listCtx.ExitCode = 137
assert.Equal(t, IncludeAction, IncludeContainer(listCtx, contain))
// test network name
listCtx.Filters = filters.NewArgs()
listCtx.Filters.Add("network", "bridge")
assert.Equal(t, IncludeAction, IncludeContainer(listCtx, contain))
listCtx.Filters.Add("network", "fooNet")
assert.Equal(t, IncludeAction, IncludeContainer(listCtx, contain))
listCtx.Filters.Del("network", "bridge")
listCtx.Filters.Del("network", "fooNet")
listCtx.Filters.Add("network", "barNet")
assert.Equal(t, ExcludeAction, IncludeContainer(listCtx, contain))
listCtx.Filters = filters.NewArgs()
listCtx.Filters.Add("network", "missed")
assert.Equal(t, ExcludeAction, IncludeContainer(listCtx, contain))
// test volume name
listCtx.Filters = filters.NewArgs()
listCtx.Filters.Add("volume", "fooVol")
assert.Equal(t, IncludeAction, IncludeContainer(listCtx, contain))
listCtx.Filters.Add("volume", "barVol")
assert.Equal(t, IncludeAction, IncludeContainer(listCtx, contain))
listCtx.Filters.Del("volume", "fooVol")
listCtx.Filters.Del("volume", "barVol")
listCtx.Filters.Add("volume", "quxVol")
assert.Equal(t, ExcludeAction, IncludeContainer(listCtx, contain))
// test volume and network filters together
listCtx.Filters = filters.NewArgs()
listCtx.Filters.Add("volume", "fooVol")
listCtx.Filters.Add("network", "bridge")
assert.Equal(t, IncludeAction, IncludeContainer(listCtx, contain))
listCtx.Filters.Add("volume", "barVol")
listCtx.Filters.Add("network", "fooNet")
assert.Equal(t, IncludeAction, IncludeContainer(listCtx, contain))
listCtx.Filters.Del("volume", "fooVol")
assert.Equal(t, ExcludeAction, IncludeContainer(listCtx, contain))
listCtx.Filters = filters.NewArgs()
listCtx.Filters.Add("status", "stopped")
assert.Equal(t, ExcludeAction, IncludeContainer(listCtx, contain))
}

View File

@@ -0,0 +1,77 @@
// 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 filter
import (
"github.com/docker/docker/api/types/filters"
)
// FilterAction represents possible results during filtering
type FilterAction int
const (
IncludeAction FilterAction = iota
ExcludeAction
StopAction
)
// FilterContext will hold the common filter requirements
type FilterContext struct {
// ID of object to filter
ID string
// Name of object to filter
Name string
// BeforeID is the filter to ignore objects that appear before the one given
BeforeID *string
// SinceID is the filter to stop iterating
SinceID *string
// Labels of object to filter
Labels map[string]string
}
// filterCommon will filter the common criteria across objects
func filterCommon(filterContext *FilterContext, cmdFilters filters.Args) FilterAction {
// have we made it to the beforeID
if filterContext.BeforeID != nil {
if filterContext.ID == *filterContext.BeforeID {
filterContext.BeforeID = nil
}
return ExcludeAction
}
// Stop iteration when the object arrives to the filter object
if filterContext.SinceID != nil {
if filterContext.ID == *filterContext.SinceID {
return StopAction
}
}
// Do not include object if any of the labels don't match
if !cmdFilters.MatchKVList("label", filterContext.Labels) {
return ExcludeAction
}
// Do not include if the id doesn't match
if !cmdFilters.Match("id", filterContext.ID) {
return ExcludeAction
}
if !cmdFilters.Match("name", filterContext.Name) {
return ExcludeAction
}
return IncludeAction
}

View File

@@ -0,0 +1,69 @@
// 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 filter
import (
"testing"
"github.com/docker/docker/api/types/filters"
"github.com/stretchr/testify/assert"
)
func TestFilterCommon(t *testing.T) {
cmdFilters := filters.NewArgs()
id := "123"
before := "456"
fContext := &FilterContext{
ID: id,
}
// no common filter
assert.Equal(t, IncludeAction, filterCommon(fContext, cmdFilters))
// exclude on Name
cmdFilters.Add("name", "jojo")
assert.Equal(t, ExcludeAction, filterCommon(fContext, cmdFilters))
// exclude on ID
cmdFilters.Add("id", before)
assert.Equal(t, ExcludeAction, filterCommon(fContext, cmdFilters))
// we've hit the before id exclude object
fContext.ID = before
fContext.BeforeID = &before
cmdFilters.Add("before", before)
assert.Equal(t, ExcludeAction, filterCommon(fContext, cmdFilters))
// stop due to since
since := "859"
fContext.SinceID = &since
fContext.ID = since
cmdFilters.Add("since", since)
assert.Equal(t, StopAction, filterCommon(fContext, cmdFilters))
// exclude based on label mismatch
fContext.Labels = createLabels()
fContext.ID = id
cmdFilters.Add("label", "joe")
assert.Equal(t, ExcludeAction, filterCommon(fContext, cmdFilters))
}
func createLabels() map[string]string {
labels := make(map[string]string)
labels["prod"] = "ATX"
labels["brown"] = "fox"
return labels
}

View File

@@ -0,0 +1,136 @@
// 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 filter
import (
"fmt"
"path"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types/filters"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
)
type ImageListContext struct {
FilterContext
// Tags for an image filtered by reference
Tags []string
// Digests for an image filtered by reference
Digests []string
}
/*
* ValidateImageFilters will validate the image filters are
* valid docker filters / values and supported by vic.
*
* The function will reuse dockers filter validation
*
*/
func ValidateImageFilters(cmdFilters filters.Args, acceptedFilters map[string]bool, unSupportedFilters map[string]bool) (*ImageListContext, error) {
// ensure filter options are valid and supported by vic
if err := ValidateFilters(cmdFilters, acceptedFilters, unSupportedFilters); err != nil {
return nil, err
}
// return value
imgFilterContext := &ImageListContext{}
err := cmdFilters.WalkValues("before", func(value string) error {
before, err := cache.ImageCache().Get(value)
if before == nil {
err = fmt.Errorf("No such image: %s", value)
} else {
imgFilterContext.BeforeID = &before.ImageID
}
return err
})
if err != nil {
return nil, err
}
err = cmdFilters.WalkValues("since", func(value string) error {
since, err := cache.ImageCache().Get(value)
if since == nil {
err = fmt.Errorf("No such image: %s", value)
} else {
imgFilterContext.SinceID = &since.ImageID
}
return err
})
if err != nil {
return nil, err
}
return imgFilterContext, nil
}
/*
* IncludeImage will evaluate the filter criteria in filterContext against the provided
* image and determine what action to take. There are three options:
* * IncludeAction
* * ExcludeAction
* * StopAction
*
*/
func IncludeImage(imgFilters filters.Args, listContext *ImageListContext) FilterAction {
// filter common requirements
act := filterCommon(&listContext.FilterContext, imgFilters)
if act != IncludeAction {
return act
}
// filter on image reference
if imgFilters.Include("reference") {
// references for this imageID
refs := cache.RepositoryCache().References(listContext.ID)
// reference filters
refFilters := imgFilters.Get("reference")
// reset the tags / digests
listContext.Tags = nil
listContext.Digests = nil
// iterate of reporsitory references and filters
for _, ref := range refs {
for _, rf := range refFilters {
// match on complete ref ie. busybox:latest
// #nosec: Errors unhandled.
matchRef, _ := path.Match(rf, ref.String())
// match on repo only ie. busybox
// #nosec: Errors unhandled.
matchName, _ := path.Match(rf, ref.Name())
// if either matched then add to tag / digest
if matchRef || matchName {
if _, ok := ref.(reference.Canonical); ok {
listContext.Digests = append(listContext.Digests, ref.String())
}
if _, ok := ref.(reference.NamedTagged); ok {
listContext.Tags = append(listContext.Tags, ref.String())
}
}
}
}
// if there were no reference matches then exclude the image
if len(listContext.Tags) == 0 && len(listContext.Digests) == 0 {
return ExcludeAction
}
}
return IncludeAction
}

View File

@@ -0,0 +1,120 @@
// 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 filter
import (
"fmt"
"testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/reference"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
"github.com/vmware/vic/lib/metadata"
)
func loadImageCache(repo string, imageCount int, t *testing.T) {
for i := 0; i < imageCount; i++ {
id := fmt.Sprintf("120%d", i)
tag := fmt.Sprintf("1.0%d", i+1)
ref := fmt.Sprintf("%s:%s", repo, tag)
img := &metadata.ImageConfig{
ImageID: id,
Tags: []string{tag},
Name: repo,
Reference: ref,
}
img.Created = time.Now().UTC()
img.ID = id
img.Config = &container.Config{}
cache.ImageCache().Add(img)
named, err := reference.ParseNamed(ref)
if err != nil {
t.Fatalf("Error while parsing reference %s: %#v", ref, err)
}
cache.RepositoryCache().AddReference(named, id, true, id, false)
}
assert.Equal(t, imageCount, len(cache.ImageCache().GetImages()))
}
func TestValidateImageFilters(t *testing.T) {
loadImageCache("busyboxy", 5, t)
cmdFilters := filters.NewArgs()
cmdFilters.Add("dangling", "true")
_, err := ValidateImageFilters(cmdFilters, acceptedImageFilterTags, unSupportedImageFilters)
assert.Error(t, err)
cmdFilters.Del("dangling", "true")
cmdFilters.Add("before", "1200")
_, err = ValidateImageFilters(cmdFilters, acceptedImageFilterTags, unSupportedImageFilters)
assert.Error(t, err)
assert.Contains(t, err.Error(), "No such image")
cmdFilters.Del("before", "1200")
cmdFilters.Add("since", "1200")
_, err = ValidateImageFilters(cmdFilters, acceptedImageFilterTags, unSupportedImageFilters)
assert.Error(t, err)
assert.Contains(t, err.Error(), "No such image")
}
func TestIncludeImage(t *testing.T) {
cmdFilters := filters.NewArgs()
cmdFilters.Add("before", "busyboxy:1.03")
imageContext, err := ValidateImageFilters(cmdFilters, acceptedImageFilterTags, unSupportedImageFilters)
assert.NoError(t, err)
assert.Equal(t, "1202", *imageContext.BeforeID)
imageContext.ID = "1202"
action := IncludeImage(cmdFilters, imageContext)
assert.Equal(t, ExcludeAction, action)
imageContext.ID = "1200"
action = IncludeImage(cmdFilters, imageContext)
assert.Equal(t, IncludeAction, action)
cmdFilters.Del("before", "busyboxy:1.03")
cmdFilters.Add("since", "busyboxy:1.01")
imageContext, err = ValidateImageFilters(cmdFilters, acceptedImageFilterTags, unSupportedImageFilters)
assert.NoError(t, err)
imageContext.ID = "1200"
action = IncludeImage(cmdFilters, imageContext)
assert.Equal(t, StopAction, action)
cmdFilters.Del("since", "busyboxy:1.01")
cmdFilters.Add("reference", "busy*")
imageContext.SinceID = nil
action = IncludeImage(cmdFilters, imageContext)
assert.Equal(t, IncludeAction, action)
// remove previous filter and reset tags / digests
cmdFilters.Del("reference", "busy*")
imageContext.Tags = []string{}
imageContext.Digests = []string{}
cmdFilters.Add("reference", "busyboxy:1.01")
action = IncludeImage(cmdFilters, imageContext)
assert.Equal(t, action, IncludeAction)
assert.EqualValues(t, 1, len(imageContext.Tags))
}

View File

@@ -0,0 +1,59 @@
// 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 filter
import (
"fmt"
"github.com/docker/docker/api/types/filters"
)
/*
* ValidateFilters will evalute the provided filters against the docker acceptedFilters and
* the filters vic currently doesn't support (unSupportedFilters)
*/
func ValidateFilters(cmdFilters filters.Args, acceptedFilters map[string]bool, unSupportedFilters map[string]bool) error {
var err error
// ensure provided filter args are accepted
if err = cmdFilters.Validate(acceptedFilters); err != nil {
return err
}
// verify that vic supports the provided filter args
// will only make it here if all the filters are valid
if err = validateSupport(cmdFilters, unSupportedFilters); err != nil {
return err
}
return err
}
/*
* validateSupport will ensure the provided filter arguments are implemented
* by vic
*/
func validateSupport(cmdFilters filters.Args, unSupported map[string]bool) error {
for filter := range unSupported {
vals := cmdFilters.Get(filter)
if len(vals) > 0 {
return fmt.Errorf("filter %s is not currently supported by vic", filter)
}
}
return nil
}

View File

@@ -0,0 +1,111 @@
// 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 filter
import (
"testing"
"github.com/docker/docker/api/types/filters"
"github.com/stretchr/testify/assert"
)
// TODO: what can we do here...don't like this, but can't
// reference upper package due to import constraints
// valid filters as of docker commit 49bf474
var acceptedImageFilterTags = map[string]bool{
"dangling": true,
"label": true,
"before": true,
"since": true,
"reference": true,
}
// currently not supported by vic
var unSupportedImageFilters = map[string]bool{
"dangling": false,
}
// valid filters as of docker commit 49bf474
var acceptedPsFilterTags = map[string]bool{
"ancestor": true,
"before": true,
"exited": true,
"id": true,
"isolation": true,
"label": true,
"name": true,
"status": true,
"health": true,
"since": true,
"volume": true,
"network": true,
"is-task": true,
}
// currently not supported by vic
var unSupportedPsFilters = map[string]bool{
"ancestor": false,
"health": false,
"isolation": false,
"is-task": false,
}
// valid volume filters as of Docker v1.13
var acceptedVolumeFilterTags = map[string]bool{
"dangling": true,
"name": true,
"driver": true,
"label": true,
}
func TestValidateFilters(t *testing.T) {
args := filters.NewArgs()
args.Add("id", "12345")
args.Add("name", "jojo")
// valid container filter
assert.NoError(t, ValidateFilters(args, acceptedPsFilterTags, unSupportedPsFilters))
// unsupported container filter
args.Add("isolation", "windows")
assert.Error(t, ValidateFilters(args, acceptedPsFilterTags, unSupportedPsFilters))
// invalid container filter
args.Add("failure", "yoyo")
assert.Error(t, ValidateFilters(args, acceptedPsFilterTags, unSupportedPsFilters))
// unsupported image filter
args = filters.NewArgs()
args.Add("dangling", "true")
assert.Error(t, ValidateFilters(args, acceptedImageFilterTags, unSupportedImageFilters))
// invalid image filter
args.Add("failure", "yoyo")
assert.Error(t, ValidateFilters(args, acceptedImageFilterTags, unSupportedImageFilters))
// valid image filter
args = filters.NewArgs()
args.Add("label", "124")
assert.NoError(t, ValidateFilters(args, acceptedImageFilterTags, unSupportedImageFilters))
// valid volume filter
args = filters.NewArgs()
args.Add("name", "vol")
assert.NoError(t, ValidateFilters(args, acceptedVolumeFilterTags, nil))
// invalid volume filter
args.Add("mountpoint", "/volumes")
assert.Error(t, ValidateFilters(args, acceptedVolumeFilterTags, nil))
}

View File

@@ -0,0 +1,87 @@
// 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 filter
import (
"fmt"
"github.com/docker/docker/api/types/filters"
)
// VolumeFilterContext stores volume information used while filtering
type VolumeFilterContext struct {
FilterContext
// Dangling is the value of the dangling filter if supplied
Dangling bool
// Joined tells whether the volume is joined to a container or not
Joined bool
// Driver is the volume's driver
Driver string
}
// ValidateVolumeFilters checks that the supplied filters are valid and supported
// and returns a context used in the IncludeVolume func.
func ValidateVolumeFilters(volFilters filters.Args, acceptedFilters, unSupportedFilters map[string]bool) (*VolumeFilterContext, error) {
if err := ValidateFilters(volFilters, acceptedFilters, unSupportedFilters); err != nil {
return nil, err
}
volFilterContext := &VolumeFilterContext{}
// Set value of dangling filter if it's supplied
if volFilters.Include("dangling") {
// Validate dangling filter's usage (per Docker code)
// Supported formats: dangling={true, false, 1, 0}
if volFilters.ExactMatch("dangling", "true") || volFilters.ExactMatch("dangling", "1") {
volFilterContext.Dangling = true
} else if !volFilters.ExactMatch("dangling", "false") && !volFilters.ExactMatch("dangling", "0") {
return nil, fmt.Errorf("invalid filter 'dangling=%s'", volFilters.Get("dangling"))
}
}
return volFilterContext, nil
}
// IncludeVolume evaluates volume filters and the filter context and returns
// an action to indicate whether to include the volume in the output or not.
func IncludeVolume(volumeFilters filters.Args, volFilterContext *VolumeFilterContext) FilterAction {
// Filter by name and label
action := filterCommon(&volFilterContext.FilterContext, volumeFilters)
if action != IncludeAction {
return action
}
if volumeFilters.Include("dangling") {
// Exclude the volume if dangling=false or it is joined,
// and if dangling=true or it is not joined
if (!volFilterContext.Dangling || volFilterContext.Joined) && (volFilterContext.Dangling || !volFilterContext.Joined) {
return ExcludeAction
}
}
if volumeFilters.Include("driver") {
if !volumeFilters.ExactMatch("driver", volFilterContext.Driver) {
return ExcludeAction
}
}
return IncludeAction
}

View File

@@ -0,0 +1,114 @@
// 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 filter
import (
"testing"
"github.com/docker/docker/api/types/filters"
"github.com/stretchr/testify/assert"
)
func TestValidateVolumeFilters(t *testing.T) {
// Valid filters
volFilters := filters.NewArgs()
volFilters.Add("dangling", "true")
volFilters.Add("name", "mulder")
volFilters.Add("driver", "aliens")
volFilterContext, err := ValidateVolumeFilters(volFilters, acceptedVolumeFilterTags, nil)
assert.NoError(t, err)
assert.Equal(t, volFilterContext.Dangling, true)
// Change a filter's value
volFilters.Del("dangling", "true")
volFilters.Add("dangling", "false")
volFilterContext, err = ValidateVolumeFilters(volFilters, acceptedVolumeFilterTags, nil)
assert.NoError(t, err)
assert.Equal(t, volFilterContext.Dangling, false)
// Valid filter with invalid value
volFilters.Del("dangling", "false")
volFilters.Add("dangling", "no")
volFilterContext, err = ValidateVolumeFilters(volFilters, acceptedVolumeFilterTags, nil)
assert.Error(t, err)
// Invalid filter
volFilters.Add("mountpoint", "/volumes")
_, err = ValidateVolumeFilters(volFilters, acceptedVolumeFilterTags, nil)
assert.Error(t, err)
}
func TestIncludeVolume(t *testing.T) {
// Filter by dangling=true
volFilters := filters.NewArgs()
volFilters.Add("dangling", "true")
volFilterContext := &VolumeFilterContext{
FilterContext: FilterContext{
Name: "scully",
Labels: map[string]string{"samplelabel": ""},
},
Driver: "science",
Joined: false,
Dangling: true,
}
action := IncludeVolume(volFilters, volFilterContext)
assert.Equal(t, action, IncludeAction)
// Filter by dangling=false
volFilters.Del("dangling", "true")
volFilters.Add("dangling", "false")
volFilterContext.Dangling = false
action = IncludeVolume(volFilters, volFilterContext)
assert.Equal(t, action, ExcludeAction)
// Filter by name and dangling=false
volFilters.Add("name", "scul")
volFilterContext.Joined = true
action = IncludeVolume(volFilters, volFilterContext)
assert.Equal(t, action, IncludeAction)
// Filter by name, dangling=false and driver=science
volFilters.Add("driver", "science")
action = IncludeVolume(volFilters, volFilterContext)
assert.Equal(t, action, IncludeAction)
// Filter by incorrect name, dangling=false and incorrect driver
volFilterContext.Name = "mulder"
volFilterContext.Driver = "aliens"
action = IncludeVolume(volFilters, volFilterContext)
assert.Equal(t, action, ExcludeAction)
// Filter by name, dangling=false and incorrect driver
volFilterContext.Name = "scully"
volFilterContext.Driver = "science"
volFilters.Del("driver", "science")
volFilters.Add("driver", "sci")
action = IncludeVolume(volFilters, volFilterContext)
assert.Equal(t, action, ExcludeAction)
// Filter by correct label
volFilters = filters.NewArgs()
volFilters.Add("label", "samplelabel")
action = IncludeVolume(volFilters, volFilterContext)
assert.Equal(t, action, IncludeAction)
// Filter by incorrect label
volFilters.Del("label", "samplelabel")
volFilters.Add("label", "wronglabel")
action = IncludeVolume(volFilters, volFilterContext)
assert.Equal(t, action, ExcludeAction)
}

View File

@@ -0,0 +1,501 @@
// Copyright 2016-2018 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 backends
import (
"fmt"
"io"
"net/url"
"os"
"strings"
"time"
log "github.com/Sirupsen/logrus"
"golang.org/x/net/context"
"github.com/docker/distribution/digest"
"github.com/docker/docker/api/types"
eventtypes "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/reference"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
vicfilter "github.com/vmware/vic/lib/apiservers/engine/backends/filter"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"github.com/vmware/vic/lib/apiservers/portlayer/client/storage"
"github.com/vmware/vic/lib/imagec"
"github.com/vmware/vic/lib/metadata"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/uid"
"github.com/vmware/vic/pkg/vsphere/sys"
)
// valid filters as of docker commit 49bf474
var acceptedImageFilterTags = map[string]bool{
"dangling": true,
"label": true,
"before": true,
"since": true,
"reference": true,
}
// currently not supported by vic
var unSupportedImageFilters = map[string]bool{
"dangling": false,
}
type ImageBackend struct {
}
func NewImageBackend() *ImageBackend {
return &ImageBackend{}
}
func (i *ImageBackend) Exists(containerName string) bool {
return false
}
// TODO fix the errors so the client doesnt print the generic POST or DELETE message
func (i *ImageBackend) ImageDelete(imageRef string, force, prune bool) ([]types.ImageDelete, error) {
defer trace.End(trace.Begin(imageRef))
var (
deletedRes []types.ImageDelete
userRefIsID bool
)
// Use the image cache to go from the reference to the ID we use in the image store
img, err := cache.ImageCache().Get(imageRef)
if err != nil {
return nil, err
}
tags := img.Tags
digests := img.Digests
// did the user pass an id or partial id
userRefIsID = cache.ImageCache().IsImageID(imageRef)
// do we have any reference conflicts
if len(tags) > 1 && userRefIsID && !force {
t := uid.Parse(img.ImageID).Truncate()
return nil,
fmt.Errorf("conflict: unable to delete %s (must be forced) - image is referenced in one or more repositories", t)
}
// if we have an ID or only 1 tag/digest lets delete the vmdk(s) via the PL
if userRefIsID || len(tags) == 1 || len(digests) == 1 {
log.Infof("Deleting image via PL %s (%s)", img.ImageID, img.ID)
// storeName is the uuid of the host this service is running on.
storeName, err := sys.UUID()
if err != nil {
return nil, err
}
// We're going to delete all of the images in the layer branch starting
// at the given leaf. BUT! we need to keep the images which may be
// referenced by tags. Therefore, we need to assemble a list of images
// (by URI) which are referred to by tags.
allImages := cache.ImageCache().GetImages()
keepNodes := make([]string, len(allImages))
for idx, node := range allImages {
imgURL, err := util.ImageURL(storeName, node.ImageID)
if err != nil {
return nil, err
}
keepNodes[idx] = imgURL.String()
}
params := storage.NewDeleteImageParamsWithContext(ctx).WithStoreName(storeName).WithID(img.ID).WithKeepNodes(keepNodes)
// TODO: This will fail if any containerVMs are referencing the vmdk - vanilla docker
// allows the removal of an image (via force flag) even if a container is referencing it
// should vic?
res, err := PortLayerClient().Storage.DeleteImage(params)
// We may have deleted images despite error. Account for that in the cache.
if res != nil {
for _, deletedImage := range res.Payload {
// map the layer id to the blob sum so the ids map to what we
// present to the user on pull
id := deletedImage.ID
i, err := imagec.LayerCache().Get(deletedImage.ID)
if err == nil {
id = i.Layer.BlobSum
}
// remove the layer from the layer cache (used by imagec)
imagec.LayerCache().Remove(deletedImage.ID)
// form the response
imageDeleted := types.ImageDelete{Deleted: strings.TrimPrefix(id, "sha256:")}
deletedRes = append(deletedRes, imageDeleted)
}
if err := imagec.LayerCache().Save(); err != nil {
return nil, fmt.Errorf("failed to save layer cache: %s", err)
}
}
if err != nil {
switch err := err.(type) {
case *storage.DeleteImageLocked:
return nil, fmt.Errorf("Failed to remove image %q: %s", imageRef, err.Payload.Message)
default:
return nil, err
}
}
// we've deleted the image so remove from cache
cache.ImageCache().RemoveImageByConfig(img)
if err := cache.ImageCache().Save(); err != nil {
return nil, fmt.Errorf("failed to save image cache: %s", err)
}
actor := CreateImageEventActorWithAttributes(imageRef, imageRef, map[string]string{})
EventService().Log("delete", eventtypes.ImageEventType, actor)
} else {
// only untag the ref supplied
n, err := reference.ParseNamed(imageRef)
if err != nil {
return nil, fmt.Errorf("unable to parse reference(%s): %s", imageRef, err.Error())
}
tag := reference.WithDefaultTag(n)
tags = []string{tag.String()}
actor := CreateImageEventActorWithAttributes(imageRef, imageRef, map[string]string{})
EventService().Log("untag", eventtypes.ImageEventType, actor)
}
// loop thru and remove from repoCache
for i := range tags {
// remove from cache, but don't save -- we'll do that afer all
// updates
// #nosec: Errors unhandled.
refNamed, _ := cache.RepositoryCache().Remove(tags[i], false)
deletedRes = append(deletedRes, types.ImageDelete{Untagged: refNamed})
}
for i := range digests {
// #nosec: Errors unhandled.
refNamed, _ := cache.RepositoryCache().Remove(digests[i], false)
deletedRes = append(deletedRes, types.ImageDelete{Untagged: refNamed})
}
// save repo now -- this will limit the number of PL
// calls to one per rmi call
err = cache.RepositoryCache().Save()
if err != nil {
return nil, fmt.Errorf("Untag error: %s", err.Error())
}
return deletedRes, err
}
func (i *ImageBackend) ImageHistory(imageName string) ([]*types.ImageHistory, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "ImageHistory")
}
func (i *ImageBackend) Images(imageFilters filters.Args, all bool, withExtraAttrs bool) ([]*types.ImageSummary, error) {
defer trace.End(trace.Begin(fmt.Sprintf("imageFilters: %#v", imageFilters)))
// validate filters for accuracy and support
filterContext, err := vicfilter.ValidateImageFilters(imageFilters, acceptedImageFilterTags, unSupportedImageFilters)
if err != nil {
return nil, err
}
// get all images
images := cache.ImageCache().GetImages()
result := make([]*types.ImageSummary, 0, len(images))
imageLoop:
for i := range images {
// provide filter with current ImageID
filterContext.ID = images[i].ImageID
// provide image labels
if images[i].Config != nil {
filterContext.Labels = images[i].Config.Labels
}
// determine if image should be part of list
action := vicfilter.IncludeImage(imageFilters, filterContext)
switch action {
case vicfilter.ExcludeAction:
continue imageLoop
case vicfilter.StopAction:
break imageLoop
}
// if we are here then add image
dockerImage := convertV1ImageToDockerImage(images[i])
// reference is a filter, so we must add the tags / digests
// identified by the filter
if imageFilters.Include("reference") {
dockerImage.RepoTags = filterContext.Tags
dockerImage.RepoDigests = filterContext.Digests
}
result = append(result, dockerImage)
}
return result, nil
}
// Docker Inspect. LookupImage looks up an image by name and returns it as an
// ImageInspect structure.
func (i *ImageBackend) LookupImage(name string) (*types.ImageInspect, error) {
defer trace.End(trace.Begin("LookupImage (docker inspect)"))
imageConfig, err := cache.ImageCache().Get(name)
if err != nil {
return nil, err
}
return imageConfigToDockerImageInspect(imageConfig, ProductName()), nil
}
func (i *ImageBackend) TagImage(imageName, repository, tag string) error {
img, err := cache.ImageCache().Get(imageName)
if err != nil {
return err
}
newTag, err := reference.WithName(repository)
if err != nil {
return err
}
if tag != "" {
if newTag, err = reference.WithTag(newTag, tag); err != nil {
return err
}
}
// place tag in repo and save to portLayer k/v store
err = cache.RepositoryCache().AddReference(newTag, img.ImageID, true, "", true)
if err != nil {
return err
}
actor := CreateImageEventActorWithAttributes(imageName, newTag.String(), map[string]string{})
EventService().Log("tag", eventtypes.ImageEventType, actor)
return nil
}
func (i *ImageBackend) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPruneReport, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "ImagesPrune")
}
func (i *ImageBackend) LoadImage(inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
return errors.APINotSupportedMsg(ProductName(), "LoadImage")
}
func (i *ImageBackend) ImportImage(src string, repository, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error {
return errors.APINotSupportedMsg(ProductName(), "ImportImage")
}
func (i *ImageBackend) ExportImage(names []string, outStream io.Writer) error {
return errors.APINotSupportedMsg(ProductName(), "ExportImage")
}
func (i *ImageBackend) PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
defer trace.End(trace.Begin(""))
log.Debugf("PullImage: image = %s, tag = %s, metaheaders = %+v\n", image, tag, metaHeaders)
//***** Code from Docker 1.13 PullImage to convert image and tag to a ref
image = strings.TrimSuffix(image, ":")
ref, err := reference.ParseNamed(image)
if err != nil {
return err
}
if tag != "" {
// The "tag" could actually be a digest.
var dgst digest.Digest
dgst, err = digest.ParseDigest(tag)
if err == nil {
ref, err = reference.WithDigest(reference.TrimNamed(ref), dgst)
} else {
ref, err = reference.WithTag(ref, tag)
}
if err != nil {
return err
}
}
//*****
options := imagec.Options{
Destination: os.TempDir(),
Reference: ref,
Timeout: imagec.DefaultHTTPTimeout,
Outstream: outStream,
}
portLayerServer := PortLayerServer()
if portLayerServer != "" {
options.Host = portLayerServer
}
ic := imagec.NewImageC(options, streamformatter.NewJSONStreamFormatter())
ic.ParseReference()
// create url from hostname
hostnameURL, err := url.Parse(ic.Registry)
if err != nil || hostnameURL.Hostname() == "" {
hostnameURL, err = url.Parse("//" + ic.Registry)
if err != nil {
log.Infof("Error parsing hostname %s during registry access: %s", ic.Registry, err.Error())
}
}
// Check if url is contained within set of whitelisted or insecure registries
regctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
whitelistOk, _, insecureOk := vchConfig.RegistryCheck(regctx, hostnameURL)
if !whitelistOk {
err = fmt.Errorf("Access denied to unauthorized registry (%s) while VCH is in whitelist mode", hostnameURL.Host)
log.Errorf(err.Error())
sf := streamformatter.NewJSONStreamFormatter()
outStream.Write(sf.FormatError(err))
return nil
}
ic.InsecureAllowHTTP = insecureOk
ic.RegistryCAs = RegistryCertPool
if authConfig != nil {
if len(authConfig.Username) > 0 {
ic.Username = authConfig.Username
}
if len(authConfig.Password) > 0 {
ic.Password = authConfig.Password
}
}
log.Infof("PullImage: reference: %s, %s, portlayer: %#v",
ic.Reference,
ic.Host,
portLayerServer)
err = ic.PullImage()
if err != nil {
return err
}
//TODO: Need repo name as second parameter. Leave blank for now
actor := CreateImageEventActorWithAttributes(image, "", map[string]string{})
EventService().Log("pull", eventtypes.ImageEventType, actor)
return nil
}
func (i *ImageBackend) PushImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
return errors.APINotSupportedMsg(ProductName(), "PushImage")
}
func (i *ImageBackend) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, limit int, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "SearchRegistryForImages")
}
// Utility functions
func convertV1ImageToDockerImage(image *metadata.ImageConfig) *types.ImageSummary {
var labels map[string]string
if image.Config != nil {
labels = image.Config.Labels
}
return &types.ImageSummary{
ID: image.ImageID,
ParentID: image.Parent,
RepoTags: image.Tags,
RepoDigests: image.Digests,
Created: image.Created.Unix(),
Size: image.Size,
VirtualSize: image.Size,
Labels: labels,
}
}
// Converts the data structure retrieved from the portlayer. This src datastructure
// represents the unmarshalled data saved in the storage port layer. The return
// data is what the Docker CLI understand and returns to user.
func imageConfigToDockerImageInspect(imageConfig *metadata.ImageConfig, productName string) *types.ImageInspect {
if imageConfig == nil {
return nil
}
rootfs := types.RootFS{
Type: "layers",
Layers: make([]string, 0, len(imageConfig.History)),
BaseLayer: "",
}
for k := range imageConfig.DiffIDs {
rootfs.Layers = append(rootfs.Layers, k)
}
inspectData := &types.ImageInspect{
RepoTags: imageConfig.Tags,
RepoDigests: imageConfig.Digests,
Parent: imageConfig.Parent,
Comment: imageConfig.Comment,
Created: imageConfig.Created.Format(time.RFC3339Nano),
Container: imageConfig.Container,
ContainerConfig: &imageConfig.ContainerConfig,
DockerVersion: imageConfig.DockerVersion,
Author: imageConfig.Author,
Config: imageConfig.Config,
Architecture: imageConfig.Architecture,
Os: imageConfig.OS,
Size: imageConfig.Size,
VirtualSize: imageConfig.Size,
RootFS: rootfs,
}
inspectData.GraphDriver.Name = productName + " " + PortlayerName
// ImageID is currently stored within VIC without the "sha256:" prefix
// so we add it here to match Docker output.
inspectData.ID = digest.Canonical.String() + ":" + imageConfig.ImageID
return inspectData
}
func CreateImageEventActorWithAttributes(imageID, refName string, attributes map[string]string) eventtypes.Actor {
if imageConfig, err := cache.ImageCache().Get(imageID); err == nil && imageConfig != nil {
for k, v := range imageConfig.Config.Labels {
attributes[k] = v
}
}
if refName != "" {
attributes["name"] = refName
}
return eventtypes.Actor{
ID: imageID,
Attributes: attributes,
}
}

View File

@@ -0,0 +1,62 @@
// 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 backends
import (
"fmt"
"testing"
"time"
"github.com/docker/docker/api/types/container"
v1 "github.com/docker/docker/image"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/metadata"
)
func TestConvertV1ImageToDockerImage(t *testing.T) {
now := time.Now()
image := &metadata.ImageConfig{
V1Image: v1.V1Image{
ID: "deadbeef",
Size: 1024,
Created: now,
Parent: "",
Config: &container.Config{
Labels: map[string]string{},
},
},
ImageID: "test_id",
Digests: []string{fmt.Sprintf("%s@sha:%s", "test_name", "12345")},
Tags: []string{fmt.Sprintf("%s:%s", "test_name", "test_tag")},
Name: "test_name",
DiffIDs: map[string]string{"test_diffid": "test_layerid"},
History: []v1.History{},
Reference: "test_name:test_tag",
}
dockerImage := convertV1ImageToDockerImage(image)
assert.Equal(t, image.ImageID, dockerImage.ID, "Error: expected id %s, got %s", image.ImageID, dockerImage.ID)
assert.Equal(t, image.Size, dockerImage.VirtualSize, "Error: expected size %s, got %s", image.Size, dockerImage.VirtualSize)
assert.Equal(t, image.Size, dockerImage.Size, "Error: expected size %s, got %s", image.Size, dockerImage.Size)
assert.Equal(t, image.Created.Unix(), dockerImage.Created, "Error: expected created %s, got %s", image.Created, dockerImage.Created)
assert.Equal(t, image.Parent, dockerImage.ParentID, "Error: expected parent %s, got %s", image.Parent, dockerImage.ParentID)
assert.Equal(t, image.Config.Labels, dockerImage.Labels, "Error: expected labels %s, got %s", image.Config.Labels, dockerImage.Labels)
assert.Equal(t, image.Digests[0], dockerImage.RepoDigests[0], "Error: expected digest %s, got %s", image.Digests[0], dockerImage.RepoDigests[0])
assert.Equal(t, image.Tags[0], dockerImage.RepoTags[0], "Error: expected tag %s, got %s", image.Tags[0], dockerImage.RepoTags[0])
}

View File

@@ -0,0 +1,104 @@
// 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 kv
import (
"errors"
"fmt"
"github.com/vmware/vic/lib/apiservers/portlayer/client"
ckv "github.com/vmware/vic/lib/apiservers/portlayer/client/kv"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
"github.com/vmware/vic/pkg/trace"
"context"
log "github.com/Sirupsen/logrus"
)
const (
// defaultNamespace is the first part of the
// k/v store key (i.e. docker.stuff)
defaultNamespace = "docker"
defaultSeparator = "."
)
var (
ErrKeyNotFound = errors.New("key not found")
)
// Get will call to the portlayer for the value of the specified key
// The key argument is prefixed w/the defaultName space for the docker
// persona. i.e. docker.{key}
//
// If the key doesn't exist an ErrKeyNotFound will be returned
func Get(client *client.PortLayer, key string) (string, error) {
defer trace.End(trace.Begin(key))
var val string
resp, err := client.Kv.GetValue(ckv.NewGetValueParamsWithContext(
context.Background()).WithKey(createNameSpacedKey(key)))
if err != nil {
switch err.(type) {
case *ckv.GetValueNotFound:
return val, ErrKeyNotFound
default:
log.Errorf("Error Getting Key/Value: %s", err.Error())
return val, err
}
}
val = resp.Payload.Value
// return the value
return val, nil
}
// Put will put the key / value in the portlayer k/v store
func Put(client *client.PortLayer, key string, val string) error {
defer trace.End(trace.Begin(key))
fullKey := createNameSpacedKey(key)
keyval := &models.KeyValue{
Key: fullKey,
Value: val,
}
_, err := client.Kv.PutValue(ckv.NewPutValueParamsWithContext(
context.Background()).WithKey(fullKey).WithKeyValue(keyval))
if err != nil {
log.Errorf("Error Putting Key/Value: %s", err)
return err
}
return nil
}
// Delete will remove the key / value from the store
func Delete(client *client.PortLayer, key string) error {
defer trace.End(trace.Begin(key))
_, err := client.Kv.DeleteValue(ckv.NewDeleteValueParamsWithContext(
context.Background()).WithKey(createNameSpacedKey(key)))
if err != nil {
log.Errorf("Error Deleting Key/Value: %s", err)
return err
}
return nil
}
func createNameSpacedKey(key string) string {
return fmt.Sprintf("%s%s%s", defaultNamespace, defaultSeparator, key)
}

View File

@@ -0,0 +1,551 @@
// 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 backends
import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"sync"
"time"
"net/http"
log "github.com/Sirupsen/logrus"
derr "github.com/docker/docker/api/errors"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
apinet "github.com/docker/docker/api/types/network"
"github.com/docker/libnetwork"
"github.com/docker/libnetwork/networkdb"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
"github.com/vmware/vic/lib/apiservers/engine/backends/convert"
vicendpoint "github.com/vmware/vic/lib/apiservers/engine/backends/endpoint"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"github.com/vmware/vic/lib/apiservers/portlayer/client/containers"
"github.com/vmware/vic/lib/apiservers/portlayer/client/scopes"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
"github.com/vmware/vic/pkg/retry"
)
type NetworkBackend struct {
}
func NewNetworkBackend() *NetworkBackend {
return &NetworkBackend{}
}
func (n *NetworkBackend) NetworkControllerEnabled() bool {
return false
}
func (n *NetworkBackend) FindNetwork(idName string) (libnetwork.Network, error) {
ok, err := PortLayerClient().Scopes.List(scopes.NewListParamsWithContext(ctx).WithIDName(idName))
if err != nil {
switch err := err.(type) {
case *scopes.ListNotFound:
return nil, derr.NewRequestNotFoundError(fmt.Errorf("network %s not found", idName))
case *scopes.ListDefault:
return nil, derr.NewErrorWithStatusCode(fmt.Errorf(err.Payload.Message), http.StatusInternalServerError)
default:
return nil, derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
}
return &vicnetwork{cfg: ok.Payload[0]}, nil
}
func (n *NetworkBackend) GetNetworkByName(idName string) (libnetwork.Network, error) {
ok, err := PortLayerClient().Scopes.List(scopes.NewListParamsWithContext(ctx).WithIDName(idName))
if err != nil {
switch err := err.(type) {
case *scopes.ListNotFound:
return nil, nil
case *scopes.ListDefault:
return nil, derr.NewErrorWithStatusCode(fmt.Errorf(err.Payload.Message), http.StatusInternalServerError)
default:
return nil, derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
}
return &vicnetwork{cfg: ok.Payload[0]}, nil
}
func (n *NetworkBackend) GetNetworksByID(partialID string) []libnetwork.Network {
ok, err := PortLayerClient().Scopes.List(scopes.NewListParamsWithContext(ctx).WithIDName(partialID))
if err != nil {
return nil
}
nets := make([]libnetwork.Network, len(ok.Payload))
for i, cfg := range ok.Payload {
nets[i] = &vicnetwork{cfg: cfg}
}
return nets
}
func (n *NetworkBackend) GetNetworks() []libnetwork.Network {
ok, err := PortLayerClient().Scopes.ListAll(scopes.NewListAllParamsWithContext(ctx))
if err != nil {
return nil
}
nets := make([]libnetwork.Network, len(ok.Payload))
for i, cfg := range ok.Payload {
nets[i] = &vicnetwork{cfg: cfg}
i++
}
return nets
}
func (n *NetworkBackend) CreateNetwork(nc types.NetworkCreateRequest) (*types.NetworkCreateResponse, error) {
if nc.IPAM != nil && len(nc.IPAM.Config) > 1 {
return nil, fmt.Errorf("at most one ipam config supported")
}
var gateway, subnet string
var pools []string
if nc.IPAM != nil && len(nc.IPAM.Config) > 0 {
if nc.IPAM.Config[0].Gateway != "" {
gateway = nc.IPAM.Config[0].Gateway
}
if nc.IPAM.Config[0].Subnet != "" {
subnet = nc.IPAM.Config[0].Subnet
}
if nc.IPAM.Config[0].IPRange != "" {
pools = append(pools, nc.IPAM.Config[0].IPRange)
}
}
if nc.Driver == "" {
nc.Driver = "bridge"
}
cfg := &models.ScopeConfig{
Gateway: gateway,
Name: nc.Name,
ScopeType: nc.Driver,
Subnet: subnet,
IPAM: pools,
Annotations: make(map[string]string),
Internal: nc.Internal,
}
// Marshal and encode the labels for transport and storage in the portlayer
if labelsBytes, err := json.Marshal(nc.Labels); err == nil {
encodedLabels := base64.StdEncoding.EncodeToString(labelsBytes)
cfg.Annotations[convert.AnnotationKeyLabels] = encodedLabels
} else {
log.Errorf("error marshaling labels: %s", err)
return nil, derr.NewErrorWithStatusCode(fmt.Errorf("unable to marshal labels: %s", err), http.StatusInternalServerError)
}
created, err := PortLayerClient().Scopes.CreateScope(scopes.NewCreateScopeParamsWithContext(ctx).WithConfig(cfg))
if err != nil {
switch err := err.(type) {
case *scopes.CreateScopeConflict:
return nil, derr.NewErrorWithStatusCode(fmt.Errorf("vicnetwork %s already exists", nc.Name), http.StatusConflict)
case *scopes.CreateScopeDefault:
return nil, derr.NewErrorWithStatusCode(fmt.Errorf(err.Payload.Message), http.StatusInternalServerError)
default:
return nil, derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
}
ncResponse := &types.NetworkCreateResponse{
ID: created.Payload.ID,
Warning: "",
}
return ncResponse, nil
}
// isCommitConflictError returns true if err is a conflict error from the portlayer's
// handle commit operation, and false otherwise.
func isCommitConflictError(err error) bool {
_, isConflictErr := err.(*containers.CommitConflict)
return isConflictErr
}
// connectContainerToNetwork performs portlayer operations to connect a container to a container vicnetwork.
func connectContainerToNetwork(containerName, networkName string, endpointConfig *apinet.EndpointSettings) error {
client := PortLayerClient()
getRes, err := client.Containers.Get(containers.NewGetParamsWithContext(ctx).WithID(containerName))
if err != nil {
switch err := err.(type) {
case *containers.GetNotFound:
return derr.NewRequestNotFoundError(fmt.Errorf(err.Payload.Message))
case *containers.GetDefault:
return derr.NewErrorWithStatusCode(fmt.Errorf(err.Payload.Message), http.StatusInternalServerError)
default:
return derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
}
h := getRes.Payload
nc := &models.NetworkConfig{NetworkName: networkName}
if endpointConfig != nil {
if endpointConfig.IPAMConfig != nil && endpointConfig.IPAMConfig.IPv4Address != "" {
nc.Address = endpointConfig.IPAMConfig.IPv4Address
}
// Pass Links and Aliases to PL.
nc.Aliases = vicendpoint.Alias(endpointConfig)
}
addConRes, err := client.Scopes.AddContainer(scopes.NewAddContainerParamsWithContext(ctx).
WithScope(nc.NetworkName).
WithConfig(&models.ScopesAddContainerConfig{
Handle: h,
NetworkConfig: nc,
}))
if err != nil {
switch err := err.(type) {
case *scopes.AddContainerNotFound:
return derr.NewRequestNotFoundError(fmt.Errorf(err.Payload.Message))
case *scopes.AddContainerInternalServerError:
return derr.NewErrorWithStatusCode(fmt.Errorf(err.Payload.Message), http.StatusInternalServerError)
default:
return derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
}
h = addConRes.Payload
// Get the power state of the container.
getStateRes, err := client.Containers.GetState(containers.NewGetStateParamsWithContext(ctx).WithHandle(h))
if err != nil {
switch err := err.(type) {
case *containers.GetStateNotFound:
return derr.NewRequestNotFoundError(fmt.Errorf(err.Payload.Message))
case *containers.GetStateDefault:
return derr.NewErrorWithStatusCode(fmt.Errorf(err.Payload.Message), http.StatusInternalServerError)
default:
return derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
}
h = getStateRes.Payload.Handle
// Only bind if the container is running.
if getStateRes.Payload.State == "RUNNING" {
bindRes, err := client.Scopes.BindContainer(scopes.NewBindContainerParamsWithContext(ctx).WithHandle(h))
if err != nil {
switch err := err.(type) {
case *scopes.BindContainerNotFound:
return derr.NewRequestNotFoundError(fmt.Errorf(err.Payload.Message))
case *scopes.BindContainerInternalServerError:
return derr.NewErrorWithStatusCode(fmt.Errorf(err.Payload.Message), http.StatusInternalServerError)
default:
return derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
}
defer func() {
if err == nil {
return
}
if _, err2 := client.Scopes.UnbindContainer(scopes.NewUnbindContainerParamsWithContext(ctx).WithHandle(h)); err2 != nil {
log.Warnf("failed bind container rollback: %s", err2)
}
}()
h = bindRes.Payload.Handle
}
// Commit the handle.
_, err = client.Containers.Commit(containers.NewCommitParamsWithContext(ctx).WithHandle(h))
return err
}
// ConnectContainerToNetwork connects a container to a container vicnetwork. It wraps the portlayer operations
// in a retry for when there's a conflict error received, such as one during a similar concurrent operation.
func (n *NetworkBackend) ConnectContainerToNetwork(containerName, networkName string, endpointConfig *apinet.EndpointSettings) error {
vc := cache.ContainerCache().GetContainer(containerName)
if vc != nil {
containerName = vc.ContainerID
}
operation := func() error {
return connectContainerToNetwork(containerName, networkName, endpointConfig)
}
config := retry.NewBackoffConfig()
config.MaxElapsedTime = maxElapsedTime
err := retry.DoWithConfig(operation, isCommitConflictError, config)
if err != nil {
switch err := err.(type) {
case *containers.CommitNotFound:
return derr.NewRequestNotFoundError(fmt.Errorf(err.Payload.Message))
case *containers.CommitDefault:
return derr.NewErrorWithStatusCode(fmt.Errorf(err.Payload.Message), http.StatusInternalServerError)
default:
return derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
}
return nil
}
func (n *NetworkBackend) DisconnectContainerFromNetwork(containerName string, networkName string, force bool) error {
vc := cache.ContainerCache().GetContainer(containerName)
if vc != nil {
containerName = vc.ContainerID
}
return errors.APINotSupportedMsg(ProductName(), "DisconnectContainerFromNetwork")
}
func (n *NetworkBackend) DeleteNetwork(name string) error {
client := PortLayerClient()
if _, err := client.Scopes.DeleteScope(scopes.NewDeleteScopeParamsWithContext(ctx).WithIDName(name)); err != nil {
switch err := err.(type) {
case *scopes.DeleteScopeNotFound:
return derr.NewRequestNotFoundError(fmt.Errorf("network %s not found", name))
case *scopes.DeleteScopeInternalServerError:
return derr.NewErrorWithStatusCode(fmt.Errorf(err.Payload.Message), http.StatusInternalServerError)
default:
return derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
}
return nil
}
func (n *NetworkBackend) NetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "NetworksPrune")
}
// vicnetwork implements the libnetwork.Network and libnetwork.NetworkInfo interfaces
type vicnetwork struct {
sync.Mutex
cfg *models.ScopeConfig
}
// A user chosen name for this vicnetwork.
func (n *vicnetwork) Name() string {
return n.cfg.Name
}
// A system generated id for this vicnetwork.
func (n *vicnetwork) ID() string {
return n.cfg.ID
}
// The type of vicnetwork, which corresponds to its managing driver.
func (n *vicnetwork) Type() string {
return n.cfg.ScopeType
}
// Create a new endpoint to this vicnetwork symbolically identified by the
// specified unique name. The options parameter carry driver specific options.
func (n *vicnetwork) CreateEndpoint(name string, options ...libnetwork.EndpointOption) (libnetwork.Endpoint, error) {
return nil, fmt.Errorf("not implemented")
}
// Delete the vicnetwork.
func (n *vicnetwork) Delete() error {
return fmt.Errorf("not implemented")
}
// Endpoints returns the list of Endpoint(s) in this vicnetwork.
func (n *vicnetwork) Endpoints() []libnetwork.Endpoint {
eps := make([]libnetwork.Endpoint, len(n.cfg.Endpoints))
for i, e := range n.cfg.Endpoints {
eps[i] = &endpoint{ep: e, sc: n.cfg}
}
return eps
}
// WalkEndpoints uses the provided function to walk the Endpoints
func (n *vicnetwork) WalkEndpoints(walker libnetwork.EndpointWalker) {
for _, e := range n.cfg.Endpoints {
if walker(&endpoint{ep: e, sc: n.cfg}) {
return
}
}
}
// EndpointByName returns the Endpoint which has the passed name. If not found, the error ErrNoSuchEndpoint is returned.
func (n *vicnetwork) EndpointByName(name string) (libnetwork.Endpoint, error) {
for _, e := range n.cfg.Endpoints {
if e.Name == name {
return &endpoint{ep: e, sc: n.cfg}, nil
}
}
return nil, fmt.Errorf("not found")
}
// EndpointByID returns the Endpoint which has the passed id. If not found, the error ErrNoSuchEndpoint is returned.
func (n *vicnetwork) EndpointByID(id string) (libnetwork.Endpoint, error) {
for _, e := range n.cfg.Endpoints {
if e.ID == id {
return &endpoint{ep: e, sc: n.cfg}, nil
}
}
return nil, fmt.Errorf("not found")
}
// Return certain operational data belonging to this vicnetwork
func (n *vicnetwork) Info() libnetwork.NetworkInfo {
return n
}
func (n *vicnetwork) IpamConfig() (string, map[string]string, []*libnetwork.IpamConf, []*libnetwork.IpamConf) {
n.Lock()
defer n.Unlock()
confs := make([]*libnetwork.IpamConf, len(n.cfg.IPAM))
for j, i := range n.cfg.IPAM {
conf := &libnetwork.IpamConf{
PreferredPool: n.cfg.Subnet,
Gateway: "",
}
if i != n.cfg.Subnet {
conf.SubPool = i
}
if n.cfg.Gateway != "" {
conf.Gateway = n.cfg.Gateway
}
confs[j] = conf
}
return "", make(map[string]string), confs, nil
}
func (n *vicnetwork) IpamInfo() ([]*libnetwork.IpamInfo, []*libnetwork.IpamInfo) {
n.Lock()
defer n.Unlock()
var infos []*libnetwork.IpamInfo
for _, i := range n.cfg.IPAM {
_, pool, err := net.ParseCIDR(i)
if err != nil {
continue
}
info := &libnetwork.IpamInfo{
Meta: make(map[string]string),
}
info.Pool = pool
if n.cfg.Gateway != "" {
info.Gateway = &net.IPNet{
IP: net.ParseIP(n.cfg.Gateway),
Mask: net.CIDRMask(32, 32),
}
}
info.AuxAddresses = make(map[string]*net.IPNet)
infos = append(infos, info)
}
return infos, nil
}
func (n *vicnetwork) DriverOptions() map[string]string {
return make(map[string]string)
}
func (n *vicnetwork) Scope() string {
return ""
}
func (n *vicnetwork) IPv6Enabled() bool {
return false
}
func (n *vicnetwork) Internal() bool {
n.Lock()
defer n.Unlock()
return n.cfg.Internal
}
// Labels decodes and unmarshals the stored blob of vicnetwork labels.
func (n *vicnetwork) Labels() map[string]string {
n.Lock()
defer n.Unlock()
labels := make(map[string]string)
if n.cfg.Annotations == nil {
return labels
}
// Look for the Docker-specific annotation (label) blob and process it for the output
if encodedLabels, ok := n.cfg.Annotations[convert.AnnotationKeyLabels]; ok {
if labelsBytes, decodeErr := base64.StdEncoding.DecodeString(encodedLabels); decodeErr == nil {
if unmarshalErr := json.Unmarshal(labelsBytes, &labels); unmarshalErr != nil {
log.Errorf("error unmarshaling labels: %s", unmarshalErr)
}
} else {
log.Errorf("error decoding label blob: %s", decodeErr)
}
}
return labels
}
func (n *vicnetwork) Attachable() bool {
return false //?
}
func (n *vicnetwork) Dynamic() bool {
return false //?
}
func (n *vicnetwork) Created() time.Time {
return time.Now()
}
// Peers returns a slice of PeerInfo structures which has the information about the peer
// nodes participating in the same overlay vicnetwork. This is currently the per-vicnetwork
// gossip cluster. For non-dynamic overlay networks and bridge networks it returns an
// empty slice
func (n *vicnetwork) Peers() []networkdb.PeerInfo {
return nil
}

View File

@@ -0,0 +1,73 @@
// 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 backends
import (
"io"
"net/http"
enginetypes "github.com/docker/docker/api/types"
"github.com/docker/docker/reference"
"golang.org/x/net/context"
"github.com/vmware/vic/lib/apiservers/engine/errors"
)
type PluginBackend struct {
}
func NewPluginBackend() *PluginBackend {
return &PluginBackend{}
}
func (p *PluginBackend) Disable(name string, config *enginetypes.PluginDisableConfig) error {
return errors.APINotSupportedMsg(ProductName(), "plugins")
}
func (p *PluginBackend) Enable(name string, config *enginetypes.PluginEnableConfig) error {
return errors.APINotSupportedMsg(ProductName(), "plugins")
}
func (p *PluginBackend) List() ([]enginetypes.Plugin, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "plugins")
}
func (p *PluginBackend) Inspect(name string) (*enginetypes.Plugin, error) {
return nil, errors.PluginNotFoundError(name)
}
func (p *PluginBackend) Remove(name string, config *enginetypes.PluginRmConfig) error {
return errors.APINotSupportedMsg(ProductName(), "plugins")
}
func (p *PluginBackend) Set(name string, args []string) error {
return errors.APINotSupportedMsg(ProductName(), "plugins")
}
func (p *PluginBackend) Privileges(ctx context.Context, ref reference.Named, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "plugins")
}
func (p *PluginBackend) Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error {
return errors.APINotSupportedMsg(ProductName(), "plugins")
}
func (p *PluginBackend) Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error {
return errors.APINotSupportedMsg(ProductName(), "plugins")
}
func (p *PluginBackend) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error {
return errors.APINotSupportedMsg(ProductName(), "plugins")
}

View File

@@ -0,0 +1,246 @@
// 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 portmap
import (
"fmt"
"net"
"strconv"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/docker/libnetwork/iptables"
)
type Operation int
const (
Map Operation = iota
Unmap
)
func (o Operation) String() string {
switch o {
case Map:
return "Map"
case Unmap:
return "Unmap"
}
return "Unknown"
}
type PortMapper interface {
MapPort(ip net.IP, port int, proto string, destIP string, destPort int, srcIface, destIface string) error
UnmapPort(ip net.IP, port int, proto string, destPort int, srcIface, destIface string) error
}
type bindKey struct {
ip string
port int
}
type portMapper struct {
sync.Mutex
bindings map[bindKey][][]string
}
func NewPortMapper() PortMapper {
return &portMapper{bindings: make(map[bindKey][][]string)}
}
func (p *portMapper) isPortAvailable(proto string, ip net.IP, port int) bool {
addr := ""
if ip != nil && !ip.IsUnspecified() {
addr = ip.String()
}
if _, ok := p.bindings[bindKey{addr, port}]; ok {
return false
}
c, err := net.Dial(proto, net.JoinHostPort(addr, strconv.Itoa(port)))
defer func() {
if c != nil {
// #nosec: Errors unhandled.
c.Close()
}
}()
if err != nil {
return true
}
return false
}
func (p *portMapper) MapPort(ip net.IP, port int, proto string, destIP string, destPort int, srcIface, destIface string) error {
p.Lock()
defer p.Unlock()
// check if port is available
if !p.isPortAvailable(proto, ip, port) {
return fmt.Errorf("port %d is not available", port)
}
if port <= 0 {
return fmt.Errorf("source port must be specified")
}
if destPort <= 0 {
log.Infof("destination port not specified, using source port %d", port)
destPort = port
}
return p.forward(Map, ip, port, proto, destIP, destPort, srcIface, destIface)
}
func (p *portMapper) UnmapPort(ip net.IP, port int, proto string, destPort int, srcIface, destIface string) error {
p.Lock()
defer p.Unlock()
if port <= 0 {
return fmt.Errorf("source port must be specified")
}
if destPort <= 0 {
log.Infof("destination port not specified, using source port %d", port)
destPort = port
}
return p.forward(Unmap, ip, port, proto, "", destPort, srcIface, destIface)
}
// iptablesRunAndCheck runs an iptables command with the provided args
func iptablesRunAndCheck(action iptables.Action, args []string) error {
args = append([]string{string(action)}, args...)
if output, err := iptables.Raw(args...); err != nil {
return err
} else if len(output) != 0 {
return iptables.ChainError{Chain: "FORWARD", Output: output}
}
return nil
}
// iptablesDelete takes the saved args from the Append operation
// and uses them to delete the previously added rules
func iptablesDelete(args [][]string) error {
var errs []error
for _, cmd := range args {
if err := iptablesRunAndCheck(iptables.Delete, cmd); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return fmt.Errorf("Failed to delete iptables rules: %s", errs)
}
return nil
}
// adapted from https://github.com/docker/libnetwork/blob/master/iptables/iptables.go
//
// assumes p is locked
func (p *portMapper) forward(op Operation, ip net.IP, port int, proto, destAddr string, destPort int, srcIface, destIface string) error {
daddr := ip.String()
if ip == nil || ip.IsUnspecified() {
// iptables interprets "0.0.0.0" as "0.0.0.0/32", whereas we
// want "0.0.0.0/0". "0/0" is correctly interpreted as "any
// value" by both iptables and ip6tables.
daddr = "0/0"
}
ipStr := ""
if ip != nil && !ip.IsUnspecified() {
ipStr = ip.String()
}
key := bindKey{ip: ipStr, port: port}
switch op {
case Unmap:
// lookup commands to reverse
if args, ok := p.bindings[key]; ok {
if err := iptablesDelete(args); err != nil {
return err
}
delete(p.bindings, bindKey{ipStr, port})
return nil
}
return fmt.Errorf("Failed to find unmap data for %s:%d", ipStr, port)
case Map:
var savedArgs [][]string
args := []string{"VIC", "-t", string(iptables.Nat),
"-i", srcIface,
"-p", proto,
"-d", daddr,
"--dport", strconv.Itoa(port),
"-j", "DNAT",
"--to-destination", net.JoinHostPort(destAddr, strconv.Itoa(destPort))}
if err := iptablesRunAndCheck(iptables.Append, args); err != nil {
return err
}
savedArgs = append(savedArgs, args)
p.bindings[key] = savedArgs
// allow traffic from container to container via vch public interface
args = []string{"VIC", "-t", string(iptables.Nat),
"-i", destIface,
"-p", proto,
"--dport", strconv.Itoa(port),
"-j", "DNAT",
"--to-destination", net.JoinHostPort(destAddr, strconv.Itoa(destPort)),
"-m", "addrtype",
"--dst-type", "LOCAL"}
if err := iptablesRunAndCheck(iptables.Append, args); err != nil {
return err
}
savedArgs = append(savedArgs, args)
p.bindings[key] = savedArgs
// rule to allow connections from the public interface for
// the mapped port
args = []string{"VIC", "-t", string(iptables.Filter),
"-i", srcIface,
"-o", destIface,
"-p", proto,
"-d", destAddr,
"--dport", strconv.Itoa(destPort),
"-j", "ACCEPT"}
if err := iptablesRunAndCheck(iptables.Append, args); err != nil {
return err
}
savedArgs = append(savedArgs, args)
p.bindings[key] = savedArgs
args = []string{"POSTROUTING", "-t", string(iptables.Nat),
"-p", proto,
"-d", destAddr,
"--dport", strconv.Itoa(destPort),
"-j", "MASQUERADE"}
if err := iptablesRunAndCheck(iptables.Append, args); err != nil {
return err
}
savedArgs = append(savedArgs, args)
p.bindings[key] = savedArgs
return nil
default:
log.Warnf("noop for given operation: %s", op)
}
return nil
}

View File

@@ -0,0 +1,122 @@
// 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 backends
import (
"net"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/libnetwork"
"github.com/docker/libnetwork/types"
)
type sandbox struct {
id string
containerID string
}
func newSandbox(containerID string) *sandbox {
return &sandbox{
id: stringid.GenerateRandomID(),
containerID: containerID,
}
}
// ID returns the ID of the sandbox
func (s *sandbox) ID() string {
return s.id
}
// Key returns the sandbox's key
func (s *sandbox) Key() string {
return ""
}
// ContainerID returns the container id associated to this sandbox
func (s *sandbox) ContainerID() string {
return s.containerID
}
// Labels returns the sandbox's labels
func (s *sandbox) Labels() map[string]interface{} {
return nil
}
// Statistics retrieves the interfaces' statistics for the sandbox
func (s *sandbox) Statistics() (map[string]*types.InterfaceStatistics, error) {
return nil, notImplementedError
}
// Refresh leaves all the endpoints, resets and re-apply the options,
// re-joins all the endpoints without destroying the osl sandbox
func (s *sandbox) Refresh(options ...libnetwork.SandboxOption) error {
return notImplementedError
}
// SetKey updates the Sandbox Key
func (s *sandbox) SetKey(key string) error {
return notImplementedError
}
// Rename changes the name of all attached Endpoints
func (s *sandbox) Rename(name string) error {
return notImplementedError
}
// Delete destroys this container after detaching it from all connected endpoints.
func (s *sandbox) Delete() error {
return notImplementedError
}
// ResolveName resolves a service name to an IPv4 or IPv6 address by searching
// the networks the sandbox is connected to. For IPv6 queries, second return
// value will be true if the name exists in docker domain but doesn't have an
// IPv6 address. Such queries shouldn't be forwarded to external nameservers.
func (s *sandbox) ResolveName(name string, iplen int) ([]net.IP, bool) {
return nil, false
}
// ResolveIP returns the service name for the passed in IP. IP is in reverse dotted
// notation; the format used for DNS PTR records
func (s *sandbox) ResolveIP(name string) string {
return ""
}
// Endpoints returns all the endpoints connected to the sandbox
func (s *sandbox) Endpoints() []libnetwork.Endpoint {
return nil
}
// ResolveService returns all the backend details about the containers or hosts
// backing a service. Its purpose is to satisfy an SRV query
func (s *sandbox) ResolveService(name string) ([]*net.SRV, []net.IP) {
return nil, nil
}
// EnableService makes a managed container's service available by adding the
// endpoint to the service load balancer and service discovery
func (s *sandbox) EnableService() error {
return notImplementedError
}
// DisableService removes a managed contianer's endpoints from the load balancer
// and service discovery
func (s *sandbox) DisableService() error {
return notImplementedError
}

View File

@@ -0,0 +1,128 @@
// 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 backends
import (
"golang.org/x/net/context"
basictypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
types "github.com/docker/docker/api/types/swarm"
"github.com/vmware/vic/lib/apiservers/engine/errors"
)
type SwarmBackend struct {
}
func NewSwarmBackend() *SwarmBackend {
return &SwarmBackend{}
}
func (s *SwarmBackend) Init(req types.InitRequest) (string, error) {
return "", errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) Join(req types.JoinRequest) error {
return errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) Leave(force bool) error {
return errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) Inspect() (types.Swarm, error) {
return types.Swarm{}, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) Update(uint64, types.Spec, types.UpdateFlags) error {
return errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) GetUnlockKey() (string, error) {
return "", errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) UnlockSwarm(req types.UnlockRequest) error {
return errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) GetServices(basictypes.ServiceListOptions) ([]types.Service, error) {
return nil, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) GetService(string) (types.Service, error) {
return types.Service{}, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error) {
return nil, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) UpdateService(string, uint64, types.ServiceSpec, string, string) (*basictypes.ServiceUpdateResponse, error) {
return nil, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) RemoveService(string) error {
return errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) ServiceLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error {
return errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) GetNodes(basictypes.NodeListOptions) ([]types.Node, error) {
return nil, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) GetNode(string) (types.Node, error) {
return types.Node{}, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) UpdateNode(string, uint64, types.NodeSpec) error {
return errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) RemoveNode(string, bool) error {
return errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) GetTasks(basictypes.TaskListOptions) ([]types.Task, error) {
return nil, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) GetTask(string) (types.Task, error) {
return types.Task{}, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error) {
return nil, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) CreateSecret(sp types.SecretSpec) (string, error) {
return "", errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) RemoveSecret(id string) error {
return errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) GetSecret(id string) (types.Secret, error) {
return types.Secret{}, errors.SwarmNotSupportedError()
}
func (s *SwarmBackend) UpdateSecret(id string, version uint64, spec types.SecretSpec) error {
return errors.SwarmNotSupportedError()
}

View File

@@ -0,0 +1,454 @@
// 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 backends
//****
// system.go
//
// Rules for code to be in here:
// 1. No remote or swagger calls. Move those code to ../proxy/system_proxy.go
// 2. Always return docker engine-api compatible errors.
// - Do NOT return fmt.Errorf()
// - Do NOT return errors.New()
// - DO USE the aliased docker error package 'derr'
// - It is OK to return errors returned from functions in system_proxy.go
import (
"crypto/x509"
"fmt"
"net/url"
"runtime"
"strings"
"sync"
"time"
"golang.org/x/net/context"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vic/lib/apiservers/engine/backends/cache"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"github.com/vmware/vic/lib/apiservers/engine/proxy"
"github.com/vmware/vic/lib/apiservers/portlayer/client"
"github.com/vmware/vic/lib/apiservers/portlayer/client/storage"
"github.com/vmware/vic/lib/imagec"
urlfetcher "github.com/vmware/vic/pkg/fetcher"
"github.com/vmware/vic/pkg/registry"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/version"
"github.com/docker/docker/api/types"
eventtypes "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/daemon/events"
"github.com/docker/docker/pkg/platform"
"github.com/docker/go-units"
)
type SystemBackend struct {
systemProxy proxy.VicSystemProxy
}
const (
systemStatusMhz = " VCH CPU limit"
systemStatusMemory = " VCH memory limit"
systemStatusCPUUsageMhz = " VCH CPU usage"
systemStatusMemUsage = " VCH memory usage"
systemOS = " VMware OS"
systemOSVersion = " VMware OS version"
systemProductName = " VMware Product"
volumeStoresID = "VolumeStores"
loginTimeout = 20 * time.Second
infoTimeout = 5 * time.Second
vchWhitelistMode = " Registry Whitelist Mode"
whitelistRegistriesLabel = " Whitelisted Registries"
insecureRegistriesLabel = " Insecure Registries"
)
// var for use by other engine components
var systemBackend *SystemBackend
var sysOnce sync.Once
func NewSystemBackend() *SystemBackend {
sysOnce.Do(func() {
systemBackend = &SystemBackend{
systemProxy: proxy.NewSystemProxy(PortLayerClient()),
}
})
return systemBackend
}
func (s *SystemBackend) SystemInfo() (*types.Info, error) {
defer trace.End(trace.Begin("SystemInfo"))
client := PortLayerClient()
// Retrieve container status from port layer
running, paused, stopped, err := s.systemProxy.ContainerCount(context.Background())
if err != nil {
log.Infof("System.SytemInfo unable to get global status on containers: %s", err.Error())
}
ctx, cancel := context.WithTimeout(context.Background(), infoTimeout)
defer cancel()
vchConfig := vchConfig.update(ctx)
vchConfig.Lock()
defer vchConfig.Unlock()
cfg := vchConfig.Cfg
// Build up the struct that the Remote API and CLI wants
info := &types.Info{
Driver: PortLayerName(),
IndexServerAddress: imagec.DefaultDockerURL,
ServerVersion: ProductVersion(),
ID: ProductName(),
Containers: running + paused + stopped,
ContainersRunning: running,
ContainersPaused: paused,
ContainersStopped: stopped,
Images: getImageCount(),
Debug: cfg.Diagnostics.DebugLevel > 0,
NGoroutines: runtime.NumGoroutine(),
SystemTime: time.Now().Format(time.RFC3339Nano),
LoggingDriver: "",
CgroupDriver: "",
DockerRootDir: "",
ClusterStore: "",
ClusterAdvertise: "",
// FIXME: Get this info once we have event listening service
// NEventsListener int
// These are system related. Some refer to cgroup info. Others are
// retrieved from the port layer and are information about the resource
// pool.
Name: cfg.Name,
KernelVersion: "",
Architecture: platform.Architecture, //stubbed
// NOTE: These values have no meaning for VIC. We default them to true to
// prevent the CLI from displaying warning messages.
CPUCfsPeriod: true,
CPUCfsQuota: true,
CPUShares: true,
CPUSet: true,
OomKillDisable: true,
MemoryLimit: true,
SwapLimit: true,
KernelMemory: true,
IPv4Forwarding: true,
BridgeNfIptables: true,
BridgeNfIP6tables: true,
HTTPProxy: "",
HTTPSProxy: "",
NoProxy: "",
}
// Add in vicnetwork info from the VCH via guestinfo
for _, network := range cfg.ContainerNetworks {
info.Plugins.Network = append(info.Plugins.Network, network.Name)
}
info.SystemStatus = make([][2]string, 0)
// Add in volume label from the VCH via guestinfo
volumeStoreString, err := FetchVolumeStores(client)
if err != nil {
log.Infof("Unable to get the volume store list from the portlayer : %s", err.Error())
} else {
customInfo := [2]string{volumeStoresID, volumeStoreString}
info.SystemStatus = append(info.SystemStatus, customInfo)
// Show a list of supported volume drivers if there's at least one volume
// store configured for the VCH. "local" is excluded because it's the default
// driver supplied by the Docker client and is equivalent to "vsphere" in
// our implementation.
if len(volumeStoreString) > 0 {
for driver := range proxy.SupportedVolDrivers {
if driver != "local" {
info.Plugins.Volume = append(info.Plugins.Volume, driver)
}
}
}
}
if s.systemProxy.PingPortlayer(context.Background()) {
status := [2]string{PortLayerName(), "RUNNING"}
info.SystemStatus = append(info.SystemStatus, status)
} else {
status := [2]string{PortLayerName(), "STOPPED"}
info.SystemStatus = append(info.SystemStatus, status)
}
// Add in vch information
vchInfo, err := s.systemProxy.VCHInfo(context.Background())
if err != nil || vchInfo == nil {
log.Infof("System.SystemInfo unable to get vch info from port layer: %s", err.Error())
} else {
if vchInfo.CPUMhz > 0 {
info.NCPU = int(vchInfo.CPUMhz)
customInfo := [2]string{systemStatusMhz, fmt.Sprintf("%d MHz", info.NCPU)}
info.SystemStatus = append(info.SystemStatus, customInfo)
}
if vchInfo.Memory > 0 {
info.MemTotal = vchInfo.Memory * 1024 * 1024 // Get Mebibytes
customInfo := [2]string{systemStatusMemory, units.BytesSize(float64(info.MemTotal))}
info.SystemStatus = append(info.SystemStatus, customInfo)
}
if vchInfo.CPUUsage >= 0 {
customInfo := [2]string{systemStatusCPUUsageMhz, fmt.Sprintf("%d MHz", int(vchInfo.CPUUsage))}
info.SystemStatus = append(info.SystemStatus, customInfo)
}
if vchInfo.MemUsage >= 0 {
customInfo := [2]string{systemStatusMemUsage, units.BytesSize(float64(vchInfo.MemUsage))}
info.SystemStatus = append(info.SystemStatus, customInfo)
}
if vchInfo.HostProductName != "" {
customInfo := [2]string{systemProductName, vchInfo.HostProductName}
info.SystemStatus = append(info.SystemStatus, customInfo)
}
if vchInfo.HostOS != "" {
info.OperatingSystem = vchInfo.HostOS
info.OSType = vchInfo.HostOS //Value for OS and OS Type the same from vmomi
customInfo := [2]string{systemOS, vchInfo.HostOS}
info.SystemStatus = append(info.SystemStatus, customInfo)
}
if vchInfo.HostOSVersion != "" {
customInfo := [2]string{systemOSVersion, vchInfo.HostOSVersion}
info.SystemStatus = append(info.SystemStatus, customInfo)
}
if len(vchConfig.Insecure) > 0 {
customInfo := [2]string{insecureRegistriesLabel, strings.Join(vchConfig.Insecure.Strings(), ",")}
info.SystemStatus = append(info.SystemStatus, customInfo)
}
if len(vchConfig.Whitelist) > 0 {
s := "enabled"
if vchConfig.remoteWl {
s += "; remote source"
}
customInfo := [2]string{vchWhitelistMode, s}
info.SystemStatus = append(info.SystemStatus, customInfo)
customInfo = [2]string{whitelistRegistriesLabel, strings.Join(vchConfig.Whitelist.Strings(), ",")}
info.SystemStatus = append(info.SystemStatus, customInfo)
} else {
customInfo := [2]string{vchWhitelistMode, "disabled. All registry access allowed."}
info.SystemStatus = append(info.SystemStatus, customInfo)
}
}
return info, nil
}
// layout for build time as per constants defined in https://golang.org/src/time/format.go
const buildTimeLayout = "2006/01/02@15:04:05"
func (s *SystemBackend) SystemVersion() types.Version {
Arch := runtime.GOARCH
BuildTime := version.BuildDate
if t, err := time.Parse(buildTimeLayout, BuildTime); err == nil {
// match time format from docker version's output
BuildTime = t.Format(time.ANSIC)
}
Experimental := true
GitCommit := version.GitCommit
GoVersion := runtime.Version()
// FIXME: fill with real kernel version
KernelVersion := "-"
Os := runtime.GOOS
Version := version.Version
if Version != "" && Version[0] == 'v' {
// match version format from docker version's output
Version = Version[1:]
}
// go runtime panics without this so keep this here
// until we find a repro case and report it to upstream
_ = Arch
version := types.Version{
APIVersion: version.DockerAPIVersion,
MinAPIVersion: version.DockerMinimumVersion,
Arch: Arch,
BuildTime: BuildTime,
Experimental: Experimental,
GitCommit: GitCommit,
GoVersion: GoVersion,
KernelVersion: KernelVersion,
Os: Os,
Version: Version,
}
log.Infof("***** version = %#v", version)
return version
}
// SystemCPUMhzLimit will return the VCH configured Mhz limit
func (s *SystemBackend) SystemCPUMhzLimit() (int64, error) {
vchInfo, err := s.systemProxy.VCHInfo(context.Background())
if err != nil || vchInfo == nil {
return 0, err
}
return vchInfo.CPUMhz, nil
}
func (s *SystemBackend) SystemDiskUsage() (*types.DiskUsage, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "SystemDiskUsage")
}
func (s *SystemBackend) SubscribeToEvents(since, until time.Time, filter filters.Args) ([]eventtypes.Message, chan interface{}) {
defer trace.End(trace.Begin(""))
ef := events.NewFilter(filter)
return EventService().SubscribeTopic(since, until, ef)
}
func (s *SystemBackend) UnsubscribeFromEvents(listener chan interface{}) {
defer trace.End(trace.Begin(""))
EventService().Evict(listener)
}
// AuthenticateToRegistry handles docker logins
func (s *SystemBackend) AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) {
defer trace.End(trace.Begin(""))
// Only look at V2 registries
registryAddress := authConfig.ServerAddress
if !strings.Contains(authConfig.ServerAddress, "/v2") {
registryAddress = registryAddress + "/v2/"
}
if !strings.HasPrefix(registryAddress, "http") {
registryAddress = "//" + registryAddress
}
loginURL, err := url.Parse(registryAddress)
if err != nil {
msg := fmt.Sprintf("Bad login address: %s", registryAddress)
log.Errorf(msg)
return msg, "", err
}
// Check if registry is contained within whitelisted or insecure registries
regctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
whitelistOk, _, insecureOk := vchConfig.RegistryCheck(regctx, loginURL)
if !whitelistOk {
msg := fmt.Sprintf("Access denied to unauthorized registry (%s) while VCH is in whitelist mode", loginURL.Host)
return msg, "", fmt.Errorf(msg)
}
var certPool *x509.CertPool
if insecureOk {
log.Infof("Attempting to log into %s insecurely", loginURL.Host)
certPool = nil
} else {
certPool = RegistryCertPool
}
dologin := func(scheme string, skipVerify bool) (string, error) {
loginURL.Scheme = scheme
var authURL *url.URL
fetcher := urlfetcher.NewURLFetcher(urlfetcher.Options{
Timeout: loginTimeout,
Username: authConfig.Username,
Password: authConfig.Password,
RootCAs: certPool,
InsecureSkipVerify: skipVerify,
})
// Attempt to get the Auth URL from a simple ping operation (GET) to the registry
hdr, err := fetcher.Ping(loginURL)
if err == nil {
if fetcher.IsStatusUnauthorized() {
log.Debugf("Looking up OAuth URL from server %s", loginURL)
authURL, err = fetcher.ExtractOAuthURL(hdr.Get("www-authenticate"), nil)
} else {
// We're not suppose to be here, but if we do end up here, use the login
// URL for the auth URL.
authURL = loginURL
}
}
if err != nil {
log.Errorf("Looking up OAuth URL failed: %s", err)
return "", err
}
log.Debugf("logging onto %s", authURL.String())
// Just check if we get a token back.
token, err := fetcher.FetchAuthToken(authURL)
if err != nil || token.Token == "" {
// At this point, if a request cannot be solved by a retry, it is an authentication error.
log.Errorf("Fetch auth token failed: %s", err)
if _, ok := err.(urlfetcher.DoNotRetry); ok {
err = fmt.Errorf("Get %s: unauthorized: incorrect username or password", loginURL)
} else {
err = urlfetcher.AuthTokenError{TokenServer: *authURL}
}
return "", err
}
return token.Token, nil
}
_, err = dologin("https", insecureOk)
if err != nil && insecureOk {
_, err = dologin("http", insecureOk)
}
if err != nil {
return "", "", err
}
// We don't return the token. The config.json will store token if we return
// it, but the regular docker daemon doesn't seem to return it either.
return "Login Succeeded", "", nil
}
// Utility functions
func getImageCount() int {
images := cache.ImageCache().GetImages()
return len(images)
}
func FetchVolumeStores(client *client.PortLayer) (string, error) {
res, err := client.Storage.VolumeStoresList(storage.NewVolumeStoresListParamsWithContext(ctx))
if err != nil {
return "", err
}
return strings.Join(res.Payload.Stores, " "), nil
}
func entryStrJoin(entries registry.Set, sep string) string {
var s string
for _, e := range entries {
s += e.String() + sep
}
return s[:len(s)-len(sep)]
}

View File

@@ -0,0 +1,232 @@
// 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 backends
import (
"context"
"encoding/json"
"fmt"
//"regexp"
//"strconv"
//"strings"
"sync"
log "github.com/Sirupsen/logrus"
//derr "github.com/docker/docker/api/errors"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
//"github.com/docker/go-units"
//"github.com/google/uuid"
vicfilter "github.com/vmware/vic/lib/apiservers/engine/backends/filter"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"github.com/vmware/vic/lib/apiservers/engine/proxy"
"github.com/vmware/vic/lib/apiservers/portlayer/client/containers"
//"github.com/vmware/vic/lib/apiservers/portlayer/client/storage"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
"github.com/vmware/vic/pkg/trace"
)
// Volume which defines the docker personalities view of a Volume
type VolumeBackend struct {
storageProxy proxy.VicStorageProxy
}
// acceptedVolumeFilters are volume filters that are supported by VIC
var acceptedVolumeFilters = map[string]bool{
"dangling": true,
"name": true,
"driver": true,
"label": true,
}
var volumeBackend *VolumeBackend
var volOnce sync.Once
func NewVolumeBackend() *VolumeBackend {
volOnce.Do(func() {
volumeBackend = &VolumeBackend{
storageProxy: proxy.NewStorageProxy(PortLayerClient()),
}
})
return volumeBackend
}
// Volumes docker personality implementation for VIC
func (v *VolumeBackend) Volumes(filter string) ([]*types.Volume, []string, error) {
defer trace.End(trace.Begin(filter))
var volumes []*types.Volume
// Get volume list from the portlayer
volumeResponses, err := v.storageProxy.VolumeList(context.Background(), filter)
if err != nil {
return nil, nil, err
}
// Parse and validate filters
volumeFilters, err := filters.FromParam(filter)
if err != nil {
return nil, nil, errors.VolumeInternalServerError(err)
}
volFilterContext, err := vicfilter.ValidateVolumeFilters(volumeFilters, acceptedVolumeFilters, nil)
if err != nil {
return nil, nil, errors.VolumeInternalServerError(err)
}
// joinedVolumes stores names of volumes that are joined to a container
// and is used while filtering the output by dangling (dangling=true should
// return volumes that are not attached to a container)
joinedVolumes := make(map[string]struct{})
if volumeFilters.Include("dangling") {
// If the dangling filter is specified, gather required items beforehand
joinedVolumes, err = fetchJoinedVolumes()
if err != nil {
return nil, nil, errors.VolumeInternalServerError(err)
}
}
log.Infoln("volumes found:")
for _, vol := range volumeResponses {
log.Infof("%s", vol.Name)
volumeMetadata, err := extractDockerMetadata(vol.Metadata)
if err != nil {
return nil, nil, errors.VolumeInternalServerError(fmt.Errorf("error unmarshalling docker metadata: %s", err))
}
// Set fields needed for filtering the output
volFilterContext.Name = vol.Name
volFilterContext.Driver = vol.Driver
_, volFilterContext.Joined = joinedVolumes[vol.Name]
volFilterContext.Labels = volumeMetadata.Labels
// Include the volume in the output if it meets the filtering criteria
filterAction := vicfilter.IncludeVolume(volumeFilters, volFilterContext)
if filterAction == vicfilter.IncludeAction {
volume := NewVolumeModel(vol, volumeMetadata.Labels)
volumes = append(volumes, volume)
}
}
return volumes, nil, nil
}
// VolumeInspect : docker personality implementation for VIC
func (v *VolumeBackend) VolumeInspect(name string) (*types.Volume, error) {
defer trace.End(trace.Begin(name))
volInfo, err := v.storageProxy.VolumeInfo(context.Background(), name)
if err != nil {
return nil, err
}
volumeMetadata, err := extractDockerMetadata(volInfo.Metadata)
if err != nil {
return nil, errors.VolumeInternalServerError(fmt.Errorf("error unmarshalling docker metadata: %s", err))
}
volume := NewVolumeModel(volInfo, volumeMetadata.Labels)
return volume, nil
}
// VolumeCreate : docker personality implementation for VIC
func (v *VolumeBackend) VolumeCreate(name, driverName string, volumeData, labels map[string]string) (*types.Volume, error) {
defer trace.End(trace.Begin(name))
result, err := v.storageProxy.Create(context.Background(), name, driverName, volumeData, labels)
if err != nil {
return nil, err
}
return result, nil
}
// VolumeRm : docker personality for VIC
func (v *VolumeBackend) VolumeRm(name string, force bool) error {
defer trace.End(trace.Begin(name))
err := v.storageProxy.Remove(context.Background(), name)
if err != nil {
return err
}
return nil
}
func (v *VolumeBackend) VolumesPrune(pruneFilters filters.Args) (*types.VolumesPruneReport, error) {
return nil, errors.APINotSupportedMsg(ProductName(), "VolumesPrune")
}
//------------------------------------
// Utility Functions
//------------------------------------
func NewVolumeModel(volume *models.VolumeResponse, labels map[string]string) *types.Volume {
return &types.Volume{
Driver: volume.Driver,
Name: volume.Name,
Labels: labels,
Mountpoint: volume.Label,
}
}
// fetchJoinedVolumes obtains all containers from the portlayer and returns a map with all
// volumes that are joined to at least one container.
func fetchJoinedVolumes() (map[string]struct{}, error) {
conts, err := allContainers()
if err != nil {
return nil, errors.VolumeInternalServerError(err)
}
joinedVolumes := make(map[string]struct{})
var s struct{}
for i := range conts {
for _, vol := range conts[i].VolumeConfig {
joinedVolumes[vol.Name] = s
}
}
return joinedVolumes, nil
}
// allContainers obtains all containers from the portlayer, akin to `docker ps -a`.
func allContainers() ([]*models.ContainerInfo, error) {
client := PortLayerClient()
if client == nil {
return nil, errors.NillPortlayerClientError("Volume Backend")
}
all := true
cons, err := client.Containers.GetContainerList(containers.NewGetContainerListParamsWithContext(ctx).WithAll(&all))
if err != nil {
return nil, err
}
return cons.Payload, nil
}
// Unmarshal the docker metadata using the docker metadata key. The docker
// metadatakey. We stash the vals we know about in that map with that key.
func extractDockerMetadata(metadataMap map[string]string) (*proxy.VolumeMetadata, error) {
v, ok := metadataMap[proxy.DockerMetadataModelKey]
if !ok {
return nil, fmt.Errorf("metadata %s missing", proxy.DockerMetadataModelKey)
}
result := &proxy.VolumeMetadata{}
err := json.Unmarshal([]byte(v), result)
return result, err
}

View File

@@ -0,0 +1,63 @@
// 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 backends
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/apiservers/engine/proxy"
)
func TestExtractDockerMetadata(t *testing.T) {
driver := "vsphere"
volumeName := "testVolume"
store := "storeName"
testCap := "512"
testOptMap := make(map[string]string)
testOptMap[proxy.OptsVolumeStoreKey] = store
testOptMap[proxy.OptsCapacityKey] = testCap
testLabelMap := make(map[string]string)
testLabelMap["someLabel"] = "this is a label"
metaDataBefore := proxy.VolumeMetadata{
Driver: driver,
Name: volumeName,
DriverOpts: testOptMap,
Labels: testLabelMap,
}
buf, err := json.Marshal(metaDataBefore)
if !assert.NoError(t, err) {
return
}
metadataMap := make(map[string]string)
metadataMap[proxy.DockerMetadataModelKey] = string(buf)
metadataAfter, err := extractDockerMetadata(metadataMap)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, metaDataBefore.DriverOpts[proxy.OptsCapacityKey], metadataAfter.DriverOpts[proxy.OptsCapacityKey])
assert.Equal(t, metaDataBefore.DriverOpts[proxy.OptsVolumeStoreKey], metadataAfter.DriverOpts[proxy.OptsVolumeStoreKey])
assert.Equal(t, metaDataBefore.Labels["someLabel"], metadataAfter.Labels["someLabel"])
assert.Equal(t, metaDataBefore.Name, metadataAfter.Name)
assert.Equal(t, metaDataBefore.Driver, metadataAfter.Driver)
}