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:
275
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/archive_proxy.go
generated
vendored
Normal file
275
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/archive_proxy.go
generated
vendored
Normal 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
|
||||
}
|
||||
34
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/client.go
generated
vendored
Normal file
34
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/client.go
generated
vendored
Normal 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
|
||||
}
|
||||
19
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/common.go
generated
vendored
Normal file
19
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/common.go
generated
vendored
Normal 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"
|
||||
)
|
||||
1379
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/container_proxy.go
generated
vendored
Normal file
1379
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/container_proxy.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
83
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/container_proxy_test.go
generated
vendored
Normal file
83
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/container_proxy_test.go
generated
vendored
Normal 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])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
626
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/storage_proxy.go
generated
vendored
Normal file
626
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/storage_proxy.go
generated
vendored
Normal 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
|
||||
}
|
||||
129
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/storage_proxy_test.go
generated
vendored
Normal file
129
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/storage_proxy_test.go
generated
vendored
Normal 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")
|
||||
}
|
||||
495
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/stream_proxy.go
generated
vendored
Normal file
495
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/stream_proxy.go
generated
vendored
Normal 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
|
||||
}
|
||||
132
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/system_proxy.go
generated
vendored
Normal file
132
vendor/github.com/vmware/vic/lib/apiservers/engine/proxy/system_proxy.go
generated
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user