Merge pull request #111620 from Jiawei0227/storageos

cleanup: Remove storageos volume plugins from k8s codebase
This commit is contained in:
Kubernetes Prow Robot 2022-08-03 18:05:36 -07:00 committed by GitHub
commit d4795e4bec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 0 additions and 5736 deletions

View File

@ -1,49 +0,0 @@
= vendor/github.com/storageos/go-api licensed under: =
MIT License
Copyright (c) 2015-2018 StorageOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Copyright (c) 2013-2017, go-dockerclient authors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
= vendor/github.com/storageos/go-api/LICENCE 10d8703157b5fd9422b1813bae555905

View File

@ -39,7 +39,6 @@ import (
"k8s.io/kubernetes/pkg/volume/iscsi"
"k8s.io/kubernetes/pkg/volume/local"
"k8s.io/kubernetes/pkg/volume/nfs"
"k8s.io/kubernetes/pkg/volume/storageos"
volumeutil "k8s.io/kubernetes/pkg/volume/util"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -59,7 +58,6 @@ func ProbeAttachableVolumePlugins() ([]volume.VolumePlugin, error) {
if err != nil {
return allPlugins, err
}
allPlugins = append(allPlugins, storageos.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, fc.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, iscsi.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, csi.ProbeVolumePlugins()...)
@ -82,7 +80,6 @@ func ProbeExpandableVolumePlugins(config persistentvolumeconfig.VolumeConfigurat
return allPlugins, err
}
allPlugins = append(allPlugins, glusterfs.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, storageos.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, fc.ProbeVolumePlugins()...)
return allPlugins, nil
}
@ -131,7 +128,6 @@ func ProbeControllerVolumePlugins(cloud cloudprovider.Interface, config persiste
}
allPlugins = append(allPlugins, local.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, storageos.ProbeVolumePlugins()...)
if utilfeature.DefaultFeatureGate.Enabled(features.CSIInlineVolume) {
allPlugins = append(allPlugins, csi.ProbeVolumePlugins()...)

View File

@ -38,7 +38,6 @@ import (
"k8s.io/kubernetes/pkg/volume/nfs"
"k8s.io/kubernetes/pkg/volume/projected"
"k8s.io/kubernetes/pkg/volume/secret"
"k8s.io/kubernetes/pkg/volume/storageos"
// Cloud providers
_ "k8s.io/kubernetes/pkg/cloudprovider/providers"
@ -72,7 +71,6 @@ func ProbeVolumePlugins(featureGate featuregate.FeatureGate) ([]volume.VolumePlu
allPlugins = append(allPlugins, configmap.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, projected.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, local.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, storageos.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, csi.ProbeVolumePlugins()...)
return allPlugins, nil
}

2
go.mod
View File

@ -65,7 +65,6 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.4.0
github.com/spf13/pflag v1.0.5
github.com/storageos/go-api v2.2.0+incompatible
github.com/stretchr/testify v1.7.0
github.com/vishvananda/netlink v1.1.0
github.com/vmware/govmomi v0.20.3
@ -489,7 +488,6 @@ replace (
github.com/spf13/cobra => github.com/spf13/cobra v1.4.0
github.com/spf13/pflag => github.com/spf13/pflag v1.0.5
github.com/stoewer/go-strcase => github.com/stoewer/go-strcase v1.2.0
github.com/storageos/go-api => github.com/storageos/go-api v2.2.0+incompatible
github.com/stretchr/objx => github.com/stretchr/objx v0.2.0
github.com/stretchr/testify => github.com/stretchr/testify v1.7.0
github.com/syndtr/gocapability => github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635

2
go.sum
View File

@ -390,8 +390,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/storageos/go-api v2.2.0+incompatible h1:U0SablXoZIg06gvSlg8BCdzq1C/SkHVygOVX95Z2MU0=
github.com/storageos/go-api v2.2.0+incompatible/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=

View File

@ -54,7 +54,6 @@ import (
"k8s.io/kubernetes/pkg/volume/projected"
"k8s.io/kubernetes/pkg/volume/rbd"
"k8s.io/kubernetes/pkg/volume/secret"
"k8s.io/kubernetes/pkg/volume/storageos"
"k8s.io/kubernetes/pkg/volume/util/hostutil"
"k8s.io/kubernetes/pkg/volume/util/subpath"
"k8s.io/kubernetes/test/utils"
@ -83,7 +82,6 @@ func volumePlugins() []volume.VolumePlugin {
allPlugins = append(allPlugins, projected.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, portworx.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, local.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, storageos.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, csi.ProbeVolumePlugins()...)
return allPlugins
}

View File

@ -1,19 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
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 storageos contains the internal representation of StorageOS
// PersistentDisk volumes.
package storageos // import "k8s.io/kubernetes/pkg/volume/storageos"

View File

@ -1,762 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
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 storageos
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"k8s.io/klog/v2"
"k8s.io/mount-utils"
utilexec "k8s.io/utils/exec"
utilstrings "k8s.io/utils/strings"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
clientset "k8s.io/client-go/kubernetes"
volumehelpers "k8s.io/cloud-provider/volume/helpers"
"k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/util"
)
// ProbeVolumePlugins is the primary entrypoint for volume plugins.
func ProbeVolumePlugins() []volume.VolumePlugin {
return []volume.VolumePlugin{&storageosPlugin{nil}}
}
type storageosPlugin struct {
host volume.VolumeHost
}
var _ volume.VolumePlugin = &storageosPlugin{}
var _ volume.PersistentVolumePlugin = &storageosPlugin{}
var _ volume.DeletableVolumePlugin = &storageosPlugin{}
var _ volume.ProvisionableVolumePlugin = &storageosPlugin{}
const (
storageosPluginName = "kubernetes.io/storageos"
defaultDeviceDir = "/var/lib/storageos/volumes"
defaultAPIAddress = "tcp://localhost:5705"
defaultAPIUser = "storageos"
defaultAPIPassword = "storageos"
defaultAPIVersion = "1"
defaultFSType = "ext4"
defaultNamespace = "default"
)
func getPath(uid types.UID, volNamespace string, volName string, pvName string, host volume.VolumeHost) string {
if len(volNamespace) != 0 && len(volName) != 0 && strings.Count(volName, ".") == 0 {
return host.GetPodVolumeDir(uid, utilstrings.EscapeQualifiedName(storageosPluginName), pvName+"."+volNamespace+"."+volName)
}
return host.GetPodVolumeDir(uid, utilstrings.EscapeQualifiedName(storageosPluginName), pvName)
}
func (plugin *storageosPlugin) Init(host volume.VolumeHost) error {
plugin.host = host
return nil
}
func (plugin *storageosPlugin) GetPluginName() string {
return storageosPluginName
}
func (plugin *storageosPlugin) GetVolumeName(spec *volume.Spec) (string, error) {
volumeSource, _, err := getVolumeSource(spec)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/%s", volumeSource.VolumeNamespace, volumeSource.VolumeName), nil
}
func (plugin *storageosPlugin) CanSupport(spec *volume.Spec) bool {
return (spec.PersistentVolume != nil && spec.PersistentVolume.Spec.StorageOS != nil) ||
(spec.Volume != nil && spec.Volume.StorageOS != nil)
}
func (plugin *storageosPlugin) RequiresRemount(spec *volume.Spec) bool {
return false
}
func (plugin *storageosPlugin) GetAccessModes() []v1.PersistentVolumeAccessMode {
return []v1.PersistentVolumeAccessMode{
v1.ReadWriteOnce,
v1.ReadOnlyMany,
}
}
func (plugin *storageosPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, _ volume.VolumeOptions) (volume.Mounter, error) {
apiCfg, err := getAPICfg(spec, pod, plugin.host.GetKubeClient())
if err != nil {
return nil, err
}
return plugin.newMounterInternal(spec, pod, apiCfg, &storageosUtil{host: plugin.host}, plugin.host.GetMounter(plugin.GetPluginName()), plugin.host.GetExec(plugin.GetPluginName()))
}
func (plugin *storageosPlugin) newMounterInternal(spec *volume.Spec, pod *v1.Pod, apiCfg *storageosAPIConfig, manager storageosManager, mounter mount.Interface, exec utilexec.Interface) (volume.Mounter, error) {
volName, volNamespace, fsType, readOnly, err := getVolumeInfoFromSpec(spec)
if err != nil {
return nil, err
}
return &storageosMounter{
storageos: &storageos{
podUID: pod.UID,
podNamespace: pod.GetNamespace(),
pvName: spec.Name(),
volName: volName,
volNamespace: volNamespace,
fsType: fsType,
readOnly: readOnly,
apiCfg: apiCfg,
manager: manager,
mounter: mounter,
exec: exec,
plugin: plugin,
MetricsProvider: volume.NewMetricsStatFS(getPath(pod.UID, volNamespace, volName, spec.Name(), plugin.host)),
},
diskMounter: &mount.SafeFormatAndMount{Interface: mounter, Exec: exec},
mountOptions: util.MountOptionFromSpec(spec),
}, nil
}
func (plugin *storageosPlugin) NewUnmounter(pvName string, podUID types.UID) (volume.Unmounter, error) {
return plugin.newUnmounterInternal(pvName, podUID, &storageosUtil{host: plugin.host}, plugin.host.GetMounter(plugin.GetPluginName()), plugin.host.GetExec(plugin.GetPluginName()))
}
func (plugin *storageosPlugin) newUnmounterInternal(pvName string, podUID types.UID, manager storageosManager, mounter mount.Interface, exec utilexec.Interface) (volume.Unmounter, error) {
// Parse volume namespace & name from mountpoint if mounted
volNamespace, volName, err := getVolumeInfo(pvName, podUID, plugin.host)
if err != nil {
return nil, err
}
return &storageosUnmounter{
storageos: &storageos{
podUID: podUID,
pvName: pvName,
volName: volName,
volNamespace: volNamespace,
manager: manager,
mounter: mounter,
exec: exec,
plugin: plugin,
MetricsProvider: volume.NewMetricsStatFS(getPath(podUID, volNamespace, volName, pvName, plugin.host)),
},
}, nil
}
func (plugin *storageosPlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, error) {
if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.StorageOS == nil {
return nil, fmt.Errorf("spec.PersistentVolumeSource.StorageOS is nil")
}
class, err := util.GetClassForVolume(plugin.host.GetKubeClient(), spec.PersistentVolume)
if err != nil {
return nil, err
}
var adminSecretName, adminSecretNamespace string
for k, v := range class.Parameters {
switch strings.ToLower(k) {
case "adminsecretname":
adminSecretName = v
case "adminsecretnamespace":
adminSecretNamespace = v
}
}
apiCfg, err := parsePVSecret(adminSecretNamespace, adminSecretName, plugin.host.GetKubeClient())
if err != nil {
return nil, fmt.Errorf("failed to get admin secret from [%q/%q]: %v", adminSecretNamespace, adminSecretName, err)
}
return plugin.newDeleterInternal(spec, apiCfg, &storageosUtil{host: plugin.host})
}
func (plugin *storageosPlugin) newDeleterInternal(spec *volume.Spec, apiCfg *storageosAPIConfig, manager storageosManager) (volume.Deleter, error) {
return &storageosDeleter{
storageosMounter: &storageosMounter{
storageos: &storageos{
pvName: spec.Name(),
volName: spec.PersistentVolume.Spec.StorageOS.VolumeName,
volNamespace: spec.PersistentVolume.Spec.StorageOS.VolumeNamespace,
apiCfg: apiCfg,
manager: manager,
plugin: plugin,
},
},
pvUID: spec.PersistentVolume.UID,
}, nil
}
func (plugin *storageosPlugin) NewProvisioner(options volume.VolumeOptions) (volume.Provisioner, error) {
return plugin.newProvisionerInternal(options, &storageosUtil{host: plugin.host})
}
func (plugin *storageosPlugin) newProvisionerInternal(options volume.VolumeOptions, manager storageosManager) (volume.Provisioner, error) {
return &storageosProvisioner{
storageosMounter: &storageosMounter{
storageos: &storageos{
manager: manager,
plugin: plugin,
},
},
options: options,
}, nil
}
func (plugin *storageosPlugin) ConstructVolumeSpec(volumeName, mountPath string) (*volume.Spec, error) {
volNamespace, volName, err := getVolumeFromRef(volumeName)
if err != nil {
volNamespace = defaultNamespace
volName = volumeName
}
storageosVolume := &v1.Volume{
Name: volumeName,
VolumeSource: v1.VolumeSource{
StorageOS: &v1.StorageOSVolumeSource{
VolumeName: volName,
VolumeNamespace: volNamespace,
},
},
}
return volume.NewSpecFromVolume(storageosVolume), nil
}
func (plugin *storageosPlugin) SupportsMountOption() bool {
return true
}
func (plugin *storageosPlugin) SupportsBulkVolumeVerification() bool {
return false
}
func getVolumeSource(spec *volume.Spec) (*v1.StorageOSVolumeSource, bool, error) {
if spec.Volume != nil && spec.Volume.StorageOS != nil {
return spec.Volume.StorageOS, spec.Volume.StorageOS.ReadOnly, nil
}
return nil, false, fmt.Errorf("Spec does not reference a StorageOS volume type")
}
func getPersistentVolumeSource(spec *volume.Spec) (*v1.StorageOSPersistentVolumeSource, bool, error) {
if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.StorageOS != nil {
return spec.PersistentVolume.Spec.StorageOS, spec.ReadOnly, nil
}
return nil, false, fmt.Errorf("Spec does not reference a StorageOS persistent volume type")
}
// storageosManager is the abstract interface to StorageOS volume ops.
type storageosManager interface {
// Connects to the StorageOS API using the supplied configuration.
NewAPI(apiCfg *storageosAPIConfig) error
// Creates a StorageOS volume.
CreateVolume(provisioner *storageosProvisioner) (*storageosVolume, error)
// Attaches the disk to the kubelet's host machine.
AttachVolume(mounter *storageosMounter) (string, error)
// Attaches the device to the host at a mount path.
AttachDevice(mounter *storageosMounter, deviceMountPath string) error
// Detaches the disk from the kubelet's host machine.
DetachVolume(unmounter *storageosUnmounter, dir string) error
// Mounts the disk on the Kubelet's host machine.
MountVolume(mounter *storageosMounter, mnt, dir string) error
// Unmounts the disk from the Kubelet's host machine.
UnmountVolume(unounter *storageosUnmounter) error
// Deletes the storageos volume. All data will be lost.
DeleteVolume(deleter *storageosDeleter) error
// Gets the node's device path.
DeviceDir(mounter *storageosMounter) string
}
// storageos volumes represent a bare host directory mount of an StorageOS export.
type storageos struct {
podUID types.UID
podNamespace string
pvName string
volName string
volNamespace string
readOnly bool
description string
pool string
fsType string
sizeGB int
labels map[string]string
apiCfg *storageosAPIConfig
manager storageosManager
mounter mount.Interface
exec utilexec.Interface
plugin *storageosPlugin
volume.MetricsProvider
}
type storageosMounter struct {
*storageos
// The directory containing the StorageOS devices
deviceDir string
// Interface used to mount the file or block device
diskMounter *mount.SafeFormatAndMount
mountOptions []string
}
var _ volume.Mounter = &storageosMounter{}
func (b *storageosMounter) GetAttributes() volume.Attributes {
return volume.Attributes{
ReadOnly: b.readOnly,
Managed: !b.readOnly,
SELinuxRelabel: true,
}
}
// SetUp attaches the disk and bind mounts to the volume path.
func (b *storageosMounter) SetUp(mounterArgs volume.MounterArgs) error {
// Need a namespace to find the volume, try pod's namespace if not set.
if b.volNamespace == "" {
klog.V(2).Infof("Setting StorageOS volume namespace to pod namespace: %s", b.podNamespace)
b.volNamespace = b.podNamespace
}
targetPath := makeGlobalPDName(b.plugin.host, b.pvName, b.volNamespace, b.volName)
// Attach the device to the host.
if err := b.manager.AttachDevice(b, targetPath); err != nil {
klog.Errorf("Failed to attach device at %s: %s", targetPath, err.Error())
return err
}
// Attach the StorageOS volume as a block device
devicePath, err := b.manager.AttachVolume(b)
if err != nil {
klog.Errorf("Failed to attach StorageOS volume %s: %s", b.volName, err.Error())
return err
}
// Mount the loop device into the plugin's disk global mount dir.
err = b.manager.MountVolume(b, devicePath, targetPath)
if err != nil {
return err
}
klog.V(4).Infof("Successfully mounted StorageOS volume %s into global mount directory", b.volName)
// Bind mount the volume into the pod
return b.SetUpAt(b.GetPath(), mounterArgs)
}
// SetUp bind mounts the disk global mount to the give volume path.
func (b *storageosMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
notMnt, err := b.mounter.IsLikelyNotMountPoint(dir)
klog.V(4).Infof("StorageOS volume set up: %s %v %v", dir, !notMnt, err)
if err != nil && !os.IsNotExist(err) {
klog.Errorf("Cannot validate mount point: %s %v", dir, err)
return err
}
if !notMnt {
return nil
}
if err = os.MkdirAll(dir, 0750); err != nil {
klog.Errorf("mkdir failed on disk %s (%v)", dir, err)
return err
}
// Perform a bind mount to the full path to allow duplicate mounts of the same PD.
options := []string{"bind"}
if b.readOnly {
options = append(options, "ro")
}
mountOptions := util.JoinMountOptions(b.mountOptions, options)
globalPDPath := makeGlobalPDName(b.plugin.host, b.pvName, b.volNamespace, b.volName)
klog.V(4).Infof("Attempting to bind mount to pod volume at %s", dir)
err = b.mounter.MountSensitiveWithoutSystemd(globalPDPath, dir, "", mountOptions, nil)
if err != nil {
notMnt, mntErr := b.mounter.IsLikelyNotMountPoint(dir)
if mntErr != nil {
klog.Errorf("IsLikelyNotMountPoint check failed: %v", mntErr)
return err
}
if !notMnt {
if mntErr = b.mounter.Unmount(dir); mntErr != nil {
klog.Errorf("Failed to unmount: %v", mntErr)
return err
}
notMnt, mntErr := b.mounter.IsLikelyNotMountPoint(dir)
if mntErr != nil {
klog.Errorf("IsLikelyNotMountPoint check failed: %v", mntErr)
return err
}
if !notMnt {
klog.Errorf("%s is still mounted, despite call to unmount(). Will try again next sync loop.", dir)
return err
}
}
os.Remove(dir)
klog.Errorf("Mount of disk %s failed: %v", dir, err)
return err
}
if !b.readOnly {
volume.SetVolumeOwnership(b, mounterArgs.FsGroup, mounterArgs.FSGroupChangePolicy, util.FSGroupCompleteHook(b.plugin, nil))
}
klog.V(4).Infof("StorageOS volume setup complete on %s", dir)
return nil
}
func makeGlobalPDName(host volume.VolumeHost, pvName, volNamespace, volName string) string {
return filepath.Join(host.GetPluginDir(utilstrings.EscapeQualifiedName(storageosPluginName)), util.MountsInGlobalPDPath, pvName+"."+volNamespace+"."+volName)
}
// Given the pod id and PV name, finds the volume's namespace and name from the
// name or volume mount. We mount as volNamespace.pvName, but k8s will specify
// only the pvName to unmount.
// Will return empty volNamespace/pvName if the volume is not mounted.
func getVolumeInfo(pvName string, podUID types.UID, host volume.VolumeHost) (string, string, error) {
if volNamespace, volName, err := getVolumeFromRef(pvName); err == nil {
return volNamespace, volName, nil
}
volumeDir := filepath.Dir(host.GetPodVolumeDir(podUID, utilstrings.EscapeQualifiedName(storageosPluginName), pvName))
files, err := ioutil.ReadDir(volumeDir)
if err != nil {
return "", "", fmt.Errorf("could not read mounts from pod volume dir: %s", err)
}
for _, f := range files {
if f.Mode().IsDir() && strings.HasPrefix(f.Name(), pvName+".") {
if volNamespace, volName, err := getVolumeFromRef(f.Name()); err == nil {
return volNamespace, volName, nil
}
}
}
return "", "", fmt.Errorf("could not get info from unmounted pv %q at %q", pvName, volumeDir)
}
// Splits the volume ref on "." to return the volNamespace and pvName. Neither
// namespaces nor service names allow "." in their names.
func getVolumeFromRef(ref string) (volNamespace string, volName string, err error) {
refParts := strings.Split(ref, ".")
switch len(refParts) {
case 2:
return refParts[0], refParts[1], nil
case 3:
return refParts[1], refParts[2], nil
}
return "", "", fmt.Errorf("ref not in format volNamespace.volName or pvName.volNamespace.volName")
}
// GetPath returns the path to the user specific mount of a StorageOS volume
func (storageosVolume *storageos) GetPath() string {
return getPath(storageosVolume.podUID, storageosVolume.volNamespace, storageosVolume.volName, storageosVolume.pvName, storageosVolume.plugin.host)
}
type storageosUnmounter struct {
*storageos
}
var _ volume.Unmounter = &storageosUnmounter{}
func (b *storageosUnmounter) GetPath() string {
return getPath(b.podUID, b.volNamespace, b.volName, b.pvName, b.plugin.host)
}
// Unmounts the bind mount, and detaches the disk only if the PD
// resource was the last reference to that disk on the kubelet.
func (b *storageosUnmounter) TearDown() error {
if len(b.volNamespace) == 0 || len(b.volName) == 0 {
klog.Warningf("volNamespace: %q, volName: %q not set, skipping TearDown", b.volNamespace, b.volName)
return fmt.Errorf("pvName not specified for TearDown, waiting for next sync loop")
}
// Unmount from pod
mountPath := b.GetPath()
err := b.TearDownAt(mountPath)
if err != nil {
klog.Errorf("Unmount from pod failed: %v", err)
return err
}
// Find device name from global mount
globalPDPath := makeGlobalPDName(b.plugin.host, b.pvName, b.volNamespace, b.volName)
devicePath, _, err := mount.GetDeviceNameFromMount(b.mounter, globalPDPath)
if err != nil {
klog.Errorf("Detach failed when getting device from global mount: %v", err)
return err
}
// Unmount from plugin's disk global mount dir.
err = b.TearDownAt(globalPDPath)
if err != nil {
klog.Errorf("Detach failed during unmount: %v", err)
return err
}
// Detach loop device
err = b.manager.DetachVolume(b, devicePath)
if err != nil {
klog.Errorf("Detach device %s failed for volume %s: %v", devicePath, b.pvName, err)
return err
}
klog.V(4).Infof("Successfully unmounted StorageOS volume %s and detached devices", b.pvName)
return nil
}
// Unmounts the bind mount, and detaches the disk only if the PD
// resource was the last reference to that disk on the kubelet.
func (b *storageosUnmounter) TearDownAt(dir string) error {
if err := mount.CleanupMountPoint(dir, b.mounter, false); err != nil {
klog.V(4).Infof("Unmounted StorageOS volume %s failed with: %v", b.pvName, err)
}
if err := b.manager.UnmountVolume(b); err != nil {
klog.V(4).Infof("Mount reference for volume %s could not be removed from StorageOS: %v", b.pvName, err)
}
return nil
}
type storageosDeleter struct {
*storageosMounter
pvUID types.UID
}
var _ volume.Deleter = &storageosDeleter{}
func (d *storageosDeleter) GetPath() string {
return getPath(d.podUID, d.volNamespace, d.volName, d.pvName, d.plugin.host)
}
func (d *storageosDeleter) Delete() error {
return d.manager.DeleteVolume(d)
}
type storageosProvisioner struct {
*storageosMounter
options volume.VolumeOptions
}
var _ volume.Provisioner = &storageosProvisioner{}
func (c *storageosProvisioner) Provision(selectedNode *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (*v1.PersistentVolume, error) {
if !util.ContainsAllAccessModes(c.plugin.GetAccessModes(), c.options.PVC.Spec.AccessModes) {
return nil, fmt.Errorf("invalid AccessModes %v: only AccessModes %v are supported", c.options.PVC.Spec.AccessModes, c.plugin.GetAccessModes())
}
if util.CheckPersistentVolumeClaimModeBlock(c.options.PVC) {
return nil, fmt.Errorf("%s does not support block volume provisioning", c.plugin.GetPluginName())
}
var adminSecretName, adminSecretNamespace string
// Apply ProvisionerParameters (case-insensitive). We leave validation of
// the values to the cloud provider.
for k, v := range c.options.Parameters {
switch strings.ToLower(k) {
case "adminsecretname":
adminSecretName = v
case "adminsecretnamespace":
adminSecretNamespace = v
case "volumenamespace":
c.volNamespace = v
case "description":
c.description = v
case "pool":
c.pool = v
case "fstype":
c.fsType = v
default:
return nil, fmt.Errorf("invalid option %q for volume plugin %s", k, c.plugin.GetPluginName())
}
}
// Set from PVC
c.podNamespace = c.options.PVC.Namespace
c.volName = c.options.PVName
if c.volNamespace == "" {
c.volNamespace = c.options.PVC.Namespace
}
c.labels = make(map[string]string)
for k, v := range c.options.PVC.Labels {
c.labels[k] = v
}
capacity := c.options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
var err error
c.sizeGB, err = volumehelpers.RoundUpToGiBInt(capacity)
if err != nil {
return nil, err
}
apiCfg, err := parsePVSecret(adminSecretNamespace, adminSecretName, c.plugin.host.GetKubeClient())
if err != nil {
return nil, err
}
c.apiCfg = apiCfg
vol, err := c.manager.CreateVolume(c)
if err != nil {
klog.Errorf("failed to create volume: %v", err)
return nil, err
}
if vol.FSType == "" {
vol.FSType = defaultFSType
}
pv := &v1.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: vol.Name,
Labels: map[string]string{},
Annotations: map[string]string{
util.VolumeDynamicallyCreatedByKey: "storageos-dynamic-provisioner",
},
},
Spec: v1.PersistentVolumeSpec{
PersistentVolumeReclaimPolicy: c.options.PersistentVolumeReclaimPolicy,
AccessModes: c.options.PVC.Spec.AccessModes,
Capacity: v1.ResourceList{
v1.ResourceName(v1.ResourceStorage): resource.MustParse(fmt.Sprintf("%dGi", vol.SizeGB)),
},
PersistentVolumeSource: v1.PersistentVolumeSource{
StorageOS: &v1.StorageOSPersistentVolumeSource{
VolumeName: vol.Name,
VolumeNamespace: vol.Namespace,
FSType: vol.FSType,
ReadOnly: false,
SecretRef: &v1.ObjectReference{
Name: adminSecretName,
Namespace: adminSecretNamespace,
},
},
},
MountOptions: c.options.MountOptions,
},
}
if len(c.options.PVC.Spec.AccessModes) == 0 {
pv.Spec.AccessModes = c.plugin.GetAccessModes()
}
if len(vol.Labels) != 0 {
if pv.Labels == nil {
pv.Labels = make(map[string]string)
}
for k, v := range vol.Labels {
pv.Labels[k] = v
}
}
return pv, nil
}
// Returns StorageOS volume name, namespace, fstype and readonly from spec
func getVolumeInfoFromSpec(spec *volume.Spec) (string, string, string, bool, error) {
if spec.PersistentVolume != nil {
source, readOnly, err := getPersistentVolumeSource(spec)
if err != nil {
return "", "", "", false, err
}
return source.VolumeName, source.VolumeNamespace, source.FSType, readOnly, nil
}
if spec.Volume != nil {
source, readOnly, err := getVolumeSource(spec)
if err != nil {
return "", "", "", false, err
}
return source.VolumeName, source.VolumeNamespace, source.FSType, readOnly, nil
}
return "", "", "", false, fmt.Errorf("spec not Volume or PersistentVolume")
}
// Returns API config if secret set, otherwise empty struct so defaults can be
// attempted.
func getAPICfg(spec *volume.Spec, pod *v1.Pod, kubeClient clientset.Interface) (*storageosAPIConfig, error) {
if spec.PersistentVolume != nil {
source, _, err := getPersistentVolumeSource(spec)
if err != nil {
return nil, err
}
if source.SecretRef == nil {
return nil, nil
}
return parsePVSecret(source.SecretRef.Namespace, source.SecretRef.Name, kubeClient)
}
if spec.Volume != nil {
source, _, err := getVolumeSource(spec)
if err != nil {
return nil, err
}
if source.SecretRef == nil {
return nil, nil
}
return parsePodSecret(pod, source.SecretRef.Name, kubeClient)
}
return nil, fmt.Errorf("spec not Volume or PersistentVolume")
}
func parsePodSecret(pod *v1.Pod, secretName string, kubeClient clientset.Interface) (*storageosAPIConfig, error) {
secret, err := util.GetSecretForPod(pod, secretName, kubeClient)
if err != nil {
klog.Errorf("failed to get secret from [%q/%q]", pod.Namespace, secretName)
return nil, fmt.Errorf("failed to get secret from [%q/%q]", pod.Namespace, secretName)
}
return parseAPIConfig(secret)
}
// Important: Only to be called with data from a PV to avoid secrets being
// loaded from a user-suppler namespace.
func parsePVSecret(namespace, secretName string, kubeClient clientset.Interface) (*storageosAPIConfig, error) {
secret, err := util.GetSecretForPV(namespace, secretName, storageosPluginName, kubeClient)
if err != nil {
klog.Errorf("failed to get secret from [%q/%q]", namespace, secretName)
return nil, fmt.Errorf("failed to get secret from [%q/%q]", namespace, secretName)
}
return parseAPIConfig(secret)
}
// Parse API configuration from parameters or secret
func parseAPIConfig(params map[string]string) (*storageosAPIConfig, error) {
if len(params) == 0 {
return nil, fmt.Errorf("empty API config")
}
c := &storageosAPIConfig{}
for name, data := range params {
switch strings.ToLower(name) {
case "apiaddress":
c.apiAddr = string(data)
case "apiusername":
c.apiUser = string(data)
case "apipassword":
c.apiPass = string(data)
case "apiversion":
c.apiVersion = string(data)
}
}
return c, nil
}

View File

@ -1,384 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
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 storageos
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"k8s.io/mount-utils"
"k8s.io/utils/exec/testing"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/fake"
utiltesting "k8s.io/client-go/util/testing"
"k8s.io/kubernetes/pkg/volume"
volumetest "k8s.io/kubernetes/pkg/volume/testing"
)
func TestCanSupport(t *testing.T) {
tmpDir, err := utiltesting.MkTmpdir("storageos_test")
if err != nil {
t.Fatalf("error creating temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil))
plug, err := plugMgr.FindPluginByName("kubernetes.io/storageos")
if err != nil {
t.Fatal("Can't find the plugin by name")
}
if plug.GetPluginName() != "kubernetes.io/storageos" {
t.Errorf("Wrong name: %s", plug.GetPluginName())
}
if !plug.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{StorageOS: &v1.StorageOSVolumeSource{}}}}) {
t.Errorf("Expected true")
}
if !plug.CanSupport(&volume.Spec{PersistentVolume: &v1.PersistentVolume{Spec: v1.PersistentVolumeSpec{PersistentVolumeSource: v1.PersistentVolumeSource{StorageOS: &v1.StorageOSPersistentVolumeSource{}}}}}) {
t.Errorf("Expected true")
}
}
func TestGetAccessModes(t *testing.T) {
tmpDir, err := utiltesting.MkTmpdir("storageos_test")
if err != nil {
t.Fatalf("error creating temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil))
plug, err := plugMgr.FindPersistentPluginByName("kubernetes.io/storageos")
if err != nil {
t.Errorf("Can't find the plugin by name")
}
if !volumetest.ContainsAccessMode(plug.GetAccessModes(), v1.ReadWriteOnce) || !volumetest.ContainsAccessMode(plug.GetAccessModes(), v1.ReadOnlyMany) {
t.Errorf("Expected two AccessModeTypes: %s and %s", v1.ReadWriteOnce, v1.ReadOnlyMany)
}
}
type fakePDManager struct {
api apiImplementer
attachCalled bool
attachDeviceCalled bool
detachCalled bool
mountCalled bool
unmountCalled bool
createCalled bool
deleteCalled bool
}
func (fake *fakePDManager) NewAPI(apiCfg *storageosAPIConfig) error {
fake.api = fakeAPI{}
return nil
}
func (fake *fakePDManager) CreateVolume(p *storageosProvisioner) (*storageosVolume, error) {
fake.createCalled = true
labels := make(map[string]string)
labels["fakepdmanager"] = "yes"
return &storageosVolume{
Name: "test-storageos-name",
Namespace: "test-storageos-namespace",
Pool: "test-storageos-pool",
SizeGB: 100,
Labels: labels,
FSType: "ext2",
}, nil
}
func (fake *fakePDManager) AttachVolume(b *storageosMounter) (string, error) {
fake.attachCalled = true
return "", nil
}
func (fake *fakePDManager) AttachDevice(b *storageosMounter, dir string) error {
fake.attachDeviceCalled = true
return nil
}
func (fake *fakePDManager) DetachVolume(b *storageosUnmounter, loopDevice string) error {
fake.detachCalled = true
return nil
}
func (fake *fakePDManager) MountVolume(b *storageosMounter, mntDevice, deviceMountPath string) error {
fake.mountCalled = true
return nil
}
func (fake *fakePDManager) UnmountVolume(b *storageosUnmounter) error {
fake.unmountCalled = true
return nil
}
func (fake *fakePDManager) DeleteVolume(d *storageosDeleter) error {
fake.deleteCalled = true
if d.volName != "test-storageos-name" {
return fmt.Errorf("Deleter got unexpected volume name: %s", d.volName)
}
return nil
}
func (fake *fakePDManager) DeviceDir(mounter *storageosMounter) string {
return defaultDeviceDir
}
func TestPlugin(t *testing.T) {
tmpDir, err := utiltesting.MkTmpdir("storageos_test")
if err != nil {
t.Fatalf("can't make a temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil))
plug, err := plugMgr.FindPluginByName("kubernetes.io/storageos")
if err != nil {
t.Errorf("Can't find the plugin by name")
}
secretName := "very-secret"
spec := &v1.Volume{
Name: "vol1-pvname",
VolumeSource: v1.VolumeSource{
StorageOS: &v1.StorageOSVolumeSource{
VolumeName: "vol1",
VolumeNamespace: "ns1",
FSType: "ext3",
SecretRef: &v1.LocalObjectReference{
Name: secretName,
},
},
},
}
client := fake.NewSimpleClientset()
client.CoreV1().Secrets("default").Create(context.TODO(), &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: "default",
},
Type: "kubernetes.io/storageos",
Data: map[string][]byte{
"apiUsername": []byte("storageos"),
"apiPassword": []byte("storageos"),
"apiAddr": []byte("tcp://localhost:5705"),
}}, metav1.CreateOptions{})
plug.(*storageosPlugin).host = volumetest.NewFakeVolumeHost(t, tmpDir, client, nil)
// Test Mounter
pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid"), Namespace: "default"}}
fakeManager := &fakePDManager{}
apiCfg, err := parsePodSecret(pod, secretName, plug.(*storageosPlugin).host.GetKubeClient())
if err != nil {
t.Errorf("Couldn't get secret from %v/%v", pod.Namespace, secretName)
}
mounter, err := plug.(*storageosPlugin).newMounterInternal(volume.NewSpecFromVolume(spec), pod, apiCfg, fakeManager, mount.NewFakeMounter(nil), &testingexec.FakeExec{})
if err != nil {
t.Fatalf("Failed to make a new Mounter: %v", err)
}
if mounter == nil {
t.Fatalf("Got a nil Mounter")
}
expectedPath := filepath.Join(tmpDir, "pods/poduid/volumes/kubernetes.io~storageos/vol1-pvname.ns1.vol1")
volPath := mounter.GetPath()
if volPath != expectedPath {
t.Errorf("Expected path: '%s' got: '%s'", expectedPath, volPath)
}
if err := mounter.SetUp(volume.MounterArgs{}); err != nil {
t.Errorf("Expected success, got: %v", err)
}
if _, err := os.Stat(volPath); err != nil {
if os.IsNotExist(err) {
t.Errorf("SetUp() failed, volume path not created: %s", volPath)
} else {
t.Errorf("SetUp() failed: %v", err)
}
}
if !fakeManager.attachDeviceCalled {
t.Errorf("AttachDevice not called")
}
if !fakeManager.attachCalled {
t.Errorf("Attach not called")
}
if !fakeManager.mountCalled {
t.Errorf("Mount not called")
}
// Test Unmounter
fakeManager = &fakePDManager{}
unmounter, err := plug.(*storageosPlugin).newUnmounterInternal("vol1-pvname", types.UID("poduid"), fakeManager, mount.NewFakeMounter(nil), &testingexec.FakeExec{})
if err != nil {
t.Errorf("Failed to make a new Unmounter: %v", err)
}
if unmounter == nil {
t.Errorf("Got a nil Unmounter")
}
volPath = unmounter.GetPath()
if volPath != expectedPath {
t.Errorf("Expected path: '%s' got: '%s'", expectedPath, volPath)
}
if err := unmounter.TearDown(); err != nil {
t.Errorf("Expected success, got: %v", err)
}
if _, err := os.Stat(volPath); err == nil {
t.Errorf("TearDown() failed, volume path still exists: %s", volPath)
} else if !os.IsNotExist(err) {
t.Errorf("TearDown() failed: %v", err)
}
if !fakeManager.unmountCalled {
t.Errorf("Unmount not called")
}
if !fakeManager.detachCalled {
t.Errorf("Detach not called")
}
// Test Provisioner
fakeManager = &fakePDManager{}
mountOptions := []string{"sync", "noatime"}
options := volume.VolumeOptions{
PVC: volumetest.CreateTestPVC("100Mi", []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}),
// PVName: "test-volume-name",
PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete,
Parameters: map[string]string{
"VolumeNamespace": "test-volume-namespace",
"adminSecretName": secretName,
"adminsecretnamespace": "default",
},
MountOptions: mountOptions,
}
provisioner, err := plug.(*storageosPlugin).newProvisionerInternal(options, fakeManager)
if err != nil {
t.Errorf("newProvisionerInternal() failed: %v", err)
}
persistentSpec, err := provisioner.Provision(nil, nil)
if err != nil {
t.Fatalf("Provision() failed: %v", err)
}
if persistentSpec.Spec.PersistentVolumeSource.StorageOS.VolumeName != "test-storageos-name" {
t.Errorf("Provision() returned unexpected volume Name: %s, expected test-storageos-name", persistentSpec.Spec.PersistentVolumeSource.StorageOS.VolumeName)
}
if persistentSpec.Spec.PersistentVolumeSource.StorageOS.VolumeNamespace != "test-storageos-namespace" {
t.Errorf("Provision() returned unexpected volume Namespace: %s", persistentSpec.Spec.PersistentVolumeSource.StorageOS.VolumeNamespace)
}
cap := persistentSpec.Spec.Capacity[v1.ResourceStorage]
size := cap.Value()
if size != 100*1024*1024*1024 {
t.Errorf("Provision() returned unexpected volume size: %v", size)
}
if persistentSpec.Spec.PersistentVolumeSource.StorageOS.FSType != "ext2" {
t.Errorf("Provision() returned unexpected volume FSType: %s", persistentSpec.Spec.PersistentVolumeSource.StorageOS.FSType)
}
if len(persistentSpec.Spec.MountOptions) != 2 {
t.Errorf("Provision() returned unexpected volume mount options: %v", persistentSpec.Spec.MountOptions)
}
if persistentSpec.Labels["fakepdmanager"] != "yes" {
t.Errorf("Provision() returned unexpected labels: %v", persistentSpec.Labels)
}
if !fakeManager.createCalled {
t.Errorf("Create not called")
}
// Test Deleter
fakeManager = &fakePDManager{}
volSpec := &volume.Spec{
PersistentVolume: persistentSpec,
}
deleter, err := plug.(*storageosPlugin).newDeleterInternal(volSpec, apiCfg, fakeManager)
if err != nil {
t.Errorf("newDeleterInternal() failed: %v", err)
}
err = deleter.Delete()
if err != nil {
t.Errorf("Deleter() failed: %v", err)
}
if !fakeManager.deleteCalled {
t.Errorf("Delete not called")
}
}
func TestPersistentClaimReadOnlyFlag(t *testing.T) {
tmpDir, err := utiltesting.MkTmpdir("storageos_test")
if err != nil {
t.Fatalf("error creating temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
pv := &v1.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "pvA",
},
Spec: v1.PersistentVolumeSpec{
PersistentVolumeSource: v1.PersistentVolumeSource{
StorageOS: &v1.StorageOSPersistentVolumeSource{VolumeName: "pvA", VolumeNamespace: "vnsA", ReadOnly: false},
},
ClaimRef: &v1.ObjectReference{
Name: "claimA",
},
},
}
claim := &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "claimA",
Namespace: "nsA",
},
Spec: v1.PersistentVolumeClaimSpec{
VolumeName: "pvA",
},
Status: v1.PersistentVolumeClaimStatus{
Phase: v1.ClaimBound,
},
}
client := fake.NewSimpleClientset(pv, claim)
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, client, nil))
plug, _ := plugMgr.FindPluginByName(storageosPluginName)
// readOnly bool is supplied by persistent-claim volume source when its mounter creates other volumes
spec := volume.NewSpecFromPersistentVolume(pv, true)
pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "nsA", UID: types.UID("poduid")}}
fakeManager := &fakePDManager{}
apiCfg := GetAPIConfig()
mounter, err := plug.(*storageosPlugin).newMounterInternal(spec, pod, apiCfg, fakeManager, mount.NewFakeMounter(nil), &testingexec.FakeExec{})
if err != nil {
t.Fatalf("error creating a new internal mounter:%v", err)
}
if !mounter.GetAttributes().ReadOnly {
t.Errorf("Expected true for mounter.IsReadOnly")
}
}

View File

@ -1,436 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
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 storageos
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
storageosapi "github.com/storageos/go-api"
storageostypes "github.com/storageos/go-api/types"
"k8s.io/klog/v2"
proxyutil "k8s.io/kubernetes/pkg/proxy/util"
"k8s.io/kubernetes/pkg/volume"
utilexec "k8s.io/utils/exec"
)
const (
losetupPath = "losetup"
modeBlock deviceType = iota
modeFile
modeUnsupported
//ErrDeviceNotFound defines "device not found"
ErrDeviceNotFound = "device not found"
//ErrDeviceNotSupported defines "device not supported"
ErrDeviceNotSupported = "device not supported"
//ErrNotAvailable defines "not available"
ErrNotAvailable = "not available"
)
type deviceType int
// storageosVolume describes a provisioned volume
type storageosVolume struct {
ID string
Name string
Namespace string
Description string
Pool string
SizeGB int
Labels map[string]string
FSType string
}
type storageosAPIConfig struct {
apiAddr string
apiUser string
apiPass string
apiVersion string
}
type apiImplementer interface {
Volume(namespace string, ref string) (*storageostypes.Volume, error)
VolumeCreate(opts storageostypes.VolumeCreateOptions) (*storageostypes.Volume, error)
VolumeMount(opts storageostypes.VolumeMountOptions) error
VolumeUnmount(opts storageostypes.VolumeUnmountOptions) error
VolumeDelete(opt storageostypes.DeleteOptions) error
Node(ref string) (*storageostypes.Node, error)
}
// storageosUtil is the utility structure to interact with the StorageOS API.
type storageosUtil struct {
api apiImplementer
host volume.VolumeHost
}
func (u *storageosUtil) NewAPI(apiCfg *storageosAPIConfig) error {
if u.api != nil {
return nil
}
if u.host == nil {
return errors.New("host must not be nil")
}
if apiCfg == nil {
apiCfg = &storageosAPIConfig{
apiAddr: defaultAPIAddress,
apiUser: defaultAPIUser,
apiPass: defaultAPIPassword,
apiVersion: defaultAPIVersion,
}
klog.V(4).Infof("using default StorageOS API settings: addr %s, version: %s", apiCfg.apiAddr, defaultAPIVersion)
}
api, err := storageosapi.NewVersionedClient(apiCfg.apiAddr, defaultAPIVersion)
if err != nil {
return err
}
api.SetAuth(apiCfg.apiUser, apiCfg.apiPass)
if err := api.SetDialContext(proxyutil.NewFilteredDialContext(api.GetDialContext(), nil, u.host.GetFilteredDialOptions())); err != nil {
return fmt.Errorf("failed to set DialContext in storageos client: %v", err)
}
u.api = api
return nil
}
// Creates a new StorageOS volume and makes it available as a device within
// /var/lib/storageos/volumes.
func (u *storageosUtil) CreateVolume(p *storageosProvisioner) (*storageosVolume, error) {
klog.V(4).Infof("creating StorageOS volume %q with namespace %q", p.volName, p.volNamespace)
if err := u.NewAPI(p.apiCfg); err != nil {
return nil, err
}
if p.labels == nil {
p.labels = make(map[string]string)
}
opts := storageostypes.VolumeCreateOptions{
Name: p.volName,
Size: p.sizeGB,
Description: p.description,
Pool: p.pool,
FSType: p.fsType,
Namespace: p.volNamespace,
Labels: p.labels,
}
vol, err := u.api.VolumeCreate(opts)
if err != nil {
// don't log error details from client calls in events
klog.V(4).Infof("volume create failed for volume %q (%v)", opts.Name, err)
return nil, errors.New("volume create failed: see kube-controller-manager.log for details")
}
return &storageosVolume{
ID: vol.ID,
Name: vol.Name,
Namespace: vol.Namespace,
Description: vol.Description,
Pool: vol.Pool,
FSType: vol.FSType,
SizeGB: int(vol.Size),
Labels: vol.Labels,
}, nil
}
// Attach exposes a volume on the host as a block device. StorageOS uses a
// global namespace, so if the volume exists, it should already be available as
// a device within `/var/lib/storageos/volumes/<id>`.
//
// Depending on the host capabilities, the device may be either a block device
// or a file device. Block devices can be used directly, but file devices must
// be made accessible as a block device before using.
func (u *storageosUtil) AttachVolume(b *storageosMounter) (string, error) {
klog.V(4).Infof("attaching StorageOS volume %q with namespace %q", b.volName, b.volNamespace)
if err := u.NewAPI(b.apiCfg); err != nil {
return "", err
}
// Get the node's device path from the API, falling back to the default if
// not set on the node.
if b.deviceDir == "" {
b.deviceDir = u.DeviceDir(b)
}
vol, err := u.api.Volume(b.volNamespace, b.volName)
if err != nil {
klog.Warningf("volume retrieve failed for volume %q with namespace %q (%v)", b.volName, b.volNamespace, err)
return "", err
}
srcPath := filepath.Join(b.deviceDir, vol.ID)
dt, err := pathDeviceType(srcPath)
if err != nil {
klog.Warningf("volume source path %q for volume %q not ready (%v)", srcPath, b.volName, err)
return "", err
}
switch dt {
case modeBlock:
return srcPath, nil
case modeFile:
return attachFileDevice(srcPath, b.exec)
default:
return "", fmt.Errorf(ErrDeviceNotSupported)
}
}
// Detach detaches a volume from the host. This is only needed when NBD is not
// enabled and loop devices are used to simulate a block device.
func (u *storageosUtil) DetachVolume(b *storageosUnmounter, devicePath string) error {
klog.V(4).Infof("detaching StorageOS volume %q with namespace %q", b.volName, b.volNamespace)
if !isLoopDevice(devicePath) {
return nil
}
if _, err := os.Stat(devicePath); os.IsNotExist(err) {
return nil
}
return removeLoopDevice(devicePath, b.exec)
}
// AttachDevice attaches the volume device to the host at a given mount path.
func (u *storageosUtil) AttachDevice(b *storageosMounter, deviceMountPath string) error {
klog.V(4).Infof("attaching StorageOS device for volume %q with namespace %q", b.volName, b.volNamespace)
if err := u.NewAPI(b.apiCfg); err != nil {
return err
}
opts := storageostypes.VolumeMountOptions{
Name: b.volName,
Namespace: b.volNamespace,
FsType: b.fsType,
Mountpoint: deviceMountPath,
Client: b.plugin.host.GetHostName(),
}
if err := u.api.VolumeMount(opts); err != nil {
return err
}
return nil
}
// Mount mounts the volume on the host.
func (u *storageosUtil) MountVolume(b *storageosMounter, mntDevice, deviceMountPath string) error {
klog.V(4).Infof("mounting StorageOS volume %q with namespace %q", b.volName, b.volNamespace)
notMnt, err := b.mounter.IsLikelyNotMountPoint(deviceMountPath)
if err != nil {
if os.IsNotExist(err) {
if err = os.MkdirAll(deviceMountPath, 0750); err != nil {
return err
}
notMnt = true
} else {
return err
}
}
if err = os.MkdirAll(deviceMountPath, 0750); err != nil {
klog.Errorf("mkdir failed on disk %s (%v)", deviceMountPath, err)
return err
}
options := []string{}
if b.readOnly {
options = append(options, "ro")
}
if notMnt {
err = b.diskMounter.FormatAndMount(mntDevice, deviceMountPath, b.fsType, options)
if err != nil {
os.Remove(deviceMountPath)
return err
}
}
return err
}
// Unmount removes the mount reference from the volume allowing it to be
// re-mounted elsewhere.
func (u *storageosUtil) UnmountVolume(b *storageosUnmounter) error {
klog.V(4).Infof("clearing StorageOS mount reference for volume %q with namespace %q", b.volName, b.volNamespace)
if err := u.NewAPI(b.apiCfg); err != nil {
// We can't always get the config we need, so allow the unmount to
// succeed even if we can't remove the mount reference from the API.
klog.Warningf("could not remove mount reference in the StorageOS API as no credentials available to the unmount operation")
return nil
}
opts := storageostypes.VolumeUnmountOptions{
Name: b.volName,
Namespace: b.volNamespace,
Client: b.plugin.host.GetHostName(),
}
return u.api.VolumeUnmount(opts)
}
// Deletes a StorageOS volume. Assumes it has already been unmounted and detached.
func (u *storageosUtil) DeleteVolume(d *storageosDeleter) error {
if err := u.NewAPI(d.apiCfg); err != nil {
return err
}
// Deletes must be forced as the StorageOS API will not normally delete
// volumes that it thinks are mounted. We can't be sure the unmount was
// registered via the API so we trust k8s to only delete volumes it knows
// are unmounted.
opts := storageostypes.DeleteOptions{
Name: d.volName,
Namespace: d.volNamespace,
Force: true,
}
if err := u.api.VolumeDelete(opts); err != nil {
// don't log error details from client calls in events
klog.V(4).Infof("volume deleted failed for volume %q in namespace %q: %v", d.volName, d.volNamespace, err)
return errors.New("volume delete failed: see kube-controller-manager.log for details")
}
return nil
}
// Get the node's device path from the API, falling back to the default if not
// specified.
func (u *storageosUtil) DeviceDir(b *storageosMounter) string {
ctrl, err := u.api.Node(b.plugin.host.GetHostName())
if err != nil {
klog.Warningf("node device path lookup failed: %v", err)
return defaultDeviceDir
}
if ctrl == nil || ctrl.DeviceDir == "" {
klog.Warningf("node device path not set, using default: %s", defaultDeviceDir)
return defaultDeviceDir
}
return ctrl.DeviceDir
}
// pathMode returns the FileMode for a path.
func pathDeviceType(path string) (deviceType, error) {
fi, err := os.Stat(path)
if err != nil {
return modeUnsupported, err
}
switch mode := fi.Mode(); {
case mode&os.ModeDevice != 0:
return modeBlock, nil
case mode.IsRegular():
return modeFile, nil
default:
return modeUnsupported, nil
}
}
// attachFileDevice takes a path to a regular file and makes it available as an
// attached block device.
func attachFileDevice(path string, exec utilexec.Interface) (string, error) {
blockDevicePath, err := getLoopDevice(path)
if err != nil && err.Error() != ErrDeviceNotFound {
return "", err
}
// If no existing loop device for the path, create one
if blockDevicePath == "" {
klog.V(4).Infof("Creating device for path: %s", path)
blockDevicePath, err = makeLoopDevice(path, exec)
if err != nil {
return "", err
}
}
return blockDevicePath, nil
}
// Returns the full path to the loop device associated with the given path.
func getLoopDevice(path string) (string, error) {
_, err := os.Stat(path)
if os.IsNotExist(err) {
return "", errors.New(ErrNotAvailable)
}
if err != nil {
return "", fmt.Errorf("not attachable: %v", err)
}
return getLoopDeviceFromSysfs(path)
}
func makeLoopDevice(path string, exec utilexec.Interface) (string, error) {
args := []string{"-f", "-P", path}
out, err := exec.Command(losetupPath, args...).CombinedOutput()
if err != nil {
klog.V(2).Infof("Failed device create command for path %s: %v %s", path, err, out)
return "", err
}
return getLoopDeviceFromSysfs(path)
}
func removeLoopDevice(device string, exec utilexec.Interface) error {
args := []string{"-d", device}
out, err := exec.Command(losetupPath, args...).CombinedOutput()
if err != nil {
if !strings.Contains(string(out), "No such device or address") {
return err
}
}
return nil
}
func isLoopDevice(device string) bool {
return strings.HasPrefix(device, "/dev/loop")
}
// getLoopDeviceFromSysfs finds the backing file for a loop
// device from sysfs via "/sys/block/loop*/loop/backing_file".
func getLoopDeviceFromSysfs(path string) (string, error) {
// If the file is a symlink.
realPath, err := filepath.EvalSymlinks(path)
if err != nil {
return "", errors.New(ErrDeviceNotFound)
}
devices, err := filepath.Glob("/sys/block/loop*")
if err != nil {
return "", errors.New(ErrDeviceNotFound)
}
for _, device := range devices {
backingFile := fmt.Sprintf("%s/loop/backing_file", device)
// The contents of this file is the absolute path of "path".
data, err := ioutil.ReadFile(backingFile)
if err != nil {
continue
}
// Return the first match.
backingFilePath := strings.TrimSpace(string(data))
if backingFilePath == path || backingFilePath == realPath {
return fmt.Sprintf("/dev/%s", filepath.Base(device)), nil
}
}
return "", errors.New(ErrDeviceNotFound)
}

View File

@ -1,225 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
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 storageos
import (
"fmt"
"os"
"testing"
storageostypes "github.com/storageos/go-api/types"
"k8s.io/mount-utils"
v1 "k8s.io/api/core/v1"
utiltesting "k8s.io/client-go/util/testing"
"k8s.io/kubernetes/pkg/volume"
volumetest "k8s.io/kubernetes/pkg/volume/testing"
)
var testVolName = "storageos-test-vol"
var testPVName = "storageos-test-pv"
var testNamespace = "storageos-test-namespace"
var testSize = 1
var testDesc = "testdescription"
var testPool = "testpool"
var testFSType = "ext2"
var testVolUUID = "01c43d34-89f8-83d3-422b-43536a0f25e6"
func GetAPIConfig() *storageosAPIConfig {
return &storageosAPIConfig{
apiAddr: "http://5.6.7.8:9999",
apiUser: "abc",
apiPass: "123",
apiVersion: "10",
}
}
func TestClient(t *testing.T) {
tmpDir, err := utiltesting.MkTmpdir("storageos_test")
if err != nil {
t.Fatalf("error creating tmpdir: %v", err)
}
util := storageosUtil{host: volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil)}
err = util.NewAPI(GetAPIConfig())
if err != nil {
t.Fatalf("error getting api config: %v", err)
}
if util.api == nil {
t.Errorf("client() unexpectedly returned nil")
}
}
type fakeAPI struct{}
func (f fakeAPI) Volume(namespace string, ref string) (*storageostypes.Volume, error) {
if namespace == testNamespace && ref == testVolName {
return &storageostypes.Volume{
ID: "01c43d34-89f8-83d3-422b-43536a0f25e6",
Name: ref,
Pool: "default",
Namespace: namespace,
Size: 5,
}, nil
}
return nil, fmt.Errorf("not found")
}
func (f fakeAPI) VolumeCreate(opts storageostypes.VolumeCreateOptions) (*storageostypes.Volume, error) {
// Append a label from the api
labels := opts.Labels
labels["labelfromapi"] = "apilabel"
return &storageostypes.Volume{
ID: testVolUUID,
Name: opts.Name,
Namespace: opts.Namespace,
Description: opts.Description,
Pool: opts.Pool,
Size: opts.Size,
FSType: opts.FSType,
Labels: labels,
}, nil
}
func (f fakeAPI) VolumeMount(opts storageostypes.VolumeMountOptions) error {
return nil
}
func (f fakeAPI) VolumeUnmount(opts storageostypes.VolumeUnmountOptions) error {
return nil
}
func (f fakeAPI) VolumeDelete(opts storageostypes.DeleteOptions) error {
return nil
}
func (f fakeAPI) Node(ref string) (*storageostypes.Node, error) {
return &storageostypes.Node{}, nil
}
func TestCreateVolume(t *testing.T) {
tmpDir, err := utiltesting.MkTmpdir("storageos_test")
if err != nil {
t.Fatalf("can't make a temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil))
plug, _ := plugMgr.FindPluginByName("kubernetes.io/storageos")
// Use real util with stubbed api
util := &storageosUtil{}
util.api = fakeAPI{}
labels := map[string]string{
"labelA": "valueA",
"labelB": "valueB",
}
options := volume.VolumeOptions{
PVName: testPVName,
PVC: volumetest.CreateTestPVC(fmt.Sprintf("%dGi", testSize), []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}),
PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete,
}
provisioner := &storageosProvisioner{
storageosMounter: &storageosMounter{
storageos: &storageos{
pvName: testPVName,
volName: testVolName,
volNamespace: testNamespace,
sizeGB: testSize,
pool: testPool,
description: testDesc,
fsType: testFSType,
labels: labels,
manager: util,
plugin: plug.(*storageosPlugin),
},
},
options: options,
}
vol, err := util.CreateVolume(provisioner)
if err != nil {
t.Errorf("CreateVolume() returned error: %v", err)
}
if vol == nil {
t.Fatalf("CreateVolume() vol is empty")
}
if vol.ID == "" {
t.Error("CreateVolume() vol ID is empty")
}
if vol.Name != testVolName {
t.Errorf("CreateVolume() returned unexpected Name %s", vol.Name)
}
if vol.Namespace != testNamespace {
t.Errorf("CreateVolume() returned unexpected Namespace %s", vol.Namespace)
}
if vol.Pool != testPool {
t.Errorf("CreateVolume() returned unexpected Pool %s", vol.Pool)
}
if vol.FSType != testFSType {
t.Errorf("CreateVolume() returned unexpected FSType %s", vol.FSType)
}
if vol.SizeGB != testSize {
t.Errorf("CreateVolume() returned unexpected Size %d", vol.SizeGB)
}
if len(vol.Labels) == 0 {
t.Error("CreateVolume() Labels are empty")
} else {
var val string
var ok bool
for k, v := range labels {
if val, ok = vol.Labels[k]; !ok {
t.Errorf("CreateVolume() Label %s not set", k)
}
if val != v {
t.Errorf("CreateVolume() returned unexpected Label value %s", val)
}
}
if val, ok = vol.Labels["labelfromapi"]; !ok {
t.Error("CreateVolume() Label from api not set")
}
if val != "apilabel" {
t.Errorf("CreateVolume() returned unexpected Label value %s", val)
}
}
}
func TestAttachVolume(t *testing.T) {
tmpDir, err := utiltesting.MkTmpdir("storageos_test")
if err != nil {
t.Fatalf("can't make a temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil))
plug, _ := plugMgr.FindPluginByName("kubernetes.io/storageos")
// Use real util with stubbed api
util := &storageosUtil{}
util.api = fakeAPI{}
_ = &storageosMounter{
storageos: &storageos{
volName: testVolName,
volNamespace: testNamespace,
manager: util,
mounter: mount.NewFakeMounter(nil),
plugin: plug.(*storageosPlugin),
},
deviceDir: tmpDir,
}
}

View File

@ -1,24 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

View File

@ -1,45 +0,0 @@
MIT License
Copyright (c) 2015-2018 StorageOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Copyright (c) 2013-2017, go-dockerclient authors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,5 +0,0 @@
# StorageOS API client library
## Swagger Spec
Swagger specification for this repo is available in the [StorageOS public documentation](https://github.com/storageos/storageos.github.io/blob/master/swagger.yaml).

View File

@ -1,612 +0,0 @@
package storageos
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/storageos/go-api/netutil"
"github.com/storageos/go-api/serror"
)
const (
// DefaultUserAgent is the default User-Agent header to include in HTTP requests.
DefaultUserAgent = "go-storageosclient"
// DefaultVersionStr is the string value of the default API version.
DefaultVersionStr = "1"
// DefaultVersion is the default API version.
DefaultVersion = 1
)
var (
// ErrConnectionRefused is returned when the client cannot connect to the given endpoint.
ErrConnectionRefused = errors.New("cannot connect to StorageOS API endpoint")
// ErrInactivityTimeout is returned when a streamable call has been inactive for some time.
ErrInactivityTimeout = errors.New("inactivity time exceeded timeout")
// ErrInvalidVersion is returned when a versioned client was requested but no version specified.
ErrInvalidVersion = errors.New("invalid version")
// ErrProxyNotSupported is returned when a client is unable to set a proxy for http requests.
ErrProxyNotSupported = errors.New("client does not support http proxy")
// ErrDialerNotSupported is returned when a client is unable to set a DialContext for http requests.
ErrDialerNotSupported = errors.New("client does not support setting DialContext")
// DefaultPort is the default API port.
DefaultPort = "5705"
// DataplaneHealthPort is the the port used by the dataplane health-check service.
DataplaneHealthPort = "5704"
// DefaultHost is the default API host.
DefaultHost = "http://localhost:" + DefaultPort
)
// APIVersion is an internal representation of a version of the Remote API.
type APIVersion int
// NewAPIVersion returns an instance of APIVersion for the given string.
//
// The given string must be in the form <major>
func NewAPIVersion(input string) (APIVersion, error) {
if input == "" {
return DefaultVersion, ErrInvalidVersion
}
version, err := strconv.Atoi(input)
if err != nil {
return 0, fmt.Errorf("Unable to parse version %q", input)
}
return APIVersion(version), nil
}
func (version APIVersion) String() string {
return fmt.Sprintf("v%d", version)
}
// Client is the basic type of this package. It provides methods for
// interaction with the API.
type Client struct {
httpClient *http.Client
addresses []string
username string
secret string
userAgent string
configLock *sync.RWMutex // Lock for config changes
addressLock *sync.Mutex // Lock used to copy/update the address slice
requestedAPIVersion APIVersion
serverAPIVersion APIVersion
expectedAPIVersion APIVersion
SkipServerVersionCheck bool
}
// ClientVersion returns the API version of the client
func (c *Client) ClientVersion() string {
return DefaultVersionStr
}
// Dialer is an interface that allows network connections to be dialed
// (net.Dialer fulfills this interface) and named pipes (a shim using
// winio.DialPipe)
type Dialer interface {
Dial(network, address string) (net.Conn, error)
}
type dialContext = func(ctx context.Context, network, address string) (net.Conn, error)
// NewClient returns a Client instance ready for communication with the given
// server endpoint. It will use the latest remote API version available in the
// server.
func NewClient(nodes string) (*Client, error) {
client, err := NewVersionedClient(nodes, "")
if err != nil {
return nil, err
}
client.SkipServerVersionCheck = true
client.userAgent = DefaultUserAgent
return client, nil
}
// NewVersionedClient returns a Client instance ready for communication with
// the given server endpoint, using a specific remote API version.
func NewVersionedClient(nodestring string, apiVersionString string) (*Client, error) {
nodes := strings.Split(nodestring, ",")
addresses, err := netutil.AddressesFromNodes(nodes)
if err != nil {
return nil, err
}
if len(addresses) > 1 {
// Shuffle returned addresses in attempt to spread the load
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
rnd.Shuffle(len(addresses), func(i, j int) {
addresses[i], addresses[j] = addresses[j], addresses[i]
})
}
client := &Client{
httpClient: defaultClient(),
addresses: addresses,
configLock: &sync.RWMutex{},
addressLock: &sync.Mutex{},
}
if apiVersionString != "" {
version, err := strconv.Atoi(apiVersionString)
if err != nil {
return nil, err
}
client.requestedAPIVersion = APIVersion(version)
}
return client, nil
}
// SetUserAgent sets the client useragent.
func (c *Client) SetUserAgent(useragent string) {
c.configLock.Lock()
defer c.configLock.Unlock()
c.userAgent = useragent
}
// SetAuth sets the API username and secret to be used for all API requests.
// It should not be called concurrently with any other Client methods.
func (c *Client) SetAuth(username string, secret string) {
c.configLock.Lock()
defer c.configLock.Unlock()
if username != "" {
c.username = username
}
if secret != "" {
c.secret = secret
}
}
// SetProxy will set the proxy URL for both the HTTPClient.
// If the transport method does not support usage
// of proxies, an error will be returned.
func (c *Client) SetProxy(proxy *url.URL) error {
c.configLock.Lock()
defer c.configLock.Unlock()
if client := c.httpClient; client != nil {
transport, supported := client.Transport.(*http.Transport)
if !supported {
return ErrProxyNotSupported
}
transport.Proxy = http.ProxyURL(proxy)
}
return nil
}
// SetTimeout takes a timeout and applies it to both the HTTPClient and
// nativeHTTPClient. It should not be called concurrently with any other Client
// methods.
func (c *Client) SetTimeout(t time.Duration) {
c.configLock.Lock()
defer c.configLock.Unlock()
if c.httpClient != nil {
c.httpClient.Timeout = t
}
}
// GetDialContext returns the current DialContext function, or nil if there is none.
func (c *Client) GetDialContext() dialContext {
c.configLock.RLock()
defer c.configLock.RUnlock()
if c.httpClient == nil {
return nil
}
transport, supported := c.httpClient.Transport.(*http.Transport)
if !supported {
return nil
}
return transport.DialContext
}
// SetDialContext uses the given dial function to establish TCP connections in the HTTPClient.
func (c *Client) SetDialContext(dial dialContext) error {
c.configLock.Lock()
defer c.configLock.Unlock()
if client := c.httpClient; client != nil {
transport, supported := client.Transport.(*http.Transport)
if !supported {
return ErrDialerNotSupported
}
transport.DialContext = dial
}
return nil
}
func (c *Client) checkAPIVersion() error {
serverAPIVersionString, err := c.getServerAPIVersionString()
if err != nil {
return err
}
c.serverAPIVersion, err = NewAPIVersion(serverAPIVersionString)
if err != nil {
return err
}
c.configLock.Lock()
defer c.configLock.Unlock()
if c.requestedAPIVersion == 0 {
c.expectedAPIVersion = c.serverAPIVersion
} else {
c.expectedAPIVersion = c.requestedAPIVersion
}
return nil
}
// Ping pings the API server
//
// See https://goo.gl/wYfgY1 for more details.
func (c *Client) Ping() error {
urlpath := "/_ping"
resp, err := c.do("GET", urlpath, doOptions{})
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return newError(resp)
}
return resp.Body.Close()
}
func (c *Client) getServerAPIVersionString() (version string, err error) {
v, err := c.ServerVersion(context.Background())
if err != nil {
return "", err
}
return v.APIVersion, nil
}
type doOptions struct {
context context.Context
data interface{}
values url.Values
headers map[string]string
fieldSelector string
labelSelector string
namespace string
forceJSON bool
force bool
unversioned bool
retryOn []int // http.status codes
}
func (c *Client) do(method, urlpath string, doOptions doOptions) (*http.Response, error) {
var params io.Reader
if doOptions.data != nil || doOptions.forceJSON {
buf, err := json.Marshal(doOptions.data)
if err != nil {
return nil, err
}
params = bytes.NewBuffer(buf)
}
// Prefix the path with the namespace if given. The caller should only set
// the namespace if this is desired.
if doOptions.namespace != "" {
urlpath = "/" + NamespaceAPIPrefix + "/" + doOptions.namespace + "/" + urlpath
}
if !c.SkipServerVersionCheck && !doOptions.unversioned {
err := c.checkAPIVersion()
if err != nil {
return nil, err
}
}
query := url.Values{}
if doOptions.values != nil {
query = doOptions.values
}
if doOptions.force {
query.Add("force", "1")
}
// Obtain a reader lock to prevent the http client from being
// modified underneath us during a do().
c.configLock.RLock()
defer c.configLock.RUnlock() // This defer matches both the initial and the above lock
httpClient := c.httpClient
endpoint := c.getAPIPath(urlpath, query, doOptions.unversioned)
// The doOptions Context is shared for every attempted request in the do.
ctx := doOptions.context
if ctx == nil {
ctx = context.Background()
}
var failedAddresses = map[string]struct{}{}
c.addressLock.Lock()
var addresses = make([]string, len(c.addresses))
copy(addresses, c.addresses)
c.addressLock.Unlock()
for _, address := range addresses {
target := address + endpoint
req, err := http.NewRequest(method, target, params)
if err != nil {
// Probably should not try and continue if we're unable
// to create the request.
return nil, err
}
req.Header.Set("User-Agent", c.userAgent)
if doOptions.data != nil {
req.Header.Set("Content-Type", "application/json")
} else if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}
if c.username != "" && c.secret != "" {
req.SetBasicAuth(c.username, c.secret)
}
for k, v := range doOptions.headers {
req.Header.Set(k, v)
}
resp, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
// If it is a custom error, return it. It probably knows more than us
if serror.IsStorageOSError(err) {
switch serror.ErrorKind(err) {
case serror.APIUncontactable:
// If API isn't contactable we should try the next address
failedAddresses[address] = struct{}{}
continue
case serror.InvalidHostConfig:
// If invalid host or unknown error, we should report back
fallthrough
case serror.UnknownError:
return nil, err
}
}
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
if _, ok := err.(net.Error); ok {
// Be optimistic and try the next endpoint
failedAddresses[address] = struct{}{}
continue
}
return nil, err
}
}
var shouldretry bool
if doOptions.retryOn != nil {
for _, code := range doOptions.retryOn {
if resp.StatusCode == code {
failedAddresses[address] = struct{}{}
shouldretry = true
}
}
}
// If we get to the point of response, we should move any failed
// addresses to the back.
failed := len(failedAddresses)
if failed > 0 {
// Copy addresses we think are okay into the head of the list
newOrder := make([]string, 0, len(addresses)-failed)
for _, addr := range addresses {
if _, exists := failedAddresses[addr]; !exists {
newOrder = append(newOrder, addr)
}
}
for addr := range failedAddresses {
newOrder = append(newOrder, addr)
}
c.addressLock.Lock()
// Bring in the new order
c.addresses = newOrder
c.addressLock.Unlock()
}
if shouldretry {
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
return nil, newError(resp) // These status codes are likely to be fatal
}
return resp, nil
}
return nil, netutil.ErrAllFailed(addresses)
}
func (c *Client) getAPIPath(path string, query url.Values, unversioned bool) string {
var apiPath = strings.TrimLeft(path, "/")
if !unversioned {
apiPath = fmt.Sprintf("/%s/%s", c.requestedAPIVersion, apiPath)
} else {
apiPath = fmt.Sprintf("/%s", apiPath)
}
if len(query) > 0 {
apiPath = apiPath + "?" + query.Encode()
}
return apiPath
}
func queryString(opts interface{}) string {
if opts == nil {
return ""
}
value := reflect.ValueOf(opts)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
if value.Kind() != reflect.Struct {
return ""
}
items := url.Values(map[string][]string{})
for i := 0; i < value.NumField(); i++ {
field := value.Type().Field(i)
if field.PkgPath != "" {
continue
}
key := field.Tag.Get("qs")
if key == "" {
key = strings.ToLower(field.Name)
} else if key == "-" {
continue
}
addQueryStringValue(items, key, value.Field(i))
}
return items.Encode()
}
func addQueryStringValue(items url.Values, key string, v reflect.Value) {
switch v.Kind() {
case reflect.Bool:
if v.Bool() {
items.Add(key, "1")
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v.Int() > 0 {
items.Add(key, strconv.FormatInt(v.Int(), 10))
}
case reflect.Float32, reflect.Float64:
if v.Float() > 0 {
items.Add(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
}
case reflect.String:
if v.String() != "" {
items.Add(key, v.String())
}
case reflect.Ptr:
if !v.IsNil() {
if b, err := json.Marshal(v.Interface()); err == nil {
items.Add(key, string(b))
}
}
case reflect.Map:
if len(v.MapKeys()) > 0 {
if b, err := json.Marshal(v.Interface()); err == nil {
items.Add(key, string(b))
}
}
case reflect.Array, reflect.Slice:
vLen := v.Len()
if vLen > 0 {
for i := 0; i < vLen; i++ {
addQueryStringValue(items, key, v.Index(i))
}
}
}
}
// Error represents failures in the API. It represents a failure from the API.
type Error struct {
Status int
Message string
}
func newError(resp *http.Response) *Error {
type jsonError struct {
Message string `json:"message"`
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return &Error{Status: resp.StatusCode, Message: fmt.Sprintf("cannot read body, err: %v", err)}
}
// attempt to unmarshal the error if in json format
jerr := &jsonError{}
err = json.Unmarshal(data, jerr)
if err != nil {
return &Error{Status: resp.StatusCode, Message: string(data)} // Failed, just return string
}
return &Error{Status: resp.StatusCode, Message: jerr.Message}
}
func (e *Error) Error() string {
var niceStatus string
switch e.Status {
case 400, 500:
niceStatus = "Server failed to process your request. Was the data correct?"
case 401:
niceStatus = "Unauthenticated access of secure endpoint, please retry after authentication"
case 403:
niceStatus = "Forbidden request. Your user cannot perform this action"
case 404:
niceStatus = "Requested object not found. Does this item exist?"
}
if niceStatus != "" {
return fmt.Sprintf("API error (%s): %s", niceStatus, e.Message)
}
return fmt.Sprintf("API error (%s): %s", http.StatusText(e.Status), e.Message)
}
// defaultPooledTransport returns a new http.Transport with similar default
// values to http.DefaultTransport. Do not use this for transient transports as
// it can leak file descriptors over time. Only use this for transports that
// will be re-used for the same host(s).
func defaultPooledTransport(dialer Dialer) *http.Transport {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: dialer.Dial,
TLSHandshakeTimeout: 5 * time.Second,
DisableKeepAlives: false,
MaxIdleConnsPerHost: 1,
}
return transport
}
// defaultClient returns a new http.Client with similar default values to
// http.Client, but with a non-shared Transport, idle connections disabled, and
// keepalives disabled.
// If a custom dialer is not provided, one with sane defaults will be created.
func defaultClient() *http.Client {
dialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second,
}
return &http.Client{
Transport: defaultPooledTransport(dialer),
}
}

View File

@ -1,48 +0,0 @@
package storageos
import (
"encoding/json"
"github.com/storageos/go-api/types"
)
var (
// ClusterMaintenanceAPIPrefix is a path to the HTTP endpoint for managing
// the cluster maintenance mode.
ClusterMaintenanceAPIPrefix = "cluster/maintenance"
)
// Maintenance returns the maintenance status of the cluster
func (c *Client) Maintenance() (*types.Maintenance, error) {
resp, err := c.do("GET", ClusterMaintenanceAPIPrefix, doOptions{})
if err != nil {
return nil, err
}
defer resp.Body.Close()
res := &types.Maintenance{}
if err := json.NewDecoder(resp.Body).Decode(res); err != nil {
return nil, err
}
return res, nil
}
// EnableMaintenance enables maintenance mode in the cluster
func (c *Client) EnableMaintenance() error {
resp, err := c.do("POST", ClusterMaintenanceAPIPrefix, doOptions{})
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// DisableMaintenance disables maintenance mode in the cluster
func (c *Client) DisableMaintenance() error {
resp, err := c.do("DELETE", ClusterMaintenanceAPIPrefix, doOptions{})
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}

View File

@ -1,91 +0,0 @@
package storageos
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/storageos/go-api/types"
)
var (
// HealthAPIPrefix is a partial path to the HTTP endpoint.
HealthAPIPrefix = "health"
)
func (c *Client) ClusterHealth(ctx context.Context) ([]*types.ClusterHealthNode, error) {
status := []*types.ClusterHealthNode{}
url := fmt.Sprintf("/cluster/%s", HealthAPIPrefix)
resp, err := c.do("GET", url, doOptions{context: ctx, retryOn: []int{http.StatusNotFound}})
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
return nil, err
}
return status, nil
}
// CPHealth returns the health of the control plane server at a given url.
func (c *Client) CPHealth(ctx context.Context, hostname string) (*types.CPHealthStatus, error) {
url := fmt.Sprintf("http://%s:%s/v1/%s", hostname, DefaultPort, HealthAPIPrefix)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", c.userAgent)
if c.username != "" && c.secret != "" {
req.SetBasicAuth(c.username, c.secret)
}
c.configLock.RLock()
resp, err := c.httpClient.Do(req.WithContext(ctx))
c.configLock.RUnlock()
if err != nil {
return nil, err
}
defer resp.Body.Close()
var status *types.CPHealthStatus
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
return nil, err
}
return status, nil
}
// DPHealth returns the health of the data plane server at a given url.
func (c *Client) DPHealth(ctx context.Context, hostname string) (*types.DPHealthStatus, error) {
url := fmt.Sprintf("http://%s:%s/v1/%s", hostname, DataplaneHealthPort, HealthAPIPrefix)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", c.userAgent)
if c.username != "" && c.secret != "" {
req.SetBasicAuth(c.username, c.secret)
}
c.configLock.RLock()
resp, err := c.httpClient.Do(req.WithContext(ctx))
c.configLock.RUnlock()
if err != nil {
return nil, err
}
defer resp.Body.Close()
var status *types.DPHealthStatus
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
return nil, err
}
return status, nil
}

View File

@ -1,43 +0,0 @@
package storageos
import (
"encoding/json"
"github.com/storageos/go-api/types"
)
const (
// licenceAPIPrefix is a partial path to the HTTP endpoint.
licenceAPIPrefix = "licencing"
)
// Licence returns the current licence on the server.
func (c *Client) Licence() (*types.Licence, error) {
resp, err := c.do("GET", licenceAPIPrefix, doOptions{})
if err != nil {
return nil, err
}
defer resp.Body.Close()
licence := &types.Licence{}
if err := json.NewDecoder(resp.Body).Decode(&licence); err != nil {
return nil, err
}
return licence, nil
}
// LicenceApply applies a licence on the server.
func (c *Client) LicenceApply(licenceKey string) error {
_, err := c.do("POST", licenceAPIPrefix, doOptions{
data: &types.LicenceKeyContainer{Key: licenceKey},
})
return err
}
// LicenceDelete removes the current licence.
func (c *Client) LicenceDelete() error {
resp, err := c.do("DELETE", licenceAPIPrefix, doOptions{})
if err != nil {
return err
}
return resp.Body.Close()
}

View File

@ -1,62 +0,0 @@
package storageos
import (
"context"
"encoding/json"
"net/url"
"github.com/storageos/go-api/types"
)
var (
// LoggerAPIPrefix is a partial path to the HTTP endpoint.
LoggerAPIPrefix = "logs"
)
// LoggerConfig returns every cluster node's logging configuration.
func (c *Client) LoggerConfig(opts types.ListOptions) ([]*types.Logger, error) {
listOpts := doOptions{
fieldSelector: opts.FieldSelector,
labelSelector: opts.LabelSelector,
context: opts.Context,
}
if opts.LabelSelector != "" {
query := url.Values{}
query.Add("labelSelector", opts.LabelSelector)
listOpts.values = query
}
resp, err := c.do("GET", LoggerAPIPrefix+"/cluster/config", listOpts)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var loggers []*types.Logger
if err := json.NewDecoder(resp.Body).Decode(&loggers); err != nil {
return nil, err
}
return loggers, nil
}
// LoggerUpdate patches updates to logging configuration. Fields to update must
// be listed in the Fields value, and if a list of Nodes is given it will only
// apply to the nodes listed. Returns the updated configuration.
func (c *Client) LoggerUpdate(opts types.LoggerUpdateOptions) ([]*types.Logger, error) {
resp, err := c.do("PATCH", LoggerAPIPrefix+"/cluster/config", doOptions{
data: opts,
context: context.Background(),
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var loggers []*types.Logger
if err := json.NewDecoder(resp.Body).Decode(&loggers); err != nil {
return nil, err
}
return loggers, nil
}

View File

@ -1,47 +0,0 @@
package storageos
import (
"encoding/json"
"errors"
)
var (
// LoginAPIPrefix is a partial path to the HTTP endpoint.
LoginAPIPrefix = "auth/login"
// ErrLoginFailed is the error returned on an unsuccessful login.
ErrLoginFailed = errors.New("failed to get token from API endpoint")
)
// Login attemps to get a token from the API
func (c *Client) Login() (token string, err error) {
resp, err := c.do("POST", LoginAPIPrefix, doOptions{data: struct {
User string `json:"username"`
Pass string `json:"password"`
}{c.username, c.secret}})
if err != nil {
if _, ok := err.(*Error); ok {
return "", ErrLoginFailed
}
return "", err
}
if resp.StatusCode != 200 {
return "", ErrLoginFailed
}
unmarsh := struct {
Token string `json:"token"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&unmarsh); err != nil {
return "", err
}
if unmarsh.Token == "" {
return "", ErrLoginFailed
}
return unmarsh.Token, nil
}

View File

@ -1,125 +0,0 @@
package storageos
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"github.com/storageos/go-api/types"
)
var (
// NamespaceAPIPrefix is a partial path to the HTTP endpoint.
NamespaceAPIPrefix = "namespaces"
// ErrNoSuchNamespace is the error returned when the namespace does not exist.
ErrNoSuchNamespace = errors.New("no such namespace")
// ErrNamespaceInUse is the error returned when the namespace requested to be removed is still in use.
ErrNamespaceInUse = errors.New("namespace in use and cannot be removed")
)
// NamespaceList returns the list of available namespaces.
func (c *Client) NamespaceList(opts types.ListOptions) ([]*types.Namespace, error) {
listOpts := doOptions{
fieldSelector: opts.FieldSelector,
labelSelector: opts.LabelSelector,
namespace: opts.Namespace,
context: opts.Context,
}
if opts.LabelSelector != "" {
query := url.Values{}
query.Add("labelSelector", opts.LabelSelector)
listOpts.values = query
}
resp, err := c.do("GET", NamespaceAPIPrefix, listOpts)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var namespaces []*types.Namespace
if err := json.NewDecoder(resp.Body).Decode(&namespaces); err != nil {
return nil, err
}
return namespaces, nil
}
// Namespace returns a namespace by its reference.
func (c *Client) Namespace(ref string) (*types.Namespace, error) {
resp, err := c.do("GET", NamespaceAPIPrefix+"/"+ref, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, ErrNoSuchNamespace
}
return nil, err
}
defer resp.Body.Close()
var namespace types.Namespace
if err := json.NewDecoder(resp.Body).Decode(&namespace); err != nil {
return nil, err
}
return &namespace, nil
}
// NamespaceCreate creates a namespace on the server and returns the new object.
func (c *Client) NamespaceCreate(opts types.NamespaceCreateOptions) (*types.Namespace, error) {
resp, err := c.do("POST", NamespaceAPIPrefix, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return nil, err
}
var namespace types.Namespace
if err := json.NewDecoder(resp.Body).Decode(&namespace); err != nil {
return nil, err
}
return &namespace, nil
}
// NamespaceUpdate updates a namespace on the server and returns the updated object.
func (c *Client) NamespaceUpdate(opts types.NamespaceCreateOptions) (*types.Namespace, error) {
resp, err := c.do("PUT", NamespaceAPIPrefix+"/"+opts.Name, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return nil, err
}
var namespace types.Namespace
if err := json.NewDecoder(resp.Body).Decode(&namespace); err != nil {
return nil, err
}
return &namespace, nil
}
// NamespaceDelete removes a namespace by its reference.
func (c *Client) NamespaceDelete(opts types.DeleteOptions) error {
deleteOpts := doOptions{
force: opts.Force,
context: opts.Context,
}
resp, err := c.do("DELETE", NamespaceAPIPrefix+"/"+opts.Name, deleteOpts)
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchNamespace
}
if e.Status == http.StatusConflict {
return ErrNamespaceInUse
}
// namespace can't be deleted yet, unless force is supplied
if e.Status == http.StatusPreconditionFailed {
return err
}
}
return err
}
defer resp.Body.Close()
return nil
}

View File

@ -1,31 +0,0 @@
package netutil
import (
"errors"
"fmt"
"strings"
"github.com/storageos/go-api/serror"
)
// ErrAllFailed produces a typed StorageOS error which should be used to indicate that
// the API is not contactable for all of the supplied node addresses.
func ErrAllFailed(addrs []string) error {
msg := fmt.Sprintf("failed to dial all known cluster members, (%s)", strings.Join(addrs, ","))
help := "ensure that the value of $STORAGEOS_HOST (or the -H flag) is correct, and that there are healthy StorageOS nodes in this cluster"
return serror.NewTypedStorageOSError(serror.APIUncontactable, nil, msg, help)
}
func newInvalidNodeError(err error) error {
msg := fmt.Sprintf("invalid node format: %s", err)
help := "please check the format of $STORAGEOS_HOST (or the -H flag) complies with the StorageOS JOIN format"
return serror.NewTypedStorageOSError(serror.InvalidHostConfig, err, msg, help)
}
var (
errUnsupportedScheme = errors.New("unsupported URL scheme")
errInvalidHostName = errors.New("invalid hostname")
errInvalidPortNumber = errors.New("invalid port number")
)

View File

@ -1,5 +0,0 @@
// Package netutil provides network related errors and helper functions.
package netutil
// DefaultDialPort is the default port which the API is contacted on.
const DefaultDialPort = "5705"

View File

@ -1,84 +0,0 @@
package netutil
import (
"net/url"
"strconv"
"strings"
)
const (
httpScheme = "http"
httpsScheme = "https"
tcpScheme = "tcp"
)
// AddressesFromNodes takes a list of node hosts and attempts to return a list of hosts in host:port
// format along with any error encountered.
//
// The function accepts node hosts in URL, ip, ip:port, resolvable-name and resolvable-name:port
// formats and will append the default port value if needed. For hosts where the scheme has been omitted,
// the scheme for the first host will be used. If the first host has no scheme, it will default to http.
func AddressesFromNodes(nodes []string) ([]string, error) {
var addresses []string
var scheme string
for _, node := range nodes {
address := node
// If no scheme present, set the first scheme
if !strings.Contains(address, "://") {
if scheme == "" {
scheme = httpScheme
}
address = strings.Join([]string{scheme, address}, "://")
}
url, err := url.Parse(address)
if err != nil {
return nil, newInvalidNodeError(err)
}
switch url.Scheme {
case tcpScheme:
url.Scheme = httpScheme
fallthrough
case httpScheme, httpsScheme:
if scheme == "" {
scheme = url.Scheme
}
default:
return nil, newInvalidNodeError(errUnsupportedScheme)
}
host := url.Hostname()
if host == "" {
return nil, newInvalidNodeError(errInvalidHostName)
}
// Given input like "http://localhost:8080:8383", url.Parse() will
// return host as "localhost:8000", which isn't a vaild DNS name.
if strings.Contains(host, ":") {
return nil, newInvalidNodeError(errInvalidHostName)
}
port := url.Port()
if port == "" {
port = DefaultDialPort
}
if !validPort(port) {
return nil, newInvalidNodeError(errInvalidPortNumber)
}
addresses = append(addresses, strings.TrimRight(url.String(), "/"))
}
return addresses, nil
}
func validPort(port string) bool {
intPort, err := strconv.Atoi(port)
return (err == nil) &&
(intPort > 0) &&
(intPort <= 65535)
}

View File

@ -1,35 +0,0 @@
package storageos
import (
"encoding/json"
"net/http"
"path"
"github.com/storageos/go-api/types"
)
var (
// NetworkDiagnosticsAPIPrefix is a partial path to the HTTP endpoint for
// the node connectivity diagnostics report.
NetworkDiagnosticsAPIPrefix = "diagnostics/network"
)
// NetworkDiagnostics returns a collection of network connectivity reports. If
// a reference to a node is given, it will only check connectivity from that
// node. Otherwise, connectivity between all cluster nodes will be returned.
func (c *Client) NetworkDiagnostics(ref string) (types.ConnectivityResults, error) {
resp, err := c.do("GET", path.Join(NetworkDiagnosticsAPIPrefix, ref), doOptions{})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, ErrNoSuchNode
}
return nil, err
}
defer resp.Body.Close()
var results types.ConnectivityResults
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
return nil, err
}
return results, nil
}

View File

@ -1,110 +0,0 @@
package storageos
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"github.com/storageos/go-api/types"
)
var (
// NodeAPIPrefix is a partial path to the HTTP endpoint.
NodeAPIPrefix = "nodes"
// ErrNoSuchNode is the error returned when the node does not exist.
ErrNoSuchNode = errors.New("no such node")
// ErrNodeInUse is the error returned when the node requested to be removed is still in use.
ErrNodeInUse = errors.New("node in use and cannot be removed")
)
// NodeList returns the list of available nodes.
func (c *Client) NodeList(opts types.ListOptions) ([]*types.Node, error) {
listOpts := doOptions{
fieldSelector: opts.FieldSelector,
labelSelector: opts.LabelSelector,
context: opts.Context,
}
if opts.LabelSelector != "" {
query := url.Values{}
query.Add("labelSelector", opts.LabelSelector)
listOpts.values = query
}
resp, err := c.do("GET", NodeAPIPrefix, listOpts)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var nodes []*types.Node
if err := json.NewDecoder(resp.Body).Decode(&nodes); err != nil {
return nil, err
}
return nodes, nil
}
// Node returns a node by its reference.
func (c *Client) Node(ref string) (*types.Node, error) {
resp, err := c.do("GET", NodeAPIPrefix+"/"+ref, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, ErrNoSuchNode
}
return nil, err
}
defer resp.Body.Close()
var node types.Node
if err := json.NewDecoder(resp.Body).Decode(&node); err != nil {
return nil, err
}
return &node, nil
}
// NodeUpdate updates a node on the server.
func (c *Client) NodeUpdate(opts types.NodeUpdateOptions) (*types.Node, error) {
ref := opts.Name
if IsUUID(opts.ID) {
ref = opts.ID
}
resp, err := c.do("PUT", NodeAPIPrefix+"/"+ref, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var node types.Node
if err := json.NewDecoder(resp.Body).Decode(&node); err != nil {
return nil, err
}
return &node, nil
}
// NodeDelete removes a node by its reference.
func (c *Client) NodeDelete(opts types.DeleteOptions) error {
deleteOpts := doOptions{
namespace: opts.Namespace,
force: opts.Force,
context: opts.Context,
}
resp, err := c.do("DELETE", NodeAPIPrefix+"/"+opts.Name, deleteOpts)
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchNode
}
if e.Status == http.StatusConflict {
return ErrNodeInUse
}
}
return err
}
defer resp.Body.Close()
return nil
}

View File

@ -1,102 +0,0 @@
package storageos
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/storageos/go-api/types"
)
var (
// PolicyAPIPrefix is a partial path to the HTTP endpoint.
PolicyAPIPrefix = "policies"
// ErrNoSuchPolicy is the error returned when the policy does not exist.
ErrNoSuchPolicy = errors.New("no such policy")
)
// nopMarshaler is an alias to a []byte that implements json.Marshaler
// it bypasses the base64 encoded string representation that json will give byte slices.
// It should only be used to wrap []byte types containing pre-rendered valid json that will later
// (out of the caller's control) be run through json.Marshal
type nopMarshaler []byte
func (n *nopMarshaler) MarshalJSON() ([]byte, error) {
return *n, nil
}
// PolicyCreate creates a policy on the server.
func (c *Client) PolicyCreate(ctx context.Context, jsonl []byte) error {
nopm := nopMarshaler(jsonl)
_, err := c.do("POST", PolicyAPIPrefix, doOptions{
data: &nopm,
context: ctx,
headers: map[string]string{"Content-Type": "application/x-jsonlines"},
})
return err
}
// Policy returns a policy on the server by ID.
func (c *Client) Policy(id string) (*types.Policy, error) {
path := fmt.Sprintf("%s/%s", PolicyAPIPrefix, id)
resp, err := c.do("GET", path, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, ErrNoSuchPolicy
}
return nil, err
}
defer resp.Body.Close()
var policy *types.Policy
if err := json.NewDecoder(resp.Body).Decode(&policy); err != nil {
return nil, err
}
return policy, nil
}
// PolicyList returns the list of policies on the server.
func (c *Client) PolicyList(opts types.ListOptions) (types.PolicySet, error) {
listOpts := doOptions{
fieldSelector: opts.FieldSelector,
labelSelector: opts.LabelSelector,
namespace: opts.Namespace,
context: opts.Context,
}
if opts.LabelSelector != "" {
query := url.Values{}
query.Add("labelSelector", opts.LabelSelector)
listOpts.values = query
}
resp, err := c.do("GET", PolicyAPIPrefix, listOpts)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var policies types.PolicySet
if err := json.NewDecoder(resp.Body).Decode(&policies); err != nil {
return nil, err
}
return policies, nil
}
// PolicyDelete deletes a policy on the server by ID.
func (c *Client) PolicyDelete(opts types.DeleteOptions) error {
resp, err := c.do("DELETE", PolicyAPIPrefix+"/"+opts.Name, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchPolicy
}
}
return err
}
defer resp.Body.Close()
return nil
}

View File

@ -1,117 +0,0 @@
package storageos
import (
"encoding/json"
"errors"
"net/http"
"github.com/storageos/go-api/types"
)
var (
// PoolAPIPrefix is a partial path to the HTTP endpoint.
PoolAPIPrefix = "pools"
// ErrNoSuchPool is the error returned when the pool does not exist.
ErrNoSuchPool = errors.New("no such pool")
// ErrPoolInUse is the error returned when the pool requested to be removed is still in use.
ErrPoolInUse = errors.New("pool in use and cannot be removed")
)
// PoolList returns the list of available pools.
func (c *Client) PoolList(opts types.ListOptions) ([]*types.Pool, error) {
listOpts := doOptions{
fieldSelector: opts.FieldSelector,
labelSelector: opts.LabelSelector,
namespace: opts.Namespace,
context: opts.Context,
}
resp, err := c.do("GET", PoolAPIPrefix, listOpts)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var pools []*types.Pool
if err := json.NewDecoder(resp.Body).Decode(&pools); err != nil {
return nil, err
}
return pools, nil
}
// PoolCreate creates a pool on the server and returns the new object.
func (c *Client) PoolCreate(opts types.PoolOptions) (*types.Pool, error) {
resp, err := c.do("POST", PoolAPIPrefix, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return nil, err
}
var pool types.Pool
if err := json.NewDecoder(resp.Body).Decode(&pool); err != nil {
return nil, err
}
return &pool, nil
}
// PoolUpdate - update pool
func (c *Client) PoolUpdate(opts types.PoolOptions) (*types.Pool, error) {
ref := opts.Name
if IsUUID(opts.ID) {
ref = opts.ID
}
resp, err := c.do("PUT", PoolAPIPrefix+"/"+ref, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return nil, err
}
var pool types.Pool
if err := json.NewDecoder(resp.Body).Decode(&pool); err != nil {
return nil, err
}
return &pool, nil
}
// Pool returns a pool by its reference.
func (c *Client) Pool(ref string) (*types.Pool, error) {
resp, err := c.do("GET", PoolAPIPrefix+"/"+ref, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, ErrNoSuchPool
}
return nil, err
}
defer resp.Body.Close()
var pool types.Pool
if err := json.NewDecoder(resp.Body).Decode(&pool); err != nil {
return nil, err
}
return &pool, nil
}
// PoolDelete removes a pool by its reference.
func (c *Client) PoolDelete(opts types.DeleteOptions) error {
deleteOpts := doOptions{
force: opts.Force,
context: opts.Context,
}
resp, err := c.do("DELETE", PoolAPIPrefix+"/"+opts.Name, deleteOpts)
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchPool
}
if e.Status == http.StatusConflict {
return ErrPoolInUse
}
}
return err
}
defer resp.Body.Close()
return nil
}

View File

@ -1,141 +0,0 @@
package storageos
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"github.com/storageos/go-api/types"
)
var (
// RuleAPIPrefix is a partial path to the HTTP endpoint.
RuleAPIPrefix = "rules"
// ErrNoSuchRule is the error returned when the rule does not exist.
ErrNoSuchRule = errors.New("no such rule")
// ErrRuleInUse is the error returned when the rule requested to be removed is still in use.
ErrRuleInUse = errors.New("rule in use and cannot be removed")
)
// RuleList returns the list of available rules.
func (c *Client) RuleList(opts types.ListOptions) ([]*types.Rule, error) {
listOpts := doOptions{
fieldSelector: opts.FieldSelector,
labelSelector: opts.LabelSelector,
namespace: opts.Namespace,
context: opts.Context,
}
if opts.LabelSelector != "" {
query := url.Values{}
query.Add("labelSelector", opts.LabelSelector)
listOpts.values = query
}
resp, err := c.do("GET", RuleAPIPrefix, listOpts)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var rules []*types.Rule
if err := json.NewDecoder(resp.Body).Decode(&rules); err != nil {
return nil, err
}
return rules, nil
}
// Rule returns a rule by its reference.
func (c *Client) Rule(namespace string, ref string) (*types.Rule, error) {
path, err := namespacedRefPath(namespace, RuleAPIPrefix, ref)
if err != nil {
return nil, err
}
resp, err := c.do("GET", path, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, ErrNoSuchRule
}
return nil, err
}
defer resp.Body.Close()
var rule types.Rule
if err := json.NewDecoder(resp.Body).Decode(&rule); err != nil {
return nil, err
}
return &rule, nil
}
// RuleCreate creates a rule on the server and returns the new object.
func (c *Client) RuleCreate(opts types.RuleCreateOptions) (*types.Rule, error) {
path, err := namespacedPath(opts.Namespace, RuleAPIPrefix)
if err != nil {
return nil, err
}
resp, err := c.do("POST", path, doOptions{
data: opts,
// namespace: opts.Namespace,
context: opts.Context,
})
if err != nil {
return nil, err
}
var rule types.Rule
if err := json.NewDecoder(resp.Body).Decode(&rule); err != nil {
return nil, err
}
return &rule, nil
}
// RuleUpdate updates a rule on the server.
func (c *Client) RuleUpdate(opts types.RuleUpdateOptions) (*types.Rule, error) {
ref := opts.Name
if IsUUID(opts.ID) {
ref = opts.ID
}
path, err := namespacedRefPath(opts.Namespace, RuleAPIPrefix, ref)
if err != nil {
return nil, err
}
resp, err := c.do("PUT", path, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var rule types.Rule
if err := json.NewDecoder(resp.Body).Decode(&rule); err != nil {
return nil, err
}
return &rule, nil
}
// RuleDelete removes a rule by its reference.
func (c *Client) RuleDelete(opts types.DeleteOptions) error {
deleteOpts := doOptions{
namespace: opts.Namespace,
force: opts.Force,
context: opts.Context,
}
resp, err := c.do("DELETE", RuleAPIPrefix+"/"+opts.Name, deleteOpts)
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchRule
}
if e.Status == http.StatusConflict {
return ErrRuleInUse
}
}
return err
}
defer resp.Body.Close()
return nil
}

View File

@ -1,11 +0,0 @@
package serror
//go:generate stringer -type=StorageOSErrorKind error_kind.go
type StorageOSErrorKind int
// Known error kinds
const (
UnknownError StorageOSErrorKind = iota
APIUncontactable
InvalidHostConfig
)

View File

@ -1,37 +0,0 @@
package serror
import (
"encoding/json"
"fmt"
"strings"
)
var kindLookupMap map[string]StorageOSErrorKind
func init() {
kindLookupMap = make(map[string]StorageOSErrorKind)
// Populate the lookup map with all the known constants
for i := StorageOSErrorKind(0); !strings.HasPrefix(i.String(), "StorageOSErrorKind("); i++ {
kindLookupMap[i.String()] = i
}
}
func (s *StorageOSErrorKind) UnmarshalJSON(b []byte) error {
str := ""
if err := json.Unmarshal(b, &str); err != nil {
return err
}
v, ok := kindLookupMap[str]
if !ok {
return fmt.Errorf("Failed to unmarshal ErrorKind %s", s)
}
*s = v
return nil
}
func (s *StorageOSErrorKind) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}

View File

@ -1,34 +0,0 @@
package serror
import (
"encoding/json"
)
type StorageOSError interface {
// embedding error provides compatibility with standard error handling code
error
// Encoding/decoding methods to help errors traverse API boundaries
json.Marshaler
json.Unmarshaler
Err() error // Returns the underlying error that caused this event
String() string // A short string representing the error (for logging etc)
Help() string // A larger string that should provide informative debug instruction to users
Kind() StorageOSErrorKind // A type representing a set of known error conditions, helpful to switch on
Extra() map[string]string // A container for error specific information
// TODO: should we include callstack traces here? We could have a debug mode for it.
}
func ErrorKind(err error) StorageOSErrorKind {
if serr, ok := err.(StorageOSError); ok {
return serr.Kind()
}
return UnknownError
}
func IsStorageOSError(err error) bool {
_, ok := err.(StorageOSError)
return ok
}

View File

@ -1,16 +0,0 @@
// Code generated by "stringer -type=StorageOSErrorKind error_kind.go"; DO NOT EDIT.
package serror
import "strconv"
const _StorageOSErrorKind_name = "UnknownErrorAPIUncontactableInvalidHostConfig"
var _StorageOSErrorKind_index = [...]uint8{0, 12, 28, 45}
func (i StorageOSErrorKind) String() string {
if i < 0 || i >= StorageOSErrorKind(len(_StorageOSErrorKind_index)-1) {
return "StorageOSErrorKind(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _StorageOSErrorKind_name[_StorageOSErrorKind_index[i]:_StorageOSErrorKind_index[i+1]]
}

View File

@ -1,64 +0,0 @@
package serror
import (
"encoding/json"
)
func NewTypedStorageOSError(kind StorageOSErrorKind, err error, msg string, help string) StorageOSError {
return &typedStorageOSError{
internal: &internal_TypedStorageOSError{
ErrorKind: &kind,
Cause: err,
ErrMessage: msg,
HelpMessage: help,
},
}
}
func NewUntypedStorageOSError(err error, msg string, help string) StorageOSError {
var kind StorageOSErrorKind = UnknownError
return &typedStorageOSError{
internal: &internal_TypedStorageOSError{
ErrorKind: &kind,
Cause: err,
ErrMessage: msg,
HelpMessage: help,
},
}
}
type internal_TypedStorageOSError struct {
ErrorKind *StorageOSErrorKind `json:"error_kind"`
Cause error `json:"caused_by"`
ErrMessage string `json:"error_message"`
HelpMessage string `json:"help_message"`
ExtraMap map[string]string `json:"extra"`
}
type typedStorageOSError struct {
internal *internal_TypedStorageOSError
}
func (t *typedStorageOSError) MarshalJSON() ([]byte, error) {
return json.Marshal(t.internal)
}
func (t *typedStorageOSError) UnmarshalJSON(d []byte) error {
internal := &internal_TypedStorageOSError{}
err := json.Unmarshal(d, internal)
if err != nil {
return err
}
t.internal = internal
return nil
}
func (t *typedStorageOSError) Error() string { return t.String() }
func (t *typedStorageOSError) Err() error { return t.internal.Cause }
func (t *typedStorageOSError) String() string { return t.internal.ErrMessage }
func (t *typedStorageOSError) Help() string { return t.internal.HelpMessage }
func (t *typedStorageOSError) Kind() StorageOSErrorKind { return *t.internal.ErrorKind }
func (t *typedStorageOSError) Extra() map[string]string { return t.internal.ExtraMap }

View File

@ -1,28 +0,0 @@
package storageos
import (
"context"
"encoding/json"
"net/http"
"github.com/storageos/go-api/types"
)
// ServerVersion returns the server's version and runtime info.
func (c *Client) ServerVersion(ctx context.Context) (*types.VersionInfo, error) {
// Send as unversioned
resp, err := c.do("GET", "version", doOptions{context: ctx, unversioned: true})
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, newError(resp)
}
defer resp.Body.Close()
var version types.VersionInfo
if err := json.NewDecoder(resp.Body).Decode(&version); err != nil {
return nil, err
}
return &version, nil
}

View File

@ -1,90 +0,0 @@
package storageos
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"strconv"
"github.com/storageos/go-api/types"
)
var (
// TemplateAPIPrefix is a partial path to the HTTP endpoint.
TemplateAPIPrefix = "/templates"
// ErrNoSuchTemplate is the error returned when the template does not exist.
ErrNoSuchTemplate = errors.New("no such template")
// ErrTemplateInUse is the error returned when the template requested to be removed is still in use.
ErrTemplateInUse = errors.New("template in use and cannot be removed")
)
// TemplateList returns the list of available templates.
func (c *Client) TemplateList(opts types.ListOptions) ([]types.Template, error) {
path := TemplateAPIPrefix + "?" + queryString(opts)
resp, err := c.do("GET", path, doOptions{context: opts.Context})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var templates []types.Template
if err := json.NewDecoder(resp.Body).Decode(&templates); err != nil {
return nil, err
}
return templates, nil
}
// TemplateCreate creates a template on the server and returns the new object.
func (c *Client) TemplateCreate(opts types.TemplateCreateOptions) (string, error) {
resp, err := c.do("POST", TemplateAPIPrefix, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return "", err
}
defer resp.Body.Close()
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return strconv.Unquote(string(out))
}
// Template returns a template by its reference.
func (c *Client) Template(ref string) (*types.Template, error) {
resp, err := c.do("GET", TemplateAPIPrefix+"/"+ref, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, ErrNoSuchTemplate
}
return nil, err
}
defer resp.Body.Close()
var template types.Template
if err := json.NewDecoder(resp.Body).Decode(&template); err != nil {
return nil, err
}
return &template, nil
}
// TemplateDelete removes a template by its reference.
func (c *Client) TemplateDelete(ref string) error {
resp, err := c.do("DELETE", TemplateAPIPrefix+"/"+ref, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchTemplate
}
if e.Status == http.StatusConflict {
return ErrTemplateInUse
}
}
return nil
}
defer resp.Body.Close()
return nil
}

View File

@ -1,14 +0,0 @@
package types
// AuthConfig contains authorization information for connecting to a Registry
type AuthConfig struct {
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Auth string `json:"auth,omitempty"`
ServerAddress string `json:"serveraddress,omitempty"`
// IdentityToken is used to authenticate the user and get
// an access token for the registry.
IdentityToken string `json:"identitytoken,omitempty"`
}

View File

@ -1,25 +0,0 @@
package types
// ErrCapacityStatsUnchanged can be used when comparing stats
const ErrCapacityStatsUnchanged = "no changes"
// CapacityStats is used to report capacity statistics on pools and controllers.
type CapacityStats struct {
// TotalCapacityBytes is the object's total capacity in bytes.
TotalCapacityBytes uint64 `json:"totalCapacityBytes"`
// AvailableCapacityBytes is the object's available capacity in bytes.
AvailableCapacityBytes uint64 `json:"availableCapacityBytes"`
// ProvisionedCapacityBytes is the object's provisioned capacity in bytes.
ProvisionedCapacityBytes uint64 `json:"provisionedCapacityBytes"`
}
// IsEqual checks if capacity values are the same
func (c CapacityStats) IsEqual(n CapacityStats) bool {
if c == n {
return true
}
return false
}

View File

@ -1,10 +0,0 @@
package types
import "time"
// Maintenance is used to place the cluster in maintenance mode.
type Maintenance struct {
Enabled bool `json:"enabled"`
UpdatedBy string `json:"updatedBy"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -1,41 +0,0 @@
package types
import "time"
// ConnectivityResult capture's a node connectivity report to a given target.
type ConnectivityResult struct {
// Label is a human-readable reference for the service being tested.
Label string `json:"label"`
// Address is the host:port of the service being tested.
Address string `json:"address"`
// Source is a human-readable reference for the source host where the tests
// were run from.
Source string `json:"source"`
// LatencyNS is the duration in nanoseconds that the check took to complete.
// Will also be set on unsuccessful attempts.
LatencyNS time.Duration `json:"latency_ns"`
// Error is set if the test returned an error.
Error string `json:"error"`
}
// IsOK returns true iff no error
func (r ConnectivityResult) IsOK() bool {
return len(r.Error) == 0
}
// ConnectivityResults is a collection of connectivty reports.
type ConnectivityResults []ConnectivityResult
// IsOK returns true iff no error in any result.
func (r ConnectivityResults) IsOK() bool {
for _, result := range r {
if !result.IsOK() {
return false
}
}
return true
}

View File

@ -1,24 +0,0 @@
package types
import "context"
// DeleteOptions are available parameters for deleting existing volumes.
type DeleteOptions struct {
// Volume unique ID.
// Read Only: true
ID string `json:"id"`
// Volume name.
// Read Only: true
Name string `json:"name"`
// Namespace is the object scope, such as for teams and projects.
Namespace string `json:"namespace"`
// Force will cause the volume to be deleted even if it's in use.
Force bool `json:"force"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,36 +0,0 @@
package types
import "time"
// Deployment Volume master or replica deployment details.
// swagger:model Deployment
type Deployment struct {
// Deployment unique ID
// Read Only: true
ID string `json:"id"`
// Inode number
// Read Only: true
Inode uint32 `json:"inode"`
// Node ID
// Read Only: true
Node string `json:"node"`
// Node name
// Read Only: true
NodeName string `json:"nodeName"`
// Health
// Read Only: true
Health string `json:"health"`
// Status
// Read Only: true
Status string `json:"status"`
// Created at
// Read Only: true
CreatedAt time.Time `json:"createdAt"`
}

View File

@ -1,86 +0,0 @@
package types
import "encoding/gob"
// DriverInstance is used to define an instance of a storage capacity driver.
type DriverInstance struct {
// Instance unique ID.
// Read Only: true
ID string `json:"id"`
// Instance name.
Name string `json:"name"`
// Instance description.
Description string `json:"description"`
// Flag describing whether the template is active.
// Default: false
Active bool `json:"active"`
// Config is JSON struct that is passed directly to the driver. There is no
// specific format, and the driver is responsible for validation.
Config interface{} `json:"config"`
// Labels define a list of labels that describe the driver instance. These
// are inherited from the pool when the driver instance is created.
Labels []string `json:"labels"`
// ControllerName specifies the controller that this instance is running on.
ControllerName string `json:"controllerName"`
// PoolID refers to the pool that this driver instance relates to.
PoolID string `json:"poolID"`
// DriverName specifies which capacity driver this is an instance of.
DriverName string `json:"driverName"`
// CapacityStats tracks that capacity usage of this driver instance on the
// current controller.
CapacityStats CapacityStats `json:"capacityStats"`
}
// DriverInstances is a collection of Driver instance objects.
type DriverInstances []*DriverInstance
func init() {
gob.Register(DriverInstance{})
gob.Register([]interface{}{})
}
// Find an instance matching the parameters.
func (i *DriverInstances) Find(pool string, driver string, controller string) *DriverInstance {
for _, inst := range *i {
if inst.PoolID == pool && inst.DriverName == driver && inst.ControllerName == controller {
return inst
}
}
return nil
}
// Add a new instance to the list of instances.
func (i *DriverInstances) Add(new *DriverInstance) {
for _, inst := range *i {
// Skip if it already exists
if inst.PoolID == new.PoolID && inst.DriverName == new.DriverName && inst.ControllerName == new.ControllerName {
return
}
}
*i = append(*i, new)
}
// Remove an instance to the list of instances.
func (i *DriverInstances) Remove(id string) {
// TODO: not working
// for ndx, inst := range *i {
// if inst.ID == id {
// // splice out the item to remove
// *i = append(*i[:ndx], *i[ndx+1:]...)
// return
// }
// }
}

View File

@ -1,13 +0,0 @@
package types
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// ErrorResponse Represents an error.
// swagger:model ErrorResponse
type ErrorResponse struct {
// The error message.
// Required: true
Message string `json:"message"`
}

View File

@ -1,60 +0,0 @@
package types
import "time"
// EventType describes the type of event
type EventType string
// EventTypes are added to events to assist with type assertions
const (
RequestType EventType = "request"
ResponseType = "response"
HeartbeatType = "heartbeat"
BackupType = "backup"
)
// Event describes the fields that all events should implement. Event is
// intended to be inherherited in more specific Event types.
type Event struct {
ID string `json:"id"`
// Parent is used to specify parent event
Parent string `json:"parent"`
EventType EventType `json:"eventType"`
Action string `json:"action"`
Timestamp int64 `json:"timestamp"`
Status string `json:"status"`
Message string `json:"message"`
Log []string `json:"log"`
ProgressPercent int `json:"progressPercent"`
CreatedBy string `json:"createdBy"`
Target string `json:"target"`
ActionPayload interface{} `json:"actionPayload"`
// payload can be encoded into bytes as well
ActionPayloadBytes []byte `json:"actionPayloadBts"`
UpdatedAt time.Time `json:"updatedAt"`
CreatedAt time.Time `json:"createdAt"`
// retry related value
Retry bool `json:"retry"`
RetriedAt time.Time `json:"retriedAt"`
Attempts int `json:"attempts"`
// optional parameter
Deadline time.Time `json:"deadline"`
// optional events to dispatch
Rollback []*Request `json:"rollback"`
RollbackDone bool `json:"rollbackDone"`
Subject string `json:"subject"` // or "queue"
// controller ID which created this event
OriginController string `json:"originController"`
}
// Request is the message structure used for sending request events
type Request struct {
Event
}

View File

@ -1,148 +0,0 @@
package types
import "encoding/json"
type SubModuleStatus struct {
Status string `json:"status"`
UpdatedAt string `json:"updatedAt"`
ChangedAt string `json:"changedAt"`
Message string `json:"message"`
}
type NamedSubModuleStatus struct {
Name string
SubModuleStatus
}
type ClusterHealthNode struct {
NodeID string `json:"nodeID"`
NodeName string `json:"nodeName"`
Submodules struct {
DirectFSInitiator SubModuleStatus `json:"directfs_initiator"`
Director SubModuleStatus `json:"director"`
KV SubModuleStatus `json:"kv"`
KVWrite SubModuleStatus `json:"kv_write"`
NATS SubModuleStatus `json:"nats"`
Presentation SubModuleStatus `json:"presentation"`
RDB SubModuleStatus `json:"rdb"`
} `json:"submodules"`
}
type CPHealthStatus struct {
KV SubModuleStatus
KVWrite SubModuleStatus
NATS SubModuleStatus
Scheduler SubModuleStatus
}
func (c *CPHealthStatus) ToNamedSubmodules() []NamedSubModuleStatus {
return []NamedSubModuleStatus{
{Name: "nats", SubModuleStatus: c.NATS},
{Name: "kv", SubModuleStatus: c.KV},
{Name: "kv_write", SubModuleStatus: c.KVWrite},
{Name: "scheduler", SubModuleStatus: c.Scheduler},
}
}
func (c *CPHealthStatus) UnmarshalJSON(data []byte) error {
unmarsh := struct {
Submodules struct {
KV SubModuleStatus `json:"kv"`
KVWrite SubModuleStatus `json:"kv_write"`
NATS SubModuleStatus `json:"nats"`
Scheduler SubModuleStatus `json:"scheduler"`
} `json:"submodules"`
}{}
if err := json.Unmarshal(data, &unmarsh); err != nil {
return err
}
c.KV = unmarsh.Submodules.KV
c.KVWrite = unmarsh.Submodules.KVWrite
c.NATS = unmarsh.Submodules.NATS
c.Scheduler = unmarsh.Submodules.Scheduler
return nil
}
type DPHealthStatus struct {
DirectFSClient SubModuleStatus
DirectFSServer SubModuleStatus
Director SubModuleStatus
FSDriver SubModuleStatus
FS SubModuleStatus
}
func (d *DPHealthStatus) ToNamedSubmodules() []NamedSubModuleStatus {
return []NamedSubModuleStatus{
{Name: "dfs_client", SubModuleStatus: d.DirectFSClient},
{Name: "dfs_server", SubModuleStatus: d.DirectFSServer},
{Name: "director", SubModuleStatus: d.Director},
{Name: "fs_driver", SubModuleStatus: d.FSDriver},
{Name: "fs", SubModuleStatus: d.FS},
}
}
func (d *DPHealthStatus) UnmarshalJSON(data []byte) error {
unmarsh := struct {
Submodules struct {
DirectFSClient SubModuleStatus `json:"directfs-client"`
DirectFSServer SubModuleStatus `json:"directfs-server"`
Director SubModuleStatus `json:"director"`
FSDriver SubModuleStatus `json:"filesystem-driver"`
FS SubModuleStatus `json:"fs"`
} `json:"submodules"`
}{}
if err := json.Unmarshal(data, &unmarsh); err != nil {
return err
}
d.DirectFSClient = unmarsh.Submodules.DirectFSClient
d.DirectFSServer = unmarsh.Submodules.DirectFSServer
d.Director = unmarsh.Submodules.Director
d.FSDriver = unmarsh.Submodules.FSDriver
d.FS = unmarsh.Submodules.FS
return nil
}
// HealthStatus is the health status json object.
type HealthStatus struct {
Submodules HealthSubmodules `json:"submodules"`
}
// HealthSubmodules is the "submodules" attribuet of HealthStatus.
type HealthSubmodules struct {
KV SubModuleStatus `json:"kv,omitempty"`
KVWrite SubModuleStatus `json:"kv_write,omitempty"`
NATS SubModuleStatus `json:"nats,omitempty"`
Scheduler SubModuleStatus `json:"scheduler,omitempty"`
DirectFSClient SubModuleStatus `json:"directfs_initiator,omitempty"`
DirectFSServer SubModuleStatus `json:"directfs_responder,omitempty"`
Director SubModuleStatus `json:"director,omitempty"`
FSDriver SubModuleStatus `json:"rdb,omitempty"`
FS SubModuleStatus `json:"presentation,omitempty"`
}
// ToCPHealthStatus returns only CPHealthStatus from the HealthStatus.
func (h *HealthStatus) ToCPHealthStatus() *CPHealthStatus {
return &CPHealthStatus{
KV: h.Submodules.KV,
KVWrite: h.Submodules.KVWrite,
NATS: h.Submodules.KVWrite,
Scheduler: h.Submodules.Scheduler,
}
}
// ToDPHealthStatus returns only DPHealthStatus from the HealthStatus.
func (h *HealthStatus) ToDPHealthStatus() *DPHealthStatus {
return &DPHealthStatus{
DirectFSClient: h.Submodules.DirectFSClient,
DirectFSServer: h.Submodules.DirectFSServer,
Director: h.Submodules.Director,
FSDriver: h.Submodules.FSDriver,
FS: h.Submodules.FS,
}
}

View File

@ -1,35 +0,0 @@
package types
import "time"
// FeatureType store features types
type FeatureType string
const (
// HA means High Availability
HA = FeatureType("HA")
// DEV means developer licence
DEV = FeatureType("DEV")
// TRIAL means trial licence
TRIAL = FeatureType("TRIAL")
)
// Licence holds the information to be encoded in the licence key. It needs to be synced across
// the django server running on portal-API as well as the corresponding decoding package on the
// storageOS control plane
type Licence struct {
ArrayUUID string `json:"arrayUUID,omitempty"`
ClusterID string `json:"clusterID,omitempty"`
CustomerID string `json:"customerID"`
CustomerName string `json:"customerName"`
Storage int `json:"storage"`
ValidUntil time.Time `json:"validUntil"`
LicenceType string `json:"licenceType"`
Features map[FeatureType]bool `json:"features"`
Unregistered bool `json:"unregistered"`
}
// LicenceKeyContainer - stores a licence key
type LicenceKeyContainer struct {
Key string `json:"key"`
}

View File

@ -1,19 +0,0 @@
package types
import "context"
// ListOptions are optional parameters for finding and listing most objects.
type ListOptions struct {
// FieldSelector restricts the list of returned objects by their fields. Defaults to everything.
FieldSelector string
// LabelSelector restricts the list of returned objects by their labels. Defaults to everything.
LabelSelector string
// Namespace is the object scope, such as for teams and projects.
Namespace string
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context
}

View File

@ -1,40 +0,0 @@
package types
import "context"
// Logger is the runtime configuration of the node's logging services.
// swagger:model Logger
type Logger struct {
// Node name
Node string `json:"node"`
// Log level
Level string `json:"level"`
// Log filter
Filter string `json:"filter"`
// Log filters by category
// Read Only: true
Categories map[string]string `json:"categories"`
}
// LoggerUpdateOptions are the available parameters for updating loggers.
type LoggerUpdateOptions struct {
// Log level
Level string `json:"level"`
// Log filter
Filter string `json:"filter"`
// List of nodes to update. All if not set.
Nodes []string `json:"nodes"`
// List of fields to update. Must be set.
Fields []string `json:"fields"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,59 +0,0 @@
package types
import (
"context"
"time"
)
// Namespace is used to as a container to isolate namespace and rule obects.
type Namespace struct {
// Namespace unique ID.
// Read Only: true
ID string `json:"id"`
// Namespace name.
// Required: true
Name string `json:"name"`
// The optional DisplayName is how the project is displayed in the web console (defaults to name).
DisplayName string `json:"displayName"`
// Namespcae description.
Description string `json:"description"`
// User-defined key/value metadata.
Labels map[string]string `json:"labels"`
// When the namespace was created.
// Read Only: true
CreatedAt time.Time `json:"createdAt"`
// User that created the namespace.
// Read Only: true
CreatedBy string `json:"createdBy"`
// When the namespace was created.
// Read Only: true
UpdatedAt time.Time `json:"updatedAt"`
}
// NamespaceCreateOptions are available parameters for creating new namespaces.
type NamespaceCreateOptions struct {
// Name is the name of the namespace to create.
// Required: true
Name string `json:"name"`
// The optional DisplayName is how the project is displayed in the web console (defaults to name).
DisplayName string `json:"displayName"`
// Description describes the namespace.
Description string `json:"description"`
// Labels are user-defined key/value metadata.
Labels map[string]string `json:"labels"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,104 +0,0 @@
package types
import (
"time"
)
// Node represents a StorageOS cluster node.
type Node struct {
NodeConfig
HostID uint32 `json:"hostID"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Health string `json:"health"`
HealthUpdatedAt time.Time `json:"healthUpdatedAt"`
VersionInfo map[string]VersionInfo `json:"versionInfo"`
Version string `json:"version"`
Revision string // the GitCommit this maps to
Scheduler bool `json:"scheduler"`
Cordon bool `json:"cordon"`
Drain bool `json:"drain"`
VolumeStats VolumeStats `json:"volumeStats"`
// PoolStats map[string]map[string]CapacityStats `json:"poolStats"`
CapacityStats CapacityStats `json:"capacityStats"`
}
// NodeConfig is a read-only representation of the node's configuration, set at
// start time by environment variables passed to the container or using defaults.
type NodeConfig struct {
// UUID is the unique identifier of the node. It cannot be changed once set.
ID string `json:"id,omitempty"`
// Hostname of the node.
Hostname string `json:"hostname"`
// Address is is used for communication between nodes.
// Nodes will fail to start if the address they first registered with
// changes. This protects against the container being re-scheduled on a
// different host. Nodes will typically use the host server's ip address,
// running the docker container in -net host mode.
Address string `json:"address"`
// KvAddr is the address of the KV store to use for storing configuration.
// It can include the address or FQDN with optional port. Defaults to
// Address/ADVERTISE_IP.
KvAddr string `json:"kvAddr"`
// Port allocations
APIPort int `json:"apiPort"`
NatsPort int `json:"natsPort"`
NatsClusterPort int `json:"natsClusterPort"`
SerfPort int `json:"serfPort"`
DFSPort int `json:"dfsPort"`
KVPeerPort int `json:"kvPeerPort"`
KVClientPort int `json:"kvClientPort"`
Labels map[string]string `json:"labels"`
LogLevel string `json:"logLevel"` // the level of the logs to outout
LogFormat string `json:"logFormat"` // either text or json
LogFilter string `json:"logFilter"` // used to discard messages based on the message's category
// BindAddr is used to control the default address StorageOS binds to. This
// should always be set to 0.0.0.0 (all interfaces).
BindAddr string `json:"bindAddr"`
// DeviceDir is where the volumes are exported. This directory must be
// shared into the container using the rshared volume mount option.
DeviceDir string `json:"deviceDir"`
// Join existing cluster
Join string `json:"join"`
// Backend selects the KV backend, either embedded (testing only) or etcd.
Backend string `json:"kvBackend"`
// EnableDebug is used to enable various debugging features. Used by http
// to enable debug endpoints and as a shortcut to enable debug logging.
EnableDebug bool `json:"debug"`
// Devices specify all devices that are available on the node.
Devices []Device `json:"devices"`
}
// Device - device type
type Device struct {
ID string
Labels map[string]string `json:"labels"`
Status string `json:"status"`
Identifier string `json:"identifier"`
Class string `json:"class"`
CapacityStats CapacityStats `json:"capacityStats"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -1,28 +0,0 @@
package types
import "context"
// NodeUpdateOptions are available parameters for updating existing nodes.
type NodeUpdateOptions struct {
// Node unique ID.
// Read Only: true
ID string `json:"id"`
// Node name.
// Read Only: true
Name string `json:"name"`
// Description of the node.
Description string `json:"description"`
// Labels are user-defined key/value metadata.
Labels map[string]string `json:"labels"`
// Cordon marks the node as unschedulable if true
Cordon bool `json:"cordon"`
Drain bool `json:"drain"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,19 +0,0 @@
package types
// Operator represents a key/field's relationship to value(s).
// See labels.Requirement and fields.Requirement for more details.
type Operator string
// Valid operators
const (
None Operator = ""
DoesNotExist Operator = "!"
Equals Operator = "="
DoubleEquals Operator = "=="
In Operator = "in"
NotEquals Operator = "!="
NotIn Operator = "notin"
Exists Operator = "exists"
GreaterThan Operator = "gt"
LessThan Operator = "lt"
)

View File

@ -1,45 +0,0 @@
package types
import (
"encoding/json"
)
type Policy struct {
Spec struct {
User string `json:"user,omitempty"`
Group string `json:"group,omitempty"`
Readonly bool `json:"readonly,omitempty"`
APIGroup string `json:"apiGroup,omitempty"`
Resource string `json:"resource,omitempty"`
Namespace string `json:"namespace,omitempty"`
NonResourcePath string `json:"nonResourcePath,omitempty"`
} `json:"spec"`
}
// PolicyWithId is used as an internal type to render table formated versions of the json response
type PolicyWithID struct {
Policy
ID string
}
// MarshalJSON returns a marshaled copy of the internal policy object, so it is still valid to use
// with the REST API
func (p *PolicyWithID) MarshalJSON() ([]byte, error) {
return json.Marshal(p.Policy)
}
// PolicySet is a representation of the data structure returned from the REST API
type PolicySet map[string]Policy
func (p PolicySet) GetPoliciesWithID() []*PolicyWithID {
rtn := make([]*PolicyWithID, 0, len(p))
for k, v := range p {
rtn = append(rtn, &PolicyWithID{
Policy: v,
ID: k,
})
}
return rtn
}

View File

@ -1,39 +0,0 @@
package types
// Pool is used to define a capacity pool.
type Pool struct {
// Pool unique ID.
// Read Only: true
ID string `json:"id"`
// Pool name.
// Required: true
Name string `json:"name"`
// Pool description.
Description string `json:"description"`
// Default determines whether this pool is the default if a volume is
// provisioned without a pool specified. There can only be one default pool.
Default bool `json:"default"`
NodeSelector string `json:"nodeSelector"`
// DeviceSelector - specifies a selector to filter node devices based on their labels.
// Only devices from nodes that are in the 'NodeNames' list can be selected
DeviceSelector string `json:"deviceSelector"`
// Populated by the system. Read-only.
CapacityStats CapacityStats `json:"capacityStats"`
// This field is computed based on NodeSelector value
// Populated by the system. Read-only.
Nodes []*Node `json:"nodes"`
// Labels define a list of labels that describe the pool.
Labels map[string]string `json:"labels"`
}
// Pools is a collection of Pool objects
type Pools []*Pool

View File

@ -1,28 +0,0 @@
package types
import "context"
// PoolOptions are available parameters for creating or updating pools.
type PoolOptions struct {
ID string `json:"id"`
Name string `json:"name"`
// Pool description.
Description string `json:"description"`
// Default determines whether this pool is the default if a volume is
// provisioned without a pool specified. There can only be one default pool.
Default bool `json:"default"`
NodeSelector string `json:"nodeSelector"`
// DeviceSelector - specifies a selector to filter node devices based on their labels.
// Only devices from nodes that are in the 'NodeNames' list can be selected
DeviceSelector string `json:"deviceSelector"`
// Labels define a list of labels that describe the pool.
Labels map[string]string `json:"labels"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,125 +0,0 @@
package types
import "context"
// Rule is used to define a rule
type Rule struct {
// Rule unique ID.
// Read Only: true
ID string `json:"id"`
// Rule name.
// Required: true
Name string `json:"name"`
// Namespace is the object name and authentication scope, such as for teams and projects.
Namespace string `json:"namespace"`
// Rule description.
Description string `json:"description"`
// Flag describing whether the rule is active.
// Default: false
Active bool `json:"active"`
// Weight is used to determine order during rule processing. Rules with
// heavier weights are processed later.
// default: 0
Weight int `json:"weight"`
// RuleAction controls whether the action is to add or remove a label from the
// matching object(s).
RuleAction string `json:"action"`
// Selectors defines the list of labels that should trigger a rule.
Selector string `json:"selector"`
// Labels define the list of labels that will be added or removed from the
// matching object(s).
Labels map[string]string `json:"labels"`
}
// Rules is a collection of Rules.
type Rules []*Rule
// RuleCreateOptions are available parameters for creating new rules.
type RuleCreateOptions struct {
// Rule name.
// Required: true
Name string `json:"name"`
// Namespace is the object name and authentication scope, such as for teams and projects.
Namespace string `json:"namespace"`
// Rule description.
Description string `json:"description"`
// Flag describing whether the rule is active.
// Default: false
Active bool `json:"active"`
// Weight is used to determine order during rule processing. Rules with
// heavier weights are processed later.
// default: 0
Weight int `json:"weight"`
// RuleAction controls whether the action is to add or remove a label from the
// matching object(s).
RuleAction string `json:"action"`
// Selectors defines the list of labels that should trigger a rule.
Selector string `json:"selector"`
// Labels define the list of labels that will be added or removed from the
// matching object(s).
Labels map[string]string `json:"labels"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}
// RuleUpdateOptions are available parameters for creating new rules.
type RuleUpdateOptions struct {
// Rule unique ID.
// Read Only: true
ID string `json:"id"`
// Rule name.
// Required: true
Name string `json:"name"`
// Namespace is the object name and authentication scope, such as for teams and projects.
Namespace string `json:"namespace"`
// Rule description.
Description string `json:"description"`
// Flag describing whether the rule is active.
// Default: false
Active bool `json:"active"`
// Weight is used to determine order during rule processing. Rules with
// heavier weights are processed later.
// default: 0
Weight int `json:"weight"`
// Operator is used to compare objects or labels.
Operator string `json:"operator"`
// RuleAction controls whether the action is to add or remove a label from the
// matching object(s).
RuleAction string `json:"action"`
// Selectors defines the list of labels that should trigger a rule.
Selector string `json:"selector"`
// Labels define the list of labels that will be added or removed from the
// matching object(s).
Labels map[string]string `json:"labels"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,53 +0,0 @@
package types
// Template is used to define an auto-naming rule.
type Template struct {
// Template unique ID.
// Read Only: true
ID string `json:"id"`
// Template name.
// Required: true
Name string `json:"name"`
// Template description.
Description string `json:"description"`
// Template format. This is used for pattern matching against labels.
Format string `json:"format"`
// Autoincrement defines whether there is a dynamic numeric component in the
// template that must auto-increment when objects with the same name already
// exists.
AutoIncrement bool `json:"autoIncrement"`
// Padding determines whether a dynamic numeric component in the name should
// be padded.
// default: false
Padding bool `json:"padding"`
// PaddingLength sets the length of the padding. A Padding length of 3 would
// set name similar to `abc001` for the first item. Ignored if Padding set to
// `false`.
PaddingLength int `json:"paddingLength"`
// Flag describing whether the template is active.
// Default: false
Active bool `json:"active"`
// Weight is used to determine order during template processing. Templates
// with heavier weights are processed later.
// default: 0
Weight int `json:"weight"`
// ObjectTypes defines the type names that the template can be applied to.
ObjectTypes []string `json:"objectTypes"`
// Labels define a list of the labels that the object must have in order for
// the template to be applied.
Labels map[string]string `json:"labels"`
}
// Templates is a collection of Template objects
type Templates []*Template

View File

@ -1,51 +0,0 @@
package types
import "context"
// TemplateCreateOptions are available parameters for creating new templates.
type TemplateCreateOptions struct {
// Template name.
// Required: true
Name string `json:"name"`
// Template description.
Description string `json:"description"`
// Template format. This is used for pattern matching against labels.
Format string `json:"format"`
// Autoincrement defines whether there is a dynamic numeric component in the
// template that must auto-increment when objects with the same name already
// exists.
AutoIncrement bool `json:"autoIncrement"`
// Padding determines whether a dynamic numeric component in the name should
// be padded.
// default: false
Padding bool `json:"padding"`
// PaddingLength sets the length of the padding. A Padding length of 3 would
// set name similar to `abc001` for the first item. Ignored if Padding set to
// `false`.
PaddingLength int `json:"paddingLength"`
// Flag describing whether the template is active.
// Default: false
Active bool `json:"active"`
// Weight is used to determine order during template processing. Templates
// with heavier weights are processed later.
// default: 0
Weight int `json:"weight"`
// ObjectTypes defines the type names that the template can be applied to.
ObjectTypes []string `json:"objectTypes"`
// Labels define a list of the labels that the object must have in order for
// the template to be applied.
Labels map[string]string `json:"labels"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,79 +0,0 @@
package types
import (
"context"
"encoding/json"
"strings"
)
type User struct {
UUID string `json:"id"`
Username string `json:"username"`
Groups []string `json:"groups"`
Password string `json:"password,omitempty"`
Role string `json:"role"`
}
func (u *User) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
UUID string `json:"id"`
Username string `json:"username"`
Groups string `json:"groups"`
Password string `json:"password,omitempty"`
Role string `json:"role"`
}{
UUID: u.UUID,
Username: u.Username,
Groups: strings.Join(u.Groups, ","),
Password: u.Password,
Role: u.Role,
})
}
func (u *User) UnmarshalJSON(data []byte) error {
temp := &struct {
UUID string `json:"id"`
Username string `json:"username"`
Groups string `json:"groups"`
Password string `json:"password"`
Role string `json:"role"`
}{}
if err := json.Unmarshal(data, temp); err != nil {
return err
}
u.UUID = temp.UUID
u.Username = temp.Username
u.Password = temp.Password
u.Role = temp.Role
u.Groups = strings.Split(temp.Groups, ",")
return nil
}
type UserCreateOptions struct {
Username string `json:"username"`
Groups []string `json:"groups"`
Password string `json:"password"`
Role string `json:"role"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}
func (u UserCreateOptions) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Username string `json:"username"`
Groups string `json:"groups"`
Password string `json:"password"`
Role string `json:"role"`
}{
Username: u.Username,
Groups: strings.Join(u.Groups, ","),
Password: u.Password,
Role: u.Role,
})
}

View File

@ -1,26 +0,0 @@
package types
// VersionInfo describes version and runtime info.
type VersionInfo struct {
Name string `json:"name"`
BuildDate string `json:"buildDate"`
Revision string `json:"revision"`
Version string `json:"version"`
APIVersion string `json:"apiVersion"`
GoVersion string `json:"goVersion"`
OS string `json:"os"`
Arch string `json:"arch"`
KernelVersion string `json:"kernelVersion"`
Experimental bool `json:"experimental"`
}
type VersionResponse struct {
Client *VersionInfo
Server *VersionInfo
}
// ServerOK returns true when the client could connect to the docker server
// and parse the information received. It returns false otherwise.
func (v VersionResponse) ServerOK() bool {
return v.Server != nil
}

View File

@ -1,143 +0,0 @@
package types
import (
"context"
"time"
)
// DefaultNamespace is used when a namespace hasn't been specified.
const DefaultNamespace = "default"
// Volume represents storage volume.
// swagger:model Volume
type Volume struct {
// Volume unique ID.
// Read Only: true
ID string `json:"id"`
// Block device inode.
// Read Only: true
Inode uint32 `json:"inode"`
// Volume name.
// Required: true
Name string `json:"name"`
// Size in GB.
// Required: true
Size int `json:"size"`
// Name of capacity pool to provision the volume in, or the name of the current pool.
Pool string `json:"pool"`
// Filesystem type to mount. May be set on create, or set by rules to influence client.
FSType string `json:"fsType"`
// Volume description.
Description string `json:"description"`
// User-defined key/value metadata.
Labels map[string]string `json:"labels"`
// Namespace is the object name and authentication scope, such as for teams and projects.
Namespace string `json:"namespace"`
// node selector (where volumes should land)
NodeSelector string `json:"nodeSelector"`
// Volume deployment information for the master volume.
// Read Only: true
Master *Deployment `json:"master,omitempty"`
// Flag indicating if the volume is mounted and in use.
// Read Only: true
Mounted bool `json:"mounted"`
// MountDevice, where the device is located
MountDevice string `json:"mountDevice"`
// Mountpoint, where the volume is mounted
Mountpoint string `json:"mountpoint"`
// When the volume was mounted.
// Read Only: true
MountedAt time.Time `json:"mountedAt,omitempty"`
// Reference to the node that has the volume mounted.
// Read Only: true
MountedBy string `json:"mountedBy,omitempty"`
// Volume deployment information for the replica volumes.
// Read Only: true
Replicas []*Deployment `json:"replicas"`
// Volume health, one of: healthy, degraded or dead.
// Read Only: true
Health string `json:"health"`
// Short status, one of: pending, evaluating, deploying, active, unavailable, failed, updating, deleting.
// Read Only: true
Status string `json:"status"`
// Status message explaining current status.
// Read Only: true
StatusMessage string `json:"statusMessage"`
// mkfs performed on new volumes
MkfsDone bool `json:"mkfsDone"`
MkfsDoneAt time.Time `json:"mkfsDoneAt"`
// When the volume was created.
// Read Only: true
CreatedAt time.Time `json:"createdAt"`
// User that created the volume.
// Read Only: true
CreatedBy string `json:"createdBy"`
}
// VolumeMountOptions - used by clients to inform of volume mount operations.
type VolumeMountOptions struct {
// Volume unique ID.
ID string `json:"id"`
// Name is the name of the volume to mount.
Name string `json:"name"`
// Mountpoint, where the volume is mounted
Mountpoint string `json:"mountpoint"`
// Filesystem type, optional but expected when mounting raw volume
FsType string `json:"fsType"`
// Namespace is the object scope, such as for teams and projects.
Namespace string `json:"namespace"`
// The hostname of the client mounting the volume.
Client string `json:"client"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}
// VolumeUnmountOptions - used by clients to inform of volume mount operations.
type VolumeUnmountOptions struct {
// Volume unique ID.
ID string `json:"id"`
// Name is the name of the volume to unmount.
Name string `json:"name"`
// Namespace is the object scope, such as for teams and projects.
Namespace string `json:"namespace"`
// The hostname of the client unmounting the volume. Must match the hostname
// of the client that registered the mount operation.
Client string `json:"client"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,36 +0,0 @@
package types
import "context"
// VolumeCreateOptions are available parameters for creating new volumes.
type VolumeCreateOptions struct {
// Name is the name of the volume to create.
// Required: true
Name string `json:"name"`
// Description describes the volume.
Description string `json:"description"`
// Size in GB.
// Required: true
Size int `json:"size"`
// Pool is the name or id of capacity pool to provision the volume in.
Pool string `json:"pool"`
// Filesystem type to mount. May be set on create, or set by rules to influence client.
FSType string `json:"fsType"`
// Namespace is the object scope, such as for teams and projects.
Namespace string `json:"namespace"`
// node selector (where volumes should land)
NodeSelector string `json:"nodeSelector"`
// Labels are user-defined key/value metadata.
Labels map[string]string `json:"labels"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,8 +0,0 @@
package types
// VolumeStats - volume stats (volume counts, looking forward to capacity)
type VolumeStats struct {
MasterVolumeCount int `json:"masterVolumeCount"`
ReplicaVolumeCount int `json:"replicaVolumeCount"`
VirtualVolumeCount int `json:"virtualVolumeCount"`
}

View File

@ -1,34 +0,0 @@
package types
import "context"
// VolumeUpdateOptions are available parameters for updating existing volumes.
type VolumeUpdateOptions struct {
// Volume unique ID.
// Read Only: true
ID string `json:"id"`
// Volume name.
// Read Only: true
Name string `json:"name"`
// Description describes the volume.
Description string `json:"description"`
// Size in GB.
// Required: true
Size int `json:"size"`
// Namespace is the object scope, such as for teams and projects.
Namespace string `json:"namespace"`
// node selector (where volumes should land)
NodeSelector string `json:"nodeSelector"`
// Labels are user-defined key/value metadata.
Labels map[string]string `json:"labels"`
// Context can be set with a timeout or can be used to cancel a request.
Context context.Context `json:"-"`
}

View File

@ -1,118 +0,0 @@
package storageos
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/storageos/go-api/types"
)
var (
// UserAPIPrefix is a partial path to the HTTP endpoint.
UserAPIPrefix = "users"
// ErrNoSuchUser is the error returned when the user does not exist.
ErrNoSuchUser = errors.New("no such user")
)
// UserList returns the list of available users.
func (c *Client) UserList(opts types.ListOptions) ([]*types.User, error) {
listOpts := doOptions{
fieldSelector: opts.FieldSelector,
labelSelector: opts.LabelSelector,
namespace: opts.Namespace,
context: opts.Context,
}
if opts.LabelSelector != "" {
query := url.Values{}
query.Add("labelSelector", opts.LabelSelector)
listOpts.values = query
}
resp, err := c.do("GET", UserAPIPrefix, listOpts)
if err != nil {
return nil, err
}
defer resp.Body.Close()
users := make([]*types.User, 0)
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
return nil, err
}
return users, nil
}
// User returns a user by its username/id.
func (c *Client) User(username string) (*types.User, error) {
path := fmt.Sprintf("%s/%s", UserAPIPrefix, username)
resp, err := c.do("GET", path, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, ErrNoSuchUser
}
return nil, err
}
defer resp.Body.Close()
var user *types.User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, err
}
return user, nil
}
// UserCreate creates a user on the server.
func (c *Client) UserCreate(opts types.UserCreateOptions) error {
_, err := c.do("POST", UserAPIPrefix, doOptions{
data: opts,
context: opts.Context,
})
return err
}
// UserUpdate updates a user on the server.
func (c *Client) UserUpdate(ctx context.Context, user *types.User) error {
var ref string
switch {
case user.UUID != "":
ref = user.UUID
case user.Username != "":
ref = user.Username
default:
return ErrNoSuchUser
}
path := fmt.Sprintf("%s/%s", UserAPIPrefix, ref)
resp, err := c.do("POST", path, doOptions{
data: user,
context: ctx,
})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return ErrNoSuchUser
}
return err
}
defer resp.Body.Close()
return nil
}
// UserDelete removes a user by its reference.
func (c *Client) UserDelete(opts types.DeleteOptions) error {
resp, err := c.do("DELETE", UserAPIPrefix+"/"+opts.Name, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchUser
}
}
return err
}
defer resp.Body.Close()
return nil
}

View File

@ -1,16 +0,0 @@
package storageos
import (
"fmt"
"strings"
)
// ParseRef is a helper to split out the namespace and name from a path
// reference.
func ParseRef(ref string) (namespace string, name string, err error) {
parts := strings.Split(ref, "/")
if len(parts) != 2 {
return "", "", fmt.Errorf("Name must be prefixed with <namespace>/")
}
return parts[0], parts[1], nil
}

View File

@ -1,76 +0,0 @@
package storageos
import (
"errors"
"regexp"
)
const (
// IDFormat are the characters allowed to represent an ID.
IDFormat = `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`
// NameFormat are the characters allowed to represent a name.
NameFormat = `[a-zA-Z0-9][a-zA-Z0-9~_.-]+`
)
var (
// IDPattern is a regular expression to validate a unique id against the
// collection of restricted characters.
IDPattern = regexp.MustCompile(`^` + IDFormat + `$`)
// NamePattern is a regular expression to validate names against the
// collection of restricted characters.
NamePattern = regexp.MustCompile(`^` + NameFormat + `$`)
// ErrNoRef is given when the reference given is invalid.
ErrNoRef = errors.New("no ref provided or incorrect format")
// ErrNoNamespace is given when the namespace given is invalid.
ErrNoNamespace = errors.New("no namespace provided or incorrect format")
)
// ValidateNamespaceAndRef returns true if both the namespace and ref are valid.
func ValidateNamespaceAndRef(namespace, ref string) error {
if !IsUUID(ref) && !IsName(ref) {
return ErrNoRef
}
if !IsName(namespace) {
return ErrNoNamespace
}
return nil
}
// ValidateNamespace returns true if the namespace uses a valid name.
func ValidateNamespace(namespace string) error {
if !IsName(namespace) {
return ErrNoNamespace
}
return nil
}
// IsUUID returns true if the string input is a valid UUID string.
func IsUUID(s string) bool {
return IDPattern.MatchString(s)
}
// IsName returns true if the string input is a valid Name string.
func IsName(s string) bool {
return NamePattern.MatchString(s)
}
// namespacedPath checks for valid input and returns api path for a namespaced
// objectType. Use namespacedRefPath for objects.
func namespacedPath(namespace, objectType string) (string, error) {
if err := ValidateNamespace(namespace); err != nil {
return "", err
}
return "/namespaces/" + namespace + "/" + objectType, nil
}
// namespacedRefPath checks for valid input and returns api path for a single
// namespaced object. Use namespacedPath for objects type path.
func namespacedRefPath(namespace, objectType, ref string) (string, error) {
if err := ValidateNamespaceAndRef(namespace, ref); err != nil {
return "", err
}
return "/namespaces/" + namespace + "/" + objectType + "/" + ref, nil
}

View File

@ -1,197 +0,0 @@
package storageos
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"github.com/storageos/go-api/types"
)
var (
// VolumeAPIPrefix is a partial path to the HTTP endpoint.
VolumeAPIPrefix = "volumes"
// ErrNoSuchVolume is the error returned when the volume does not exist.
ErrNoSuchVolume = errors.New("no such volume")
// ErrVolumeInUse is the error returned when the volume requested to be removed is still in use.
ErrVolumeInUse = errors.New("volume in use and cannot be removed")
)
// VolumeList returns the list of available volumes.
func (c *Client) VolumeList(opts types.ListOptions) ([]*types.Volume, error) {
listOpts := doOptions{
fieldSelector: opts.FieldSelector,
labelSelector: opts.LabelSelector,
namespace: opts.Namespace,
context: opts.Context,
}
if opts.LabelSelector != "" {
query := url.Values{}
query.Add("labelSelector", opts.LabelSelector)
listOpts.values = query
}
resp, err := c.do("GET", VolumeAPIPrefix, listOpts)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var volumes []*types.Volume
if err := json.NewDecoder(resp.Body).Decode(&volumes); err != nil {
return nil, err
}
return volumes, nil
}
// Volume returns a volume by its reference.
func (c *Client) Volume(namespace string, ref string) (*types.Volume, error) {
path, err := namespacedRefPath(namespace, VolumeAPIPrefix, ref)
if err != nil {
return nil, err
}
resp, err := c.do("GET", path, doOptions{})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, ErrNoSuchVolume
}
return nil, err
}
defer resp.Body.Close()
var volume types.Volume
if err := json.NewDecoder(resp.Body).Decode(&volume); err != nil {
return nil, err
}
return &volume, nil
}
// VolumeCreate creates a volume on the server and returns the new object.
func (c *Client) VolumeCreate(opts types.VolumeCreateOptions) (*types.Volume, error) {
path, err := namespacedPath(opts.Namespace, VolumeAPIPrefix)
if err != nil {
return nil, err
}
resp, err := c.do("POST", path, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var volume types.Volume
if err := json.NewDecoder(resp.Body).Decode(&volume); err != nil {
return nil, err
}
return &volume, nil
}
// VolumeUpdate updates a volume on the server.
func (c *Client) VolumeUpdate(opts types.VolumeUpdateOptions) (*types.Volume, error) {
ref := opts.Name
if IsUUID(opts.ID) {
ref = opts.ID
}
path, err := namespacedRefPath(opts.Namespace, VolumeAPIPrefix, ref)
if err != nil {
return nil, err
}
resp, err := c.do("PUT", path, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var volume types.Volume
if err := json.NewDecoder(resp.Body).Decode(&volume); err != nil {
return nil, err
}
return &volume, nil
}
// VolumeDelete removes a volume by its reference.
func (c *Client) VolumeDelete(opts types.DeleteOptions) error {
deleteOpts := doOptions{
namespace: opts.Namespace,
force: opts.Force,
context: opts.Context,
}
resp, err := c.do("DELETE", VolumeAPIPrefix+"/"+opts.Name, deleteOpts)
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchVolume
}
if e.Status == http.StatusConflict {
return ErrVolumeInUse
}
}
return err
}
defer resp.Body.Close()
return nil
}
// VolumeMount updates the volume with the client that mounted it.
func (c *Client) VolumeMount(opts types.VolumeMountOptions) error {
ref := opts.Name
if IsUUID(opts.ID) {
ref = opts.ID
}
path, err := namespacedRefPath(opts.Namespace, VolumeAPIPrefix, ref)
if err != nil {
return err
}
resp, err := c.do("POST", path+"/mount", doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchVolume
}
if e.Status == http.StatusConflict {
return ErrVolumeInUse
}
}
return err
}
defer resp.Body.Close()
return nil
}
// VolumeUnmount removes the client from the mount reference.
func (c *Client) VolumeUnmount(opts types.VolumeUnmountOptions) error {
ref := opts.Name
if IsUUID(opts.ID) {
ref = opts.ID
}
path, err := namespacedRefPath(opts.Namespace, VolumeAPIPrefix, ref)
if err != nil {
return err
}
resp, err := c.do("POST", path+"/unmount", doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
if e, ok := err.(*Error); ok {
if e.Status == http.StatusNotFound {
return ErrNoSuchVolume
}
if e.Status == http.StatusConflict {
return ErrVolumeInUse
}
}
return err
}
defer resp.Body.Close()
return nil
}

7
vendor/modules.txt vendored
View File

@ -762,12 +762,6 @@ github.com/spf13/pflag
# github.com/stoewer/go-strcase v1.2.0 => github.com/stoewer/go-strcase v1.2.0
## explicit; go 1.11
github.com/stoewer/go-strcase
# github.com/storageos/go-api v2.2.0+incompatible => github.com/storageos/go-api v2.2.0+incompatible
## explicit
github.com/storageos/go-api
github.com/storageos/go-api/netutil
github.com/storageos/go-api/serror
github.com/storageos/go-api/types
# github.com/stretchr/objx v0.2.0 => github.com/stretchr/objx v0.2.0
## explicit; go 1.12
github.com/stretchr/objx
@ -2786,7 +2780,6 @@ sigs.k8s.io/yaml
# github.com/spf13/cobra => github.com/spf13/cobra v1.4.0
# github.com/spf13/pflag => github.com/spf13/pflag v1.0.5
# github.com/stoewer/go-strcase => github.com/stoewer/go-strcase v1.2.0
# github.com/storageos/go-api => github.com/storageos/go-api v2.2.0+incompatible
# github.com/stretchr/objx => github.com/stretchr/objx v0.2.0
# github.com/stretchr/testify => github.com/stretchr/testify v1.7.0
# github.com/syndtr/gocapability => github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635