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

View File

@@ -0,0 +1,19 @@
// 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 constants
const (
DefaultVolumeDriver = "vsphere"
)

View File

@@ -0,0 +1,182 @@
// 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 errors
import (
"fmt"
"net/http"
derr "github.com/docker/docker/api/errors"
)
// Used to check status code of derr, which is not a public type
type httpStatusError interface {
HTTPErrorStatusCode() int
}
// InvalidVolumeError is returned when the user specifies a client directory as a volume.
type InvalidVolumeError struct {
}
func (e InvalidVolumeError) Error() string {
return fmt.Sprintf("mounting directories as a data volume is not supported.")
}
// InvalidBindError is returned when create/run -v has more params than allowed.
type InvalidBindError struct {
Volume string
}
func (e InvalidBindError) Error() string {
return fmt.Sprintf("volume bind input is invalid: -v %s", e.Volume)
}
type ServerNotReadyError struct {
Name string
}
func (e ServerNotReadyError) Error() string {
return fmt.Sprintf("Server %s not ready", e.Name)
}
func APINotSupportedMsg(product, method string) error {
return fmt.Errorf("%s does not yet implement %s", product, method)
}
func NillPortlayerClientError(caller string) error {
return derr.NewErrorWithStatusCode(fmt.Errorf("%s failed to get a portlayer client", caller), http.StatusInternalServerError)
}
// VolumeJoinNotFoundError returns a 404 docker error for a volume join request.
func VolumeJoinNotFoundError(msg string) error {
return derr.NewRequestNotFoundError(fmt.Errorf(msg))
}
// VolumeCreateNotFoundError returns a 404 docker error for a volume create request.
func VolumeCreateNotFoundError(msg string) error {
return derr.NewErrorWithStatusCode(fmt.Errorf("No volume store named (%s) exists", msg), http.StatusInternalServerError)
}
// VolumeNotFoundError returns a 404 docker error for a volume get request.
func VolumeNotFoundError(msg string) error {
return derr.NewErrorWithStatusCode(fmt.Errorf("No such volume: %s", msg), http.StatusNotFound)
}
// VolumeInternalServerError returns a 500 docker error for a volume-related request.
func VolumeInternalServerError(err error) error {
return derr.NewErrorWithStatusCode(err, http.StatusInternalServerError)
}
func ContainerResourceNotFoundError(cid, res string) error {
return derr.NewRequestNotFoundError(fmt.Errorf("No such %s for container: %s", res, cid))
}
func ResourceNotFoundError(res string) error {
return derr.NewRequestNotFoundError(fmt.Errorf("No such %s", res))
}
// NotFoundError returns a 404 docker error when a container is not found.
func NotFoundError(msg string) error {
return derr.NewRequestNotFoundError(fmt.Errorf("No such container: %s", msg))
}
func ImageNotFoundError(image, tag string) error {
return derr.NewRequestNotFoundError(fmt.Errorf("An image does not exist locally with the tag: %s", image))
}
func TagNotFoundError(image, tag string) error {
return derr.NewRequestNotFoundError(fmt.Errorf("tag does not exist: %s:%s", image, tag))
}
// ResourceLockedError returns a 423 http status
func ResourceLockedError(msg string) error {
return derr.NewErrorWithStatusCode(fmt.Errorf("Resource locked: %s", msg), http.StatusLocked)
}
// InternalServerError returns a 500 docker error on a portlayer error.
func InternalServerError(msg string) error {
return derr.NewErrorWithStatusCode(fmt.Errorf("Server error from portlayer: %s", msg), http.StatusInternalServerError)
}
// BadRequestError returns a 400 docker error on a bad request.
func BadRequestError(msg string) error {
return derr.NewErrorWithStatusCode(fmt.Errorf("Bad request error from portlayer: %s", msg), http.StatusBadRequest)
}
func ConflictError(msg string) error {
return derr.NewRequestConflictError(fmt.Errorf("Conflict error from portlayer: %s", msg))
}
func PluginNotFoundError(name string) error {
return derr.NewErrorWithStatusCode(fmt.Errorf("plugin %s not found", name), http.StatusNotFound)
}
func SwarmNotSupportedError() error {
return derr.NewErrorWithStatusCode(fmt.Errorf("Docker Swarm is not yet supported"), http.StatusNotFound)
}
func StreamFormatNotRecognized() error {
return derr.NewRequestConflictError(fmt.Errorf("Stream format not recognized"))
}
func ConcurrentAPIError(name, request string) error {
return derr.NewRequestConflictError(fmt.Errorf("%s request is already in progress for container '%s'.", request, name))
}
// Error type check
func IsNotFoundError(err error) bool {
// if error was created with the docker error function, check the status code
if httpErr, ok := err.(httpStatusError); ok {
if httpErr.HTTPErrorStatusCode() == http.StatusNotFound {
return true
}
}
return false
}
func IsConflictError(err error) bool {
// if error was created with the docker error function, check the status code
if httpErr, ok := err.(httpStatusError); ok {
if httpErr.HTTPErrorStatusCode() == http.StatusConflict {
return true
}
}
return false
}
func IsResourceInUse(err error) bool {
if httpErr, ok := err.(httpStatusError); ok {
if httpErr.HTTPErrorStatusCode() == http.StatusLocked {
return true
}
}
return false
}
func IsServerNotReady(err error) bool {
_, ok := err.(ServerNotReadyError)
return ok
}
type DetachError struct{}
func (DetachError) Error() string {
return "detached from container"
}

View File

@@ -0,0 +1,614 @@
// 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 network
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/docker/libnetwork/iptables"
"github.com/docker/libnetwork/portallocator"
"github.com/vishvananda/netlink"
viccontainer "github.com/vmware/vic/lib/apiservers/engine/backends/container"
"github.com/vmware/vic/lib/apiservers/engine/backends/portmap"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
"github.com/vmware/vic/lib/config/executor"
)
const (
bridgeIfaceName = "bridge"
)
var (
publicIfaceName = "public"
portMapper portmap.PortMapper
// bridge-to-bridge rules, indexed by mapped port;
// this map is used to delete the rule once
// the container stops or is removed
btbRules map[string][]string
cbpLock sync.Mutex
ContainerByPort map[string]string // port:containerID
)
func init() {
portMapper = portmap.NewPortMapper()
btbRules = make(map[string][]string)
ContainerByPort = make(map[string]string)
l, err := netlink.LinkByName(publicIfaceName)
if l == nil {
l, err = netlink.LinkByAlias(publicIfaceName)
if err != nil {
log.Errorf("interface %s not found", publicIfaceName)
return
}
}
// don't use interface alias for iptables rules
publicIfaceName = l.Attrs().Name
}
// requestHostPort finds a free port on the host
func requestHostPort(proto string) (int, error) {
pa := portallocator.Get()
return pa.RequestPortInRange(nil, proto, 0, 0)
}
type portMapping struct {
intHostPort int
strHostPort string
portProto nat.Port
}
// unrollPortMap processes config for mapping/unmapping ports e.g. from hostconfig.PortBindings
func unrollPortMap(portMap nat.PortMap) ([]*portMapping, error) {
var portMaps []*portMapping
for i, pb := range portMap {
proto, port := nat.SplitProtoPort(string(i))
nport, err := nat.NewPort(proto, port)
if err != nil {
return nil, err
}
// iterate over all the ports in pb []nat.PortBinding
for i := range pb {
var hostPort int
var hPort string
if pb[i].HostPort == "" {
// use a random port since no host port is specified
hostPort, err = requestHostPort(proto)
if err != nil {
log.Errorf("could not find available port on host")
return nil, err
}
log.Infof("using port %d on the host for port mapping", hostPort)
// update the hostconfig
pb[i].HostPort = strconv.Itoa(hostPort)
} else {
hostPort, err = strconv.Atoi(pb[i].HostPort)
if err != nil {
return nil, err
}
}
hPort = strconv.Itoa(hostPort)
portMaps = append(portMaps, &portMapping{
intHostPort: hostPort,
strHostPort: hPort,
portProto: nport,
})
}
}
return portMaps, nil
}
// MapPorts maps ports defined in bridge endpoint for containerID
func MapPorts(vc *viccontainer.VicContainer, endpoint *models.EndpointConfig, containerID string) error {
if endpoint == nil {
return fmt.Errorf("invalid endpoint")
}
var containerIP net.IP
containerIP = net.ParseIP(endpoint.Address)
if containerIP == nil {
return fmt.Errorf("invalid endpoint address %s", endpoint.Address)
}
portMap := addIndirectEndpointsToPortMap([]*models.EndpointConfig{endpoint}, nil)
log.Debugf("Mapping ports of %q on endpoint %s: %v", containerID, endpoint.Name, portMap)
if len(portMap) == 0 {
return nil
}
mappings, err := unrollPortMap(portMap)
if err != nil {
return err
}
// cannot occur direct under the lock below because unmap ports take a lock.
defer func() {
if err != nil {
// if we didn't succeed then make sure we clean up
UnmapPorts(containerID, vc)
}
}()
cbpLock.Lock()
defer cbpLock.Unlock()
vc.NATMap = portMap
for _, p := range mappings {
// update mapped ports
if ContainerByPort[p.strHostPort] == containerID {
log.Debugf("Skipping mapping for already mapped port %s for %s", p.strHostPort, containerID)
continue
}
if err = portMapper.MapPort(nil, p.intHostPort, p.portProto.Proto(), containerIP.String(), p.portProto.Int(), publicIfaceName, bridgeIfaceName); err != nil {
return err
}
// bridge-to-bridge pin hole for traffic from containers for exposed port
if err = interBridgeTraffic(portmap.Map, p.strHostPort, p.portProto.Proto(), containerIP.String(), p.portProto.Port()); err != nil {
return err
}
// update mapped ports
ContainerByPort[p.strHostPort] = containerID
log.Debugf("mapped port %s for container %s", p.strHostPort, containerID)
}
return nil
}
// UnmapPorts unmaps ports defined in hostconfig if it's mapped for this container
func UnmapPorts(id string, vc *viccontainer.VicContainer) error {
portMap := vc.NATMap
log.Debugf("UnmapPorts for %s: %v", vc.ContainerID, portMap)
if len(portMap) == 0 {
return nil
}
mappings, err := unrollPortMap(vc.NATMap)
if err != nil {
return err
}
cbpLock.Lock()
defer cbpLock.Unlock()
vc.NATMap = nil
for _, p := range mappings {
// check if we should actually unmap based on current mappings
mappedID, mapped := ContainerByPort[p.strHostPort]
if !mapped {
log.Debugf("skipping already unmapped %s", p.strHostPort)
continue
}
if mappedID != id {
log.Debugf("port is mapped for container %s, not %s, skipping", mappedID, id)
continue
}
if err = portMapper.UnmapPort(nil, p.intHostPort, p.portProto.Proto(), p.portProto.Int(), publicIfaceName, bridgeIfaceName); err != nil {
log.Warnf("failed to unmap port %s: %s", p.strHostPort, err)
continue
}
// bridge-to-bridge pin hole for traffic from containers for exposed port
if err = interBridgeTraffic(portmap.Unmap, p.strHostPort, "", "", ""); err != nil {
log.Warnf("failed to undo bridge-to-bridge pinhole %s: %s", p.strHostPort, err)
continue
}
// update mapped ports
delete(ContainerByPort, p.strHostPort)
log.Debugf("unmapped port %s", p.strHostPort)
}
return nil
}
// interBridgeTraffic enables traffic for exposed port from one bridge network to another
func interBridgeTraffic(op portmap.Operation, hostPort, proto, containerAddr, containerPort string) error {
switch op {
case portmap.Map:
switch proto {
case "udp", "tcp":
default:
return fmt.Errorf("unknown protocol: %s", proto)
}
// rule to allow connections from bridge interface for the
// specific mapped port. has to inserted at the top of the
// chain rather than appended to supersede bridge-to-bridge
// traffic blocking
baseArgs := []string{"-t", string(iptables.Filter),
"-i", bridgeIfaceName,
"-o", bridgeIfaceName,
"-p", proto,
"-d", containerAddr,
"--dport", containerPort,
"-j", "ACCEPT",
}
args := append([]string{string(iptables.Insert), "VIC", "1"}, baseArgs...)
if _, err := iptables.Raw(args...); err != nil && !os.IsExist(err) {
return err
}
btbRules[hostPort] = baseArgs
case portmap.Unmap:
if args, ok := btbRules[hostPort]; ok {
args = append([]string{string(iptables.Delete), "VIC"}, args...)
if _, err := iptables.Raw(args...); err != nil && !os.IsNotExist(err) {
return err
}
delete(btbRules, hostPort)
}
}
return nil
}
func PublicIPv4Addrs() ([]string, error) {
l, err := netlink.LinkByName(publicIfaceName)
if err != nil {
return nil, fmt.Errorf("could not look up link from interface name %s: %s", publicIfaceName, err.Error())
}
addrs, err := netlink.AddrList(l, netlink.FAMILY_V4)
if err != nil {
return nil, fmt.Errorf("could not get addresses from public link: %s", err.Error())
}
ips := make([]string, len(addrs))
for i := range addrs {
ips[i] = addrs[i].IP.String()
}
return ips, nil
}
// portMapFromContainer constructs a docker portmap from the container's
// info as returned by the portlayer and adds nil entries for any exposed ports
// that are unmapped
func PortMapFromContainer(vc *viccontainer.VicContainer, t *models.ContainerInfo) nat.PortMap {
var mappings nat.PortMap
if t != nil {
mappings = addDirectEndpointsToPortMap(t.Endpoints, mappings)
}
if vc != nil && vc.Config != nil {
if vc.NATMap != nil {
// if there's a NAT map for the container then just use that for the indirect port set
mappings = mergePortMaps(vc.NATMap, mappings)
} else {
// if there's no NAT map then we use the backend data every time
mappings = addIndirectEndpointsToPortMap(t.Endpoints, mappings)
}
mappings = addExposedToPortMap(vc.Config, mappings)
}
return mappings
}
func ContainerWithPort(hostPort string) (string, bool) {
cbpLock.Lock()
mappedCtr, mapped := ContainerByPort[hostPort]
cbpLock.Unlock()
return mappedCtr, mapped
}
// mergePortMaps creates a new map containing the union of the two arguments
func mergePortMaps(map1, map2 nat.PortMap) nat.PortMap {
resultMap := make(map[nat.Port][]nat.PortBinding)
for k, v := range map1 {
resultMap[k] = v
}
for k, v := range map2 {
vr := resultMap[k]
resultMap[k] = append(vr, v...)
}
return resultMap
}
// addIndirectEndpointToPortMap constructs a docker portmap from the container's info as returned by the portlayer for those ports that
// require NAT forward on the endpointVM.
// The portMap provided is modified and returned - the return value should always be used.
func addIndirectEndpointsToPortMap(endpoints []*models.EndpointConfig, portMap nat.PortMap) nat.PortMap {
if len(endpoints) == 0 {
return portMap
}
// will contain a combined set of port mappings
if portMap == nil {
portMap = make(nat.PortMap)
}
// add IP address into port spec to allow direct usage of data returned by calls such as docker port
var ip string
ips, _ := PublicIPv4Addrs()
if len(ips) > 0 {
ip = ips[0]
}
// Preserve the existing behaviour if we do not have an IP for some reason.
if ip == "" {
ip = "0.0.0.0"
}
for _, ep := range endpoints {
if ep.Direct {
continue
}
for _, port := range ep.Ports {
mappings, err := nat.ParsePortSpec(port)
if err != nil {
log.Error(err)
// just continue if we do have partial port data
}
for i := range mappings {
p := mappings[i].Port
b := mappings[i].Binding
if b.HostIP == "" {
b.HostIP = ip
}
if mappings[i].Binding.HostPort == "" {
// leave this undefined for dynamic assignment
// TODO: for port stability over VCH restart we would expect to set the dynamically assigned port
// recorded in containerVM annotations here, so that the old host->port mapping is preserved.
}
log.Debugf("Adding indirect mapping for port %v: %v (%s)", p, b, port)
current, _ := portMap[p]
portMap[p] = append(current, b)
}
}
}
return portMap
}
// addDirectEndpointsToPortMap constructs a docker portmap from the container's info as returned by the portlayer for those
// ports exposed directly from the containerVM via container network
// The portMap provided is modified and returned - the return value should always be used.
func addDirectEndpointsToPortMap(endpoints []*models.EndpointConfig, portMap nat.PortMap) nat.PortMap {
if len(endpoints) == 0 {
return portMap
}
if portMap == nil {
portMap = make(nat.PortMap)
}
for _, ep := range endpoints {
if !ep.Direct {
continue
}
// add IP address into the port spec to allow direct usage of data returned by calls such as docker port
var ip string
rawIP, _, _ := net.ParseCIDR(ep.Address)
if rawIP != nil {
ip = rawIP.String()
}
if ip == "" {
ip = "0.0.0.0"
}
for _, port := range ep.Ports {
mappings, err := nat.ParsePortSpec(port)
if err != nil {
log.Error(err)
// just continue if we do have partial port data
}
for i := range mappings {
if mappings[i].Binding.HostIP == "" {
mappings[i].Binding.HostIP = ip
}
if mappings[i].Binding.HostPort == "" {
// If there's no explicit host port and it's a direct endpoint, then
// mirror the actual port. It's a bit misleading but we're trying to
// pack extended function into an existing structure.
_, p := nat.SplitProtoPort(string(mappings[i].Port))
mappings[i].Binding.HostPort = p
}
}
for _, mapping := range mappings {
p := mapping.Port
current, _ := portMap[p]
portMap[p] = append(current, mapping.Binding)
}
}
}
return portMap
}
// addExposedToPortMap ensures that exposed ports are all present in the port map.
// This means nil entries for any exposed ports that are not mapped.
// The portMap provided is modified and returned - the return value should always be used.
func addExposedToPortMap(config *container.Config, portMap nat.PortMap) nat.PortMap {
if config == nil || len(config.ExposedPorts) == 0 {
return portMap
}
if portMap == nil {
portMap = make(nat.PortMap)
}
for p := range config.ExposedPorts {
if _, ok := portMap[p]; ok {
continue
}
portMap[p] = nil
}
return portMap
}
func DirectPortInformation(t *models.ContainerInfo) []types.Port {
var resultPorts []types.Port
for _, ne := range t.Endpoints {
trust, _ := executor.ParseTrustLevel(ne.Trust)
if !ne.Direct || trust == executor.Closed || trust == executor.Outbound || trust == executor.Peers {
// we don't publish port info for ports that are not directly accessible from outside of the VCH
continue
}
ip := strings.SplitN(ne.Address, "/", 2)[0]
// if it's an open network then inject an "all ports" entry
if trust == executor.Open {
resultPorts = append(resultPorts, types.Port{
IP: ip,
PrivatePort: 0,
PublicPort: 0,
Type: "*",
})
}
for _, p := range ne.Ports {
port := types.Port{IP: ip}
portsAndType := strings.SplitN(p, "/", 2)
port.Type = portsAndType[1]
mapping := strings.Split(portsAndType[0], ":")
// if no mapping is supplied then there's only one and that's public. If there is a mapping then the first
// entry is the public
public, err := strconv.Atoi(mapping[0])
if err != nil {
log.Errorf("Got an error trying to convert public port number \"%s\" to an int: %s", mapping[0], err)
continue
}
port.PublicPort = uint16(public)
// If port is on container network then a different container could be forwarding the same port via the endpoint
// so must check for explicit ID match. If a match then it's definitely not accessed directly.
if ContainerByPort[mapping[0]] == t.ContainerConfig.ContainerID {
continue
}
// did not find a way to have the client not render both ports so setting them the same even if there's not
// redirect occurring
port.PrivatePort = port.PublicPort
// for open networks we don't bother listing direct ports
if len(mapping) == 1 {
if trust != executor.Open {
resultPorts = append(resultPorts, port)
}
continue
}
private, err := strconv.Atoi(mapping[1])
if err != nil {
log.Errorf("Got an error trying to convert private port number \"%s\" to an int: %s", mapping[1], err)
continue
}
port.PrivatePort = uint16(private)
resultPorts = append(resultPorts, port)
}
}
return resultPorts
}
// returns port bindings as a slice of Docker Ports for return to the client
// returns empty slice on error
//func PortForwardingInformation(t *models.ContainerInfo, ips []string) []types.Port {
func PortForwardingInformation(vc *viccontainer.VicContainer, ips []string) []types.Port {
//cid := t.ContainerConfig.ContainerID
//c := cache.ContainerCache().GetContainer(cid)
if vc == nil {
log.Errorf("Could not get port forwarding info for container")
return nil
}
portBindings := vc.NATMap
var resultPorts []types.Port
// create a port for each IP on the interface (usually only 1, but could be more)
// (works with both IPv4 and IPv6 addresses)
for _, ip := range ips {
port := types.Port{IP: ip}
for portBindingPrivatePort, hostPortBindings := range portBindings {
proto, pnum := nat.SplitProtoPort(string(portBindingPrivatePort))
portNum, err := strconv.Atoi(pnum)
if err != nil {
log.Warnf("Unable to convert private port %q to an int", pnum)
continue
}
port.PrivatePort = uint16(portNum)
port.Type = proto
for i := 0; i < len(hostPortBindings); i++ {
// If port is on container network then a different container could be forwarding the same port via the endpoint
// so must check for explicit ID match. If no match, definitely not forwarded via endpoint.
//if ContainerByPort[hostPortBindings[i].HostPort] != t.ContainerConfig.ContainerID {
if ContainerByPort[hostPortBindings[i].HostPort] != vc.ContainerID {
continue
}
newport := port
publicPort, err := strconv.Atoi(hostPortBindings[i].HostPort)
if err != nil {
log.Infof("Got an error trying to convert public port number to an int")
continue
}
newport.PublicPort = uint16(publicPort)
// sanity check -- sometimes these come back as 0 when no binding actually exists
// that doesn't make sense, so in that case we don't want to report these bindings
if newport.PublicPort != 0 && newport.PrivatePort != 0 {
resultPorts = append(resultPorts, newport)
}
}
}
}
return resultPorts
}

View File

@@ -0,0 +1,275 @@
// 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 proxy
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"github.com/vmware/vic/lib/apiservers/portlayer/client"
"github.com/vmware/vic/lib/apiservers/portlayer/client/storage"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/pkg/trace"
"github.com/docker/docker/api/types"
)
type VicArchiveProxy interface {
ArchiveExportReader(op trace.Operation, store, ancestorStore, deviceID, ancestor string, data bool, filterSpec archive.FilterSpec) (io.ReadCloser, error)
ArchiveImportWriter(op trace.Operation, store, deviceID string, filterSpec archive.FilterSpec, wg *sync.WaitGroup, errchan chan error) (io.WriteCloser, error)
StatPath(op trace.Operation, store, deviceID string, filterSpec archive.FilterSpec) (*types.ContainerPathStat, error)
}
//------------------------------------
// ArchiveProxy
//------------------------------------
type ArchiveProxy struct {
client *client.PortLayer
}
var archiveProxy *ArchiveProxy
func NewArchiveProxy(client *client.PortLayer) VicArchiveProxy {
return &ArchiveProxy{client: client}
}
func GetArchiveProxy() VicArchiveProxy {
return archiveProxy
}
// ArchiveExportReader streams a tar archive from the portlayer. Once the stream is complete,
// an io.Reader is returned and the caller can use that reader to parse the data.
func (a *ArchiveProxy) ArchiveExportReader(op trace.Operation, store, ancestorStore, deviceID, ancestor string, data bool, filterSpec archive.FilterSpec) (io.ReadCloser, error) {
defer trace.End(trace.Begin(deviceID))
if a.client == nil {
return nil, errors.NillPortlayerClientError("ArchiveProxy")
}
if store == "" || deviceID == "" {
return nil, fmt.Errorf("ArchiveExportReader called with either empty store or deviceID")
}
var err error
pipeReader, pipeWriter := io.Pipe()
go func() {
// make sure we get out of io.Copy if context is canceled
select {
case <-op.Done():
// Attempt to tell the portlayer to cancel the stream. This is one way of cancelling the
// stream. The other way is for the caller of this function to close the returned CloseReader.
// Callers of this function should do one but not both.
err := pipeReader.Close()
if err != nil {
op.Errorf("Error closing pipereader in ArchiveExportReader: %s", err.Error())
}
}
}()
go func() {
params := storage.NewExportArchiveParamsWithContext(op).
WithStore(store).
WithAncestorStore(&ancestorStore).
WithDeviceID(deviceID).
WithAncestor(&ancestor).
WithData(data)
// Encode the filter spec
encodedFilter := ""
if valueBytes, merr := json.Marshal(filterSpec); merr == nil {
encodedFilter = base64.StdEncoding.EncodeToString(valueBytes)
params = params.WithFilterSpec(&encodedFilter)
op.Infof(" encodedFilter = %s", encodedFilter)
}
_, err = a.client.Storage.ExportArchive(params, pipeWriter)
if err != nil {
op.Errorf("Error from ExportArchive: %s", err.Error())
switch err := err.(type) {
case *storage.ExportArchiveInternalServerError:
plErr := errors.InternalServerError(fmt.Sprintf("Server error from archive reader for device %s", deviceID))
op.Errorf(plErr.Error())
pipeWriter.CloseWithError(plErr)
case *storage.ExportArchiveLocked:
plErr := errors.ResourceLockedError(fmt.Sprintf("Resource locked for device %s", deviceID))
op.Errorf(plErr.Error())
pipeWriter.CloseWithError(plErr)
case *storage.ExportArchiveUnprocessableEntity:
plErr := errors.InternalServerError("failed to process given path")
op.Errorf(plErr.Error())
pipeWriter.CloseWithError(plErr)
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(), SwaggerSubstringEOF) {
op.Debugf("swagger error %s", err.Error())
pipeWriter.Close()
} else {
pipeWriter.CloseWithError(err)
}
}
} else {
pipeWriter.Close()
}
}()
return pipeReader, nil
}
// ArchiveImportWriter initializes a write stream for a path. This is usually called
// for getting a writer during docker cp TO container.
func (a *ArchiveProxy) ArchiveImportWriter(op trace.Operation, store, deviceID string, filterSpec archive.FilterSpec, wg *sync.WaitGroup, errchan chan error) (io.WriteCloser, error) {
defer trace.End(trace.Begin(deviceID))
if a.client == nil {
return nil, errors.NillPortlayerClientError("ArchiveProxy")
}
if store == "" || deviceID == "" {
return nil, fmt.Errorf("ArchiveImportWriter called with either empty store or deviceID")
}
var err error
pipeReader, pipeWriter := io.Pipe()
go func() {
// make sure we get out of io.Copy if context is canceled
select {
case <-op.Done():
pipeWriter.Close()
}
}()
wg.Add(1)
go func() {
var plErr error
defer func() {
op.Debugf("Stream for device %s has returned from PL. Err received is %v ", deviceID, plErr)
errchan <- plErr
wg.Done()
}()
// encodedFilter and destination are not required (from swagge spec) because
// they are allowed to be empty.
params := storage.NewImportArchiveParamsWithContext(op).
WithStore(store).
WithDeviceID(deviceID).
WithArchive(pipeReader)
// Encode the filter spec
encodedFilter := ""
if valueBytes, merr := json.Marshal(filterSpec); merr == nil {
encodedFilter = base64.StdEncoding.EncodeToString(valueBytes)
params = params.WithFilterSpec(&encodedFilter)
}
_, err = a.client.Storage.ImportArchive(params)
if err != nil {
switch err := err.(type) {
case *storage.ImportArchiveInternalServerError:
plErr = errors.InternalServerError(fmt.Sprintf("error writing files to device %s", deviceID))
op.Errorf(plErr.Error())
pipeReader.CloseWithError(plErr)
case *storage.ImportArchiveLocked:
plErr = errors.ResourceLockedError(fmt.Sprintf("resource locked for device %s", deviceID))
op.Errorf(plErr.Error())
pipeReader.CloseWithError(plErr)
case *storage.ImportArchiveNotFound:
plErr = errors.ResourceNotFoundError("file or directory")
op.Errorf(plErr.Error())
pipeReader.CloseWithError(plErr)
case *storage.ImportArchiveUnprocessableEntity:
plErr = errors.InternalServerError("failed to process given path")
op.Errorf(plErr.Error())
pipeReader.CloseWithError(plErr)
case *storage.ImportArchiveConflict:
plErr = errors.InternalServerError("unexpected copy failure may result in truncated copy, please try again")
op.Errorf(plErr.Error())
pipeReader.CloseWithError(plErr)
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
plErr = err
if strings.Contains(err.Error(), SwaggerSubstringEOF) {
op.Error(err)
pipeReader.Close()
} else {
pipeReader.CloseWithError(err)
}
}
} else {
pipeReader.Close()
}
}()
return pipeWriter, nil
}
// StatPath requests the portlayer to stat the filesystem resource at the
// specified path in the container vc.
func (a *ArchiveProxy) StatPath(op trace.Operation, store, deviceID string, filterSpec archive.FilterSpec) (*types.ContainerPathStat, error) {
defer trace.End(trace.Begin(deviceID))
if a.client == nil {
return nil, errors.NillPortlayerClientError("ArchiveProxy")
}
statPathParams := storage.
NewStatPathParamsWithContext(op).
WithStore(store).
WithDeviceID(deviceID)
spec, err := archive.EncodeFilterSpec(op, &filterSpec)
if err != nil {
op.Errorf(err.Error())
return nil, errors.InternalServerError(err.Error())
}
statPathParams = statPathParams.WithFilterSpec(spec)
statPathOk, err := a.client.Storage.StatPath(statPathParams)
if err != nil {
op.Errorf(err.Error())
return nil, err
}
stat := &types.ContainerPathStat{
Name: statPathOk.Name,
Mode: os.FileMode(statPathOk.Mode),
Size: statPathOk.Size,
LinkTarget: statPathOk.LinkTarget,
}
var modTime time.Time
if err := modTime.GobDecode([]byte(statPathOk.ModTime)); err != nil {
op.Debugf("error getting mod time from statpath: %s", err.Error())
} else {
stat.Mtime = modTime
}
return stat, nil
}

View File

@@ -0,0 +1,34 @@
// Copyright 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 proxy
import (
"github.com/go-openapi/runtime"
rc "github.com/go-openapi/runtime/client"
apiclient "github.com/vmware/vic/lib/apiservers/portlayer/client"
)
func NewPortLayerClient(portLayerAddr string) *apiclient.PortLayer {
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)
return portLayerClient
}

View File

@@ -0,0 +1,19 @@
// 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 proxy
const (
SwaggerSubstringEOF = "EOF"
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
// 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 proxy
import (
"testing"
"github.com/docker/go-connections/nat"
"github.com/stretchr/testify/assert"
)
func TestProcessVolumeParams(t *testing.T) {
rawTestVolumes := []string{"/blah", "testVolume:/mount", "testVolume:/mount/path:r"}
invalidVolume := "/dir:/dir"
var processedTestVolumes []volumeFields
for _, testString := range rawTestVolumes {
processedFields, err := processVolumeParam(testString)
assert.Nil(t, err)
processedTestVolumes = append(processedTestVolumes, processedFields)
}
assert.Equal(t, 3, len(processedTestVolumes))
assert.NotEmpty(t, processedTestVolumes[0].ID)
assert.Equal(t, "/blah", processedTestVolumes[0].Dest)
assert.Equal(t, "rw", processedTestVolumes[0].Flags)
assert.Equal(t, "testVolume", processedTestVolumes[1].ID)
assert.Equal(t, "/mount", processedTestVolumes[1].Dest)
assert.Equal(t, "rw", processedTestVolumes[1].Flags)
assert.Equal(t, "testVolume", processedTestVolumes[2].ID)
assert.Equal(t, "/mount/path", processedTestVolumes[2].Dest)
assert.Equal(t, "r", processedTestVolumes[2].Flags)
invalidFields, _ := processVolumeParam(invalidVolume)
assert.Equal(t, volumeFields{}, invalidFields)
}
func TestPort(t *testing.T) {
portMap, bindingMap, err := nat.ParsePortSpecs([]string{
"1236:1235/tcp",
"1237:1235/tcp",
"2345/udp", "80",
"127.0.0.1::8080",
"127.0.0.1:5279:8080",
})
if err != nil {
t.Errorf("Failed to parse ports: %s", err.Error())
}
t.Logf("portMap: %s", portMap)
t.Logf("bindingMap: %s", bindingMap)
for p := range bindingMap {
expected := bindingMap[p]
for i := range expected {
expected[i].HostIP = ""
}
bindings := fromPortbinding(p, bindingMap[p])
t.Logf("binding: %s", bindings)
_, outMap, err := nat.ParsePortSpecs(bindings)
if err != nil {
t.Errorf("Failed to parse back string bindings: %s", err)
}
for op := range outMap {
assert.Equal(t, outMap[op], bindingMap[op])
}
}
}

View File

@@ -0,0 +1,626 @@
// 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 proxy
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
log "github.com/Sirupsen/logrus"
"github.com/google/uuid"
derr "github.com/docker/docker/api/errors"
"github.com/docker/docker/api/types"
"github.com/docker/go-units"
viccontainer "github.com/vmware/vic/lib/apiservers/engine/backends/container"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"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/storage"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/pkg/trace"
)
type VicStorageProxy interface {
Create(ctx context.Context, name, driverName string, volumeData, labels map[string]string) (*types.Volume, error)
VolumeList(ctx context.Context, filter string) ([]*models.VolumeResponse, error)
VolumeInfo(ctx context.Context, name string) (*models.VolumeResponse, error)
Remove(ctx context.Context, name string) error
AddVolumesToContainer(ctx context.Context, handle string, config types.ContainerCreateConfig) (string, error)
}
type StorageProxy struct {
client *client.PortLayer
}
type volumeFields struct {
ID string
Dest string
Flags string
}
type VolumeMetadata struct {
Driver string
DriverOpts map[string]string
Name string
Labels map[string]string
AttachHistory []string
Image string
}
const (
DriverArgFlagKey = "flags"
DriverArgContainerKey = "container"
DriverArgImageKey = "image"
OptsVolumeStoreKey string = "volumestore"
OptsCapacityKey string = "capacity"
DockerMetadataModelKey string = "DockerMetaData"
)
// define a set (whitelist) of valid driver opts keys for command line argument validation
var validDriverOptsKeys = map[string]struct{}{
OptsVolumeStoreKey: {},
OptsCapacityKey: {},
DriverArgFlagKey: {},
DriverArgContainerKey: {},
DriverArgImageKey: {},
}
// Volume drivers currently supported. "local" is the default driver supplied by the client
// and is equivalent to "vsphere" for our implementation.
var SupportedVolDrivers = map[string]struct{}{
"vsphere": {},
"local": {},
}
//Validation pattern for Volume Names
var volumeNameRegex = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_.-]*$")
func NewStorageProxy(client *client.PortLayer) VicStorageProxy {
if client == nil {
return nil
}
return &StorageProxy{client: client}
}
func (s *StorageProxy) Create(ctx context.Context, name, driverName string, volumeData, labels map[string]string) (*types.Volume, error) {
defer trace.End(trace.Begin(""))
if s.client == nil {
return nil, errors.NillPortlayerClientError("StorageProxy")
}
result, err := s.volumeCreate(ctx, name, driverName, volumeData, labels)
if err != nil {
switch err := err.(type) {
case *storage.CreateVolumeConflict:
return result, errors.VolumeInternalServerError(fmt.Errorf("A volume named %s already exists. Choose a different volume name.", name))
case *storage.CreateVolumeNotFound:
return result, errors.VolumeInternalServerError(fmt.Errorf("No volume store named (%s) exists", volumeStore(volumeData)))
case *storage.CreateVolumeInternalServerError:
// FIXME: right now this does not return an error model...
return result, errors.VolumeInternalServerError(fmt.Errorf("%s", err.Error()))
case *storage.CreateVolumeDefault:
return result, errors.VolumeInternalServerError(fmt.Errorf("%s", err.Payload.Message))
default:
return result, errors.VolumeInternalServerError(fmt.Errorf("%s", err))
}
}
return result, nil
}
// volumeCreate issues a CreateVolume request to the portlayer
func (s *StorageProxy) volumeCreate(ctx context.Context, name, driverName string, volumeData, labels map[string]string) (*types.Volume, error) {
defer trace.End(trace.Begin(""))
result := &types.Volume{}
if s.client == nil {
return nil, errors.NillPortlayerClientError("StorageProxy")
}
if name == "" {
name = uuid.New().String()
}
// TODO: support having another driver besides vsphere.
// assign the values of the model to be passed to the portlayer handler
req, varErr := newVolumeCreateReq(name, driverName, volumeData, labels)
if varErr != nil {
return result, varErr
}
log.Infof("Finalized model for volume create request to portlayer: %#v", req)
res, err := s.client.Storage.CreateVolume(storage.NewCreateVolumeParamsWithContext(ctx).WithVolumeRequest(req))
if err != nil {
return result, err
}
return NewVolumeModel(res.Payload, labels), nil
}
func (s *StorageProxy) VolumeList(ctx context.Context, filter string) ([]*models.VolumeResponse, error) {
defer trace.End(trace.Begin(""))
if s.client == nil {
return nil, errors.NillPortlayerClientError("StorageProxy")
}
res, err := s.client.Storage.ListVolumes(storage.NewListVolumesParamsWithContext(ctx).WithFilterString(&filter))
if err != nil {
switch err := err.(type) {
case *storage.ListVolumesInternalServerError:
return nil, errors.VolumeInternalServerError(fmt.Errorf("error from portlayer server: %s", err.Payload.Message))
case *storage.ListVolumesDefault:
return nil, errors.VolumeInternalServerError(fmt.Errorf("error from portlayer server: %s", err.Payload.Message))
default:
return nil, errors.VolumeInternalServerError(fmt.Errorf("error from portlayer server: %s", err.Error()))
}
}
return res.Payload, nil
}
func (s *StorageProxy) VolumeInfo(ctx context.Context, name string) (*models.VolumeResponse, error) {
defer trace.End(trace.Begin(name))
if name == "" {
return nil, nil
}
if s.client == nil {
return nil, errors.NillPortlayerClientError("StorageProxy")
}
param := storage.NewGetVolumeParamsWithContext(ctx).WithName(name)
res, err := s.client.Storage.GetVolume(param)
if err != nil {
switch err := err.(type) {
case *storage.GetVolumeNotFound:
return nil, errors.VolumeNotFoundError(name)
default:
return nil, errors.VolumeInternalServerError(fmt.Errorf("error from portlayer server: %s", err.Error()))
}
}
return res.Payload, nil
}
func (s *StorageProxy) Remove(ctx context.Context, name string) error {
defer trace.End(trace.Begin(name))
if s.client == nil {
return errors.NillPortlayerClientError("StorageProxy")
}
_, err := s.client.Storage.RemoveVolume(storage.NewRemoveVolumeParamsWithContext(ctx).WithName(name))
if err != nil {
switch err := err.(type) {
case *storage.RemoveVolumeNotFound:
return derr.NewRequestNotFoundError(fmt.Errorf("Get %s: no such volume", name))
case *storage.RemoveVolumeConflict:
return derr.NewRequestConflictError(fmt.Errorf(err.Payload.Message))
case *storage.RemoveVolumeInternalServerError:
return errors.VolumeInternalServerError(fmt.Errorf("Server error from portlayer: %s", err.Payload.Message))
default:
return errors.VolumeInternalServerError(fmt.Errorf("Server error from portlayer: %s", err))
}
}
return nil
}
// AddVolumesToContainer adds volumes to a container, referenced by handle.
// If an error is returned, the returned handle should not be used.
//
// returns:
// modified handle
func (s *StorageProxy) AddVolumesToContainer(ctx context.Context, handle string, config types.ContainerCreateConfig) (string, error) {
defer trace.End(trace.Begin(handle))
if s.client == nil {
return "", errors.NillPortlayerClientError("StorageProxy")
}
// Volume Attachment Section
log.Debugf("ContainerProxy.AddVolumesToContainer - VolumeSection")
log.Debugf("Raw volume arguments: binds: %#v, volumes: %#v", config.HostConfig.Binds, config.Config.Volumes)
// Collect all volume mappings. In a docker create/run, they
// can be anonymous (-v /dir) or specific (-v vol-name:/dir).
// anonymous volumes can also come from Image Metadata
rawAnonVolumes := make([]string, 0, len(config.Config.Volumes))
for k := range config.Config.Volumes {
rawAnonVolumes = append(rawAnonVolumes, k)
}
volList, err := finalizeVolumeList(config.HostConfig.Binds, rawAnonVolumes)
if err != nil {
return handle, errors.BadRequestError(err.Error())
}
log.Infof("Finalized volume list: %#v", volList)
if len(config.Config.Volumes) > 0 {
// override anonymous volume list with generated volume id
for _, vol := range volList {
if _, ok := config.Config.Volumes[vol.Dest]; ok {
delete(config.Config.Volumes, vol.Dest)
mount := getMountString(vol.ID, vol.Dest, vol.Flags)
config.Config.Volumes[mount] = struct{}{}
log.Debugf("Replace anonymous volume config %s with %s", vol.Dest, mount)
}
}
}
// Create and join volumes.
for _, fields := range volList {
// We only set these here for volumes made on a docker create
volumeData := make(map[string]string)
volumeData[DriverArgFlagKey] = fields.Flags
volumeData[DriverArgContainerKey] = config.Name
volumeData[DriverArgImageKey] = config.Config.Image
// NOTE: calling volumeCreate regardless of whether the volume is already
// present can be avoided by adding an extra optional param to VolumeJoin,
// which would then call volumeCreate if the volume does not exist.
_, err := s.volumeCreate(ctx, fields.ID, "vsphere", volumeData, nil)
if err != nil {
switch err := err.(type) {
case *storage.CreateVolumeConflict:
// Implicitly ignore the error where a volume with the same name
// already exists. We can just join the said volume to the container.
log.Infof("a volume with the name %s already exists", fields.ID)
case *storage.CreateVolumeNotFound:
return handle, errors.VolumeCreateNotFoundError(volumeStore(volumeData))
default:
return handle, errors.InternalServerError(err.Error())
}
} else {
log.Infof("volumeCreate succeeded. Volume mount section ID: %s", fields.ID)
}
flags := make(map[string]string)
//NOTE: for now we are passing the flags directly through. This is NOT SAFE and only a stop gap.
flags[constants.Mode] = fields.Flags
joinParams := storage.NewVolumeJoinParamsWithContext(ctx).WithJoinArgs(&models.VolumeJoinConfig{
Flags: flags,
Handle: handle,
MountPath: fields.Dest,
}).WithName(fields.ID)
res, err := s.client.Storage.VolumeJoin(joinParams)
if err != nil {
switch err := err.(type) {
case *storage.VolumeJoinInternalServerError:
return handle, errors.InternalServerError(err.Payload.Message)
case *storage.VolumeJoinDefault:
return handle, errors.InternalServerError(err.Payload.Message)
case *storage.VolumeJoinNotFound:
return handle, errors.VolumeJoinNotFoundError(err.Payload.Message)
default:
return handle, errors.InternalServerError(err.Error())
}
}
handle = res.Payload
}
return handle, nil
}
// allContainers obtains all containers from the portlayer, akin to `docker ps -a`.
func (s *StorageProxy) allContainers(ctx context.Context) ([]*models.ContainerInfo, error) {
if s.client == nil {
return nil, errors.NillPortlayerClientError("StorageProxy")
}
all := true
cons, err := s.client.Containers.GetContainerList(containers.NewGetContainerListParamsWithContext(ctx).WithAll(&all))
if err != nil {
return nil, err
}
return cons.Payload, nil
}
// fetchJoinedVolumes obtains all containers from the portlayer and returns a map with all
// volumes that are joined to at least one container.
func (s *StorageProxy) fetchJoinedVolumes(ctx context.Context) (map[string]struct{}, error) {
conts, err := s.allContainers(ctx)
if err != nil {
return nil, errors.VolumeInternalServerError(err)
}
joinedVolumes := make(map[string]struct{})
var v struct{}
for i := range conts {
for _, vol := range conts[i].VolumeConfig {
joinedVolumes[vol.Name] = v
}
}
return joinedVolumes, nil
}
//------------------------------------
// 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,
}
}
// volumeStore returns the value of the optional volume store param specified in the CLI.
func volumeStore(args map[string]string) string {
storeName, ok := args[OptsVolumeStoreKey]
if !ok {
return "default"
}
return storeName
}
// getMountString returns a colon-delimited string containing a volume's name/ID, mount
// point and flags.
func getMountString(mounts ...string) string {
return strings.Join(mounts, ":")
}
func createVolumeMetadata(req *models.VolumeRequest, driverargs, labels map[string]string) (string, error) {
metadata := VolumeMetadata{
Driver: req.Driver,
DriverOpts: req.DriverArgs,
Name: req.Name,
Labels: labels,
AttachHistory: []string{driverargs[DriverArgContainerKey]},
Image: driverargs[DriverArgImageKey],
}
result, err := json.Marshal(metadata)
return string(result), err
}
// RemoveAnonContainerVols removes anonymous volumes joined to a container. It is invoked
// once the said container has been removed. It fetches a list of volumes that are joined
// to at least one other container, and calls the portlayer to remove this container's
// anonymous volumes if they are dangling. Errors, if any, are only logged.
func RemoveAnonContainerVols(ctx context.Context, pl *client.PortLayer, cID string, vc *viccontainer.VicContainer) {
// NOTE: these strings come in the form of <volume id>:<destination>:<volume options>
volumes := vc.Config.Volumes
// NOTE: these strings come in the form of <volume id>:<destination path>
namedVolumes := vc.HostConfig.Binds
// assemble a mask of volume paths before processing binds. MUST be paths, as we want to move to honoring the proper metadata in the "volumes" section in the future.
namedMaskList := make(map[string]struct{}, 0)
for _, entry := range namedVolumes {
fields := strings.SplitN(entry, ":", 2)
if len(fields) != 2 {
log.Errorf("Invalid entry in the HostConfig.Binds metadata section for container %s: %s", cID, entry)
continue
}
destPath := fields[1]
namedMaskList[destPath] = struct{}{}
}
proxy := StorageProxy{client: pl}
joinedVols, err := proxy.fetchJoinedVolumes(ctx)
if err != nil {
log.Errorf("Unable to obtain joined volumes from portlayer, skipping removal of anonymous volumes for %s: %s", cID, err.Error())
return
}
for vol := range volumes {
// Extract the volume ID from the full mount path, which is of form "id:mountpath:flags" - see getMountString().
volFields := strings.SplitN(vol, ":", 3)
// NOTE(mavery): this check will start to fail when we fix our metadata correctness issues
if len(volFields) != 3 {
log.Debugf("Invalid entry in the volumes metadata section for container %s: %s", cID, vol)
continue
}
volName := volFields[0]
volPath := volFields[1]
_, isNamed := namedMaskList[volPath]
_, joined := joinedVols[volName]
if !joined && !isNamed {
_, err := pl.Storage.RemoveVolume(storage.NewRemoveVolumeParamsWithContext(ctx).WithName(volName))
if err != nil {
log.Debugf("Unable to remove anonymous volume %s in container %s: %s", volName, cID, err.Error())
continue
}
log.Debugf("Successfully removed anonymous volume %s during remove operation against container(%s)", volName, cID)
}
}
}
// processVolumeParam is used to turn any call from docker create -v <stuff> into a volumeFields object.
// The -v has 3 forms. -v <anonymous mount path>, -v <Volume Name>:<Destination Mount Path> and
// -v <Volume Name>:<Destination Mount Path>:<mount flags>
func processVolumeParam(volString string) (volumeFields, error) {
volumeStrings := strings.Split(volString, ":")
fields := volumeFields{}
// Error out if the intended volume is a directory on the client filesystem.
numVolParams := len(volumeStrings)
if numVolParams > 1 && strings.HasPrefix(volumeStrings[0], "/") {
return volumeFields{}, errors.InvalidVolumeError{}
}
// This switch determines which type of -v was invoked.
switch numVolParams {
case 1:
VolumeID, err := uuid.NewUUID()
if err != nil {
return fields, err
}
fields.ID = VolumeID.String()
fields.Dest = volumeStrings[0]
fields.Flags = "rw"
case 2:
fields.ID = volumeStrings[0]
fields.Dest = volumeStrings[1]
fields.Flags = "rw"
case 3:
fields.ID = volumeStrings[0]
fields.Dest = volumeStrings[1]
fields.Flags = volumeStrings[2]
default:
// NOTE: the docker cli should cover this case. This is here for posterity.
return volumeFields{}, errors.InvalidBindError{Volume: volString}
}
return fields, nil
}
// processVolumeFields parses fields for volume mappings specified in a create/run -v.
// It returns a map of unique mountable volumes. This means that it removes dupes favoring
// specified volumes over anonymous volumes.
func processVolumeFields(volumes []string) (map[string]volumeFields, error) {
volumeFields := make(map[string]volumeFields)
for _, v := range volumes {
fields, err := processVolumeParam(v)
log.Infof("Processed volume arguments: %#v", fields)
if err != nil {
return nil, err
}
volumeFields[fields.Dest] = fields
}
return volumeFields, nil
}
func finalizeVolumeList(specifiedVolumes, anonymousVolumes []string) ([]volumeFields, error) {
log.Infof("Specified Volumes : %#v", specifiedVolumes)
processedVolumes, err := processVolumeFields(specifiedVolumes)
if err != nil {
return nil, err
}
log.Infof("anonymous Volumes : %#v", anonymousVolumes)
processedAnonVolumes, err := processVolumeFields(anonymousVolumes)
if err != nil {
return nil, err
}
//combine all volumes, specified volumes are taken over anonymous volumes
for k, v := range processedVolumes {
processedAnonVolumes[k] = v
}
finalizedVolumes := make([]volumeFields, 0, len(processedAnonVolumes))
for _, v := range processedAnonVolumes {
finalizedVolumes = append(finalizedVolumes, v)
}
return finalizedVolumes, nil
}
func newVolumeCreateReq(name, driverName string, volumeData, labels map[string]string) (*models.VolumeRequest, error) {
if _, ok := SupportedVolDrivers[driverName]; !ok {
return nil, fmt.Errorf("error looking up volume plugin %s: plugin not found", driverName)
}
if !volumeNameRegex.Match([]byte(name)) && name != "" {
return nil, fmt.Errorf("volume name %q includes invalid characters, only \"[a-zA-Z0-9][a-zA-Z0-9_.-]\" are allowed", name)
}
req := &models.VolumeRequest{
Driver: driverName,
DriverArgs: volumeData,
Name: name,
Metadata: make(map[string]string),
}
metadata, err := createVolumeMetadata(req, volumeData, labels)
if err != nil {
return nil, err
}
req.Metadata[DockerMetadataModelKey] = metadata
if err := validateDriverArgs(volumeData, req); err != nil {
return nil, fmt.Errorf("bad driver value - %s", err)
}
return req, nil
}
func validateDriverArgs(args map[string]string, req *models.VolumeRequest) error {
if err := normalizeDriverArgs(args); err != nil {
return err
}
// volumestore name validation
req.Store = volumeStore(args)
// capacity validation
capstr, ok := args[OptsCapacityKey]
if !ok {
req.Capacity = -1
return nil
}
//check if it is just a numerical value
capacity, err := strconv.ParseInt(capstr, 10, 64)
if err == nil {
//input has no units in this case.
if capacity < 1 {
return fmt.Errorf("Invalid size: %s", capstr)
}
req.Capacity = capacity
return nil
}
capacity, err = units.FromHumanSize(capstr)
if err != nil {
return err
}
if capacity < 1 {
return fmt.Errorf("Capacity value too large: %s", capstr)
}
req.Capacity = int64(capacity) / int64(units.MB)
return nil
}
func normalizeDriverArgs(args map[string]string) error {
// normalize keys to lowercase & validate them
for k, val := range args {
lowercase := strings.ToLower(k)
if _, ok := validDriverOptsKeys[lowercase]; !ok {
return fmt.Errorf("%s is not a supported option", k)
}
if strings.Compare(lowercase, k) != 0 {
delete(args, k)
args[lowercase] = val
}
}
return nil
}

View File

@@ -0,0 +1,129 @@
// 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 proxy
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/apiservers/portlayer/models"
)
func TestFillDockerVolume(t *testing.T) {
testResponse := &models.VolumeResponse{
Driver: "vsphere",
Name: "Test Volume",
Label: "Test Label",
}
testLabels := make(map[string]string)
testLabels["TestMeta"] = "custom info about my volume"
dockerVolume := NewVolumeModel(testResponse, testLabels)
assert.Equal(t, "vsphere", dockerVolume.Driver)
assert.Equal(t, "Test Volume", dockerVolume.Name)
assert.Equal(t, "Test Label", dockerVolume.Mountpoint)
assert.Equal(t, "custom info about my volume", dockerVolume.Labels["TestMeta"])
}
func TestTranslatVolumeRequestModel(t *testing.T) {
testLabels := make(map[string]string)
testLabels["TestMeta"] = "custom info about my volume"
testDriverArgs := make(map[string]string)
testDriverArgs["testarg"] = "important driver stuff"
testDriverArgs[OptsVolumeStoreKey] = "testStore"
testDriverArgs[OptsCapacityKey] = "12MB"
testRequest, err := newVolumeCreateReq("testName", "vsphere", testDriverArgs, testLabels)
if !assert.Error(t, err) {
return
}
delete(testDriverArgs, "testarg")
testRequest, err = newVolumeCreateReq("testName", "vsphere", testDriverArgs, testLabels)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, "testName", testRequest.Name)
assert.Equal(t, "", testRequest.DriverArgs["testarg"]) // unsupported keys should just be empty
assert.Equal(t, "testStore", testRequest.Store)
assert.Equal(t, "vsphere", testRequest.Driver)
assert.Equal(t, int64(12), testRequest.Capacity)
testMetaDatabuf, err := createVolumeMetadata(testRequest, testDriverArgs, testLabels)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, testMetaDatabuf, testRequest.Metadata[DockerMetadataModelKey])
assert.Nil(t, err)
}
func TestValidateDriverArgs(t *testing.T) {
testMap := make(map[string]string)
testStore := "Mystore"
testCap := "12MB"
testBadCap := "This is not valid!"
testModel := models.VolumeRequest{
Driver: "vsphere",
DriverArgs: testMap,
Name: "testModel",
}
err := validateDriverArgs(testMap, &testModel)
if !assert.Equal(t, "default", testModel.Store) || !assert.Equal(t, int64(-1), testModel.Capacity) || !assert.NoError(t, err) {
return
}
testMap[OptsVolumeStoreKey] = testStore
testMap[OptsCapacityKey] = testCap
err = validateDriverArgs(testMap, &testModel)
if !assert.Equal(t, testStore, testModel.Store) || !assert.Equal(t, int64(12), testModel.Capacity) || !assert.NoError(t, err) {
return
}
//This is a negative test case. We want an error
testMap[OptsCapacityKey] = testBadCap
err = validateDriverArgs(testMap, &testModel)
if !assert.Equal(t, testStore, testModel.Store) || !assert.Equal(t, int64(12), testModel.Capacity) || !assert.Error(t, err) {
return
}
testMap[OptsCapacityKey] = testCap
delete(testMap, OptsVolumeStoreKey)
err = validateDriverArgs(testMap, &testModel)
if !assert.Equal(t, "default", testModel.Store) || !assert.Equal(t, int64(12), testModel.Capacity) || !assert.NoError(t, err) {
return
}
}
func TestNormalizeDriverArgs(t *testing.T) {
testOptMap := make(map[string]string)
testOptMap["VOLUMESTORE"] = "foo"
testOptMap["CAPACITY"] = "bar"
normalizeDriverArgs(testOptMap)
assert.Equal(t, testOptMap["volumestore"], "foo")
assert.Equal(t, testOptMap["capacity"], "bar")
testOptMap["bogus"] = "bogus"
err := normalizeDriverArgs(testOptMap)
assert.Error(t, err, "expected: bogus is not a supported option")
}

View File

@@ -0,0 +1,495 @@
// 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 proxy
import (
"context"
"fmt"
"io"
"strings"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/go-openapi/strfmt"
"github.com/docker/docker/api/types/backend"
"github.com/docker/docker/pkg/term"
"github.com/vmware/vic/lib/apiservers/engine/backends/convert"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"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/interaction"
"github.com/vmware/vic/pkg/trace"
)
type VicStreamProxy interface {
AttachStreams(ctx context.Context, ac *AttachConfig, stdin io.ReadCloser, stdout, stderr io.Writer) error
StreamContainerLogs(ctx context.Context, name string, out io.Writer, started chan struct{}, showTimestamps bool, followLogs bool, since int64, tailLines int64) error
StreamContainerStats(ctx context.Context, config *convert.ContainerStatsConfig) error
}
type StreamProxy struct {
client *client.PortLayer
}
const (
attachConnectTimeout time.Duration = 15 * time.Second //timeout for the connection
attachAttemptTimeout time.Duration = 60 * time.Second //timeout before we ditch an attach attempt
attachPLAttemptDiff time.Duration = 10 * time.Second
attachStdinInitString = "v1c#>"
archiveStreamBufSize = 64 * 1024
)
// AttachConfig wraps backend.ContainerAttachConfig and adds other required fields
// Similar to https://github.com/docker/docker/blob/master/container/stream/attach.go
type AttachConfig struct {
*backend.ContainerAttachConfig
// ID of the session
ID string
// Tells the attach copier that the stream's stdin is a TTY and to look for
// escape sequences in stdin to detach from the stream.
// When true the escape sequence is not passed to the underlying stream
UseTty bool
// CloseStdin signals that once done, stdin for the attached stream should be closed
// For example, this would close the attached container's stdin.
CloseStdin bool
}
func NewStreamProxy(client *client.PortLayer) VicStreamProxy {
return &StreamProxy{client: client}
}
// AttachStreams takes the the hijacked connections from the calling client and attaches
// them to the 3 streams from the portlayer's rest server.
// stdin, stdout, stderr are the hijacked connection
func (s *StreamProxy) AttachStreams(ctx context.Context, ac *AttachConfig, stdin io.ReadCloser, stdout, stderr io.Writer) error {
// Cancel will close the child connections.
var wg, outWg sync.WaitGroup
if s.client == nil {
return errors.NillPortlayerClientError("StreamProxy")
}
errChan := make(chan error, 3)
var keys []byte
var err error
if ac.DetachKeys != "" {
keys, err = term.ToBytes(ac.DetachKeys)
if err != nil {
return fmt.Errorf("Invalid escape keys (%s) provided", ac.DetachKeys)
}
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if ac.UseStdin {
wg.Add(1)
}
if ac.UseStdout {
wg.Add(1)
outWg.Add(1)
}
if ac.UseStderr {
wg.Add(1)
outWg.Add(1)
}
// cancel stdin if all output streams are complete
go func() {
outWg.Wait()
cancel()
}()
EOForCanceled := func(err error) bool {
return err != nil && ctx.Err() != context.Canceled && !strings.HasSuffix(err.Error(), SwaggerSubstringEOF)
}
if ac.UseStdin {
go func() {
defer wg.Done()
err := copyStdIn(ctx, s.client, ac, stdin, keys)
if err != nil {
log.Errorf("container attach: stdin (%s): %s", ac.ID, err)
} else {
log.Infof("container attach: stdin (%s) done", ac.ID)
}
if !ac.CloseStdin || ac.UseTty {
cancel()
}
// Check for EOF or canceled context. We can only detect EOF by checking the error string returned by swagger :/
if EOForCanceled(err) {
errChan <- err
}
}()
}
if ac.UseStdout {
go func() {
defer outWg.Done()
defer wg.Done()
err := copyStdOut(ctx, s.client, ac, stdout, attachAttemptTimeout)
if err != nil {
log.Errorf("container attach: stdout (%s): %s", ac.ID, err)
} else {
log.Infof("container attach: stdout (%s) done", ac.ID)
}
// Check for EOF or canceled context. We can only detect EOF by checking the error string returned by swagger :/
if EOForCanceled(err) {
errChan <- err
}
}()
}
if ac.UseStderr {
go func() {
defer outWg.Done()
defer wg.Done()
err := copyStdErr(ctx, s.client, ac, stderr)
if err != nil {
log.Errorf("container attach: stderr (%s): %s", ac.ID, err)
} else {
log.Infof("container attach: stderr (%s) done", ac.ID)
}
// Check for EOF or canceled context. We can only detect EOF by checking the error string returned by swagger :/
if EOForCanceled(err) {
errChan <- err
}
}()
}
// Wait for all stream copy to exit
wg.Wait()
// close the channel so that we don't leak (if there is an error)/or get blocked (if there are no errors)
close(errChan)
log.Infof("cleaned up connections to %s. Checking errors", ac.ID)
for err := range errChan {
if err != nil {
// check if we got DetachError
if _, ok := err.(errors.DetachError); ok {
log.Infof("Detached from container detected")
return err
}
// If we get here, most likely something went wrong with the port layer API server
// These errors originate within the go-swagger client itself.
// Go-swagger returns untyped errors to us if the error is not one that we define
// in the swagger spec. Even EOF. Therefore, we must scan the error string (if there
// is an error string in the untyped error) for the term EOF.
log.Errorf("container attach error: %s", err)
return err
}
}
log.Infof("No error found. Returning nil...")
return nil
}
// StreamContainerLogs reads the log stream from the portlayer rest server and writes
// it directly to the io.Writer that is passed in.
func (s *StreamProxy) StreamContainerLogs(ctx context.Context, name string, out io.Writer, started chan struct{}, showTimestamps bool, followLogs bool, since int64, tailLines int64) error {
defer trace.End(trace.Begin(""))
if s.client == nil {
return errors.NillPortlayerClientError("StreamProxy")
}
close(started)
params := containers.NewGetContainerLogsParamsWithContext(ctx).
WithID(name).
WithFollow(&followLogs).
WithTimestamp(&showTimestamps).
WithSince(&since).
WithTaillines(&tailLines)
_, err := s.client.Containers.GetContainerLogs(params, out)
if err != nil {
switch err := err.(type) {
case *containers.GetContainerLogsNotFound:
return errors.NotFoundError(name)
case *containers.GetContainerLogsInternalServerError:
return errors.InternalServerError("Server error from the interaction 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(), SwaggerSubstringEOF) {
return nil
}
return errors.InternalServerError(fmt.Sprintf("Unknown error from the interaction port layer: %s", err))
}
}
return nil
}
// StreamContainerStats will provide a stream of container stats written to the provided
// io.Writer. Prior to writing to the provided io.Writer there will be a transformation
// from the portLayer representation of stats to the docker format
func (s *StreamProxy) StreamContainerStats(ctx context.Context, config *convert.ContainerStatsConfig) error {
defer trace.End(trace.Begin(config.ContainerID))
if s.client == nil {
return errors.NillPortlayerClientError("StreamProxy")
}
// create a child context that we control
ctx, cancel := context.WithCancel(ctx)
defer cancel()
params := containers.NewGetContainerStatsParamsWithContext(ctx)
params.ID = config.ContainerID
params.Stream = config.Stream
config.Ctx = ctx
config.Cancel = cancel
// create our converter
containerConverter := convert.NewContainerStats(config)
// provide the writer for the portLayer and start listening for metrics
writer := containerConverter.Listen()
if writer == nil {
// problem with the listener
return errors.InternalServerError(fmt.Sprintf("unable to gather container(%s) statistics", config.ContainerID))
}
_, err := s.client.Containers.GetContainerStats(params, writer)
if err != nil {
switch err := err.(type) {
case *containers.GetContainerStatsNotFound:
return errors.NotFoundError(config.ContainerID)
case *containers.GetContainerStatsInternalServerError:
return errors.InternalServerError("Server error from the interaction port layer")
default:
if ctx.Err() == context.Canceled {
return nil
}
//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(), SwaggerSubstringEOF) {
return nil
}
return errors.InternalServerError(fmt.Sprintf("Unknown error from the interaction port layer: %s", err))
}
}
return nil
}
//------------------------------------
// ContainerAttach() Utility Functions
//------------------------------------
func copyStdIn(ctx context.Context, pl *client.PortLayer, ac *AttachConfig, stdin io.ReadCloser, keys []byte) error {
// Pipe for stdin so we can interject and watch the input streams for detach keys.
stdinReader, stdinWriter := io.Pipe()
defer stdinReader.Close()
var detach bool
done := make(chan struct{})
go func() {
// make sure we get out of io.Copy if context is canceled
select {
case <-ctx.Done():
// This will cause the transport to the API client to be shut down, so all output
// streams will get closed as well.
// See the closer in container_routes.go:postContainersAttach
// We're closing this here to disrupt the io.Copy below
// TODO: seems like we should be providing an io.Copy impl with ctx argument that honors
// cancelation with the amount of code dedicated to working around it
// TODO: I think this still leaves a race between closing of the API client transport and
// copying of the output streams, it's just likely the error will be dropped as the transport is
// closed when it occurs.
// We should move away from needing to close transports to interrupt reads.
stdin.Close()
case <-done:
}
}()
go func() {
defer close(done)
defer stdinWriter.Close()
// Copy the stdin from the CLI and write to a pipe. We need to do this so we can
// watch the stdin stream for the detach keys.
var err error
// Write some init bytes into the pipe to force Swagger to make the initial
// call to the portlayer, prior to any user input in whatever attach client
// he/she is using.
log.Debugf("copyStdIn writing primer bytes")
stdinWriter.Write([]byte(attachStdinInitString))
if ac.UseTty {
_, err = copyEscapable(stdinWriter, stdin, keys)
} else {
_, err = io.Copy(stdinWriter, stdin)
}
if err != nil {
if _, ok := err.(errors.DetachError); ok {
log.Infof("stdin detach detected")
detach = true
} else {
log.Errorf("stdin err: %s", err)
}
}
}()
id := ac.ID
// Swagger wants an io.reader so give it the reader pipe. Also, the swagger call
// to set the stdin is synchronous so we need to run in a goroutine
setStdinParams := interaction.NewContainerSetStdinParamsWithContext(ctx).WithID(id)
setStdinParams = setStdinParams.WithRawStream(stdinReader)
_, err := pl.Interaction.ContainerSetStdin(setStdinParams)
<-done
if ac.CloseStdin && !ac.UseTty {
// Close the stdin connection. Mimicing Docker's behavior.
log.Errorf("Attach stream has stdinOnce set. Closing the stdin.")
params := interaction.NewContainerCloseStdinParamsWithContext(ctx).WithID(id)
_, err := pl.Interaction.ContainerCloseStdin(params)
if err != nil {
log.Errorf("CloseStdin failed with %s", err)
}
}
// ignore the portlayer error when it is DetachError as that is what we should return to the caller when we detach
if detach {
return errors.DetachError{}
}
return err
}
func copyStdOut(ctx context.Context, pl *client.PortLayer, ac *AttachConfig, stdout io.Writer, attemptTimeout time.Duration) error {
id := ac.ID
//Calculate how much time to let portlayer attempt
plAttemptTimeout := attemptTimeout - attachPLAttemptDiff //assumes personality deadline longer than portlayer's deadline
plAttemptDeadline := time.Now().Add(plAttemptTimeout)
swaggerDeadline := strfmt.DateTime(plAttemptDeadline)
log.Debugf("* stdout portlayer deadline: %s", plAttemptDeadline.Format(time.UnixDate))
log.Debugf("* stdout personality deadline: %s", time.Now().Add(attemptTimeout).Format(time.UnixDate))
log.Debugf("* stdout attach start %s", time.Now().Format(time.UnixDate))
getStdoutParams := interaction.NewContainerGetStdoutParamsWithContext(ctx).WithID(id).WithDeadline(&swaggerDeadline)
_, err := pl.Interaction.ContainerGetStdout(getStdoutParams, stdout)
log.Debugf("* stdout attach end %s", time.Now().Format(time.UnixDate))
if err != nil {
if _, ok := err.(*interaction.ContainerGetStdoutNotFound); ok {
return errors.ContainerResourceNotFoundError(id, "interaction connection")
}
return errors.InternalServerError(err.Error())
}
return nil
}
func copyStdErr(ctx context.Context, pl *client.PortLayer, ac *AttachConfig, stderr io.Writer) error {
id := ac.ID
getStderrParams := interaction.NewContainerGetStderrParamsWithContext(ctx).WithID(id)
_, err := pl.Interaction.ContainerGetStderr(getStderrParams, stderr)
if err != nil {
if _, ok := err.(*interaction.ContainerGetStderrNotFound); ok {
errors.ContainerResourceNotFoundError(id, "interaction connection")
}
return errors.InternalServerError(err.Error())
}
return nil
}
// FIXME: Move this function to a pkg to show it's origination from Docker once
// we have ignore capabilities in our header-check.sh that checks for copyright
// header.
// Code c/c from io.Copy() modified by Docker to handle escape sequence
// Begin
func copyEscapable(dst io.Writer, src io.ReadCloser, keys []byte) (written int64, err error) {
if len(keys) == 0 {
// Default keys : ctrl-p ctrl-q
keys = []byte{16, 17}
}
buf := make([]byte, 32*1024)
for {
nr, er := src.Read(buf)
if nr > 0 {
// ---- Docker addition
preservBuf := []byte{}
for i, key := range keys {
preservBuf = append(preservBuf, buf[0:nr]...)
if nr != 1 || buf[0] != key {
break
}
if i == len(keys)-1 {
src.Close()
return 0, errors.DetachError{}
}
nr, er = src.Read(buf)
}
var nw int
var ew error
if len(preservBuf) > 0 {
nw, ew = dst.Write(preservBuf)
nr = len(preservBuf)
} else {
// ---- End of docker
nw, ew = dst.Write(buf[0:nr])
}
if nw > 0 {
written += int64(nw)
}
if ew != nil {
err = ew
break
}
if nr != nw {
err = io.ErrShortWrite
break
}
}
if er == io.EOF {
break
}
if er != nil {
err = er
break
}
}
return written, err
}

View File

@@ -0,0 +1,132 @@
// 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 proxy
//****
// system_proxy.go
//
// Contains all code that touches the portlayer for system operations and all
// code that converts swagger based returns to docker personality backend structs.
// The goal is to make the backend code that implements the docker engine-api
// interfaces be as simple as possible and contain no swagger or portlayer code.
//
// Rule for code to be in here:
// 1. touches VIC portlayer
// 2. converts swagger to docker engine-api structs
// 3. errors MUST be docker engine-api compatible errors. DO NOT return arbitrary errors!
// - Do NOT return portlayer errors
// - Do NOT return fmt.Errorf()
// - Do NOT return errors.New()
// - DO USE the aliased docker error package 'derr'
import (
"fmt"
"net/http"
"golang.org/x/net/context"
log "github.com/Sirupsen/logrus"
derr "github.com/docker/docker/api/errors"
"github.com/vmware/vic/lib/apiservers/engine/errors"
"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/models"
"github.com/vmware/vic/pkg/trace"
)
type VicSystemProxy interface {
PingPortlayer(ctx context.Context) bool
ContainerCount(ctx context.Context) (int, int, int, error)
VCHInfo(ctx context.Context) (*models.VCHInfo, error)
}
type SystemProxy struct {
client *client.PortLayer
}
func NewSystemProxy(client *client.PortLayer) VicSystemProxy {
if client == nil {
return nil
}
return &SystemProxy{client: client}
}
func (s *SystemProxy) PingPortlayer(ctx context.Context) bool {
defer trace.End(trace.Begin(""))
if s.client == nil {
log.Errorf("Portlayer client is invalid")
return false
}
pingParams := misc.NewPingParamsWithContext(ctx)
_, err := s.client.Misc.Ping(pingParams)
if err != nil {
log.Info("Ping to portlayer failed")
return false
}
return true
}
// Use the Portlayer's support for docker ps to get the container count
// return order: running, paused, stopped counts
func (s *SystemProxy) ContainerCount(ctx context.Context) (int, int, int, error) {
defer trace.End(trace.Begin(""))
var running, paused, stopped int
if s.client == nil {
return 0, 0, 0, errors.NillPortlayerClientError("SystemProxy")
}
all := true
containList, err := s.client.Containers.GetContainerList(containers.NewGetContainerListParamsWithContext(ctx).WithAll(&all))
if err != nil {
return 0, 0, 0, derr.NewErrorWithStatusCode(fmt.Errorf("Failed to get container list: %s", err), http.StatusInternalServerError)
}
for _, t := range containList.Payload {
st := t.ContainerConfig.State
if st == "Running" {
running++
} else if st == "Stopped" || st == "Created" {
stopped++
}
}
return running, paused, stopped, nil
}
func (s *SystemProxy) VCHInfo(ctx context.Context) (*models.VCHInfo, error) {
defer trace.End(trace.Begin(""))
if s.client == nil {
return nil, errors.NillPortlayerClientError("SystemProxy")
}
params := misc.NewGetVCHInfoParamsWithContext(ctx)
resp, err := s.client.Misc.GetVCHInfo(params)
if err != nil {
//There are no custom error for this operation. If we get back an error, it's
//unknown.
return nil, derr.NewErrorWithStatusCode(fmt.Errorf("Unknown error from port layer: %s", err),
http.StatusInternalServerError)
}
return resp.Payload, nil
}