diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 830640d3a17..fad7ca33647 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1907,6 +1907,10 @@ "ImportPath": "github.com/prometheus/procfs", "Rev": "454a56f35412459b5e684fd5ec0f9211b94f002a" }, + { + "ImportPath": "github.com/quobyte/api", + "Rev": "bf713b5a4333f44504fa1ce63690de45cfed6413" + }, { "ImportPath": "github.com/rackspace/gophercloud", "Comment": "v1.0.0-920-g934dbf8", diff --git a/Godeps/LICENSES b/Godeps/LICENSES index fff6edfd912..66de2864e8e 100644 --- a/Godeps/LICENSES +++ b/Godeps/LICENSES @@ -60578,6 +60578,42 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ +================================================================================ += vendor/github.com/quobyte/api licensed under: = + + +Copyright (c) 2016, Quobyte Inc. +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. + +* Neither the name of quobyte-automation nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +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/quobyte/api/LICENSE beacc5ea3bcda24bdcec545022dbb0b4 - +================================================================================ + + ================================================================================ = vendor/github.com/rackspace/gophercloud licensed under: = diff --git a/cmd/kube-controller-manager/app/plugins.go b/cmd/kube-controller-manager/app/plugins.go index 89a572ed4ad..231e39a7a4a 100644 --- a/cmd/kube-controller-manager/app/plugins.go +++ b/cmd/kube-controller-manager/app/plugins.go @@ -45,6 +45,7 @@ import ( "k8s.io/kubernetes/pkg/volume/glusterfs" "k8s.io/kubernetes/pkg/volume/host_path" "k8s.io/kubernetes/pkg/volume/nfs" + "k8s.io/kubernetes/pkg/volume/quobyte" "k8s.io/kubernetes/pkg/volume/rbd" "k8s.io/kubernetes/pkg/volume/vsphere_volume" ) @@ -105,6 +106,8 @@ func ProbeControllerVolumePlugins(cloud cloudprovider.Interface, config componen allPlugins = append(allPlugins, glusterfs.ProbeVolumePlugins()...) // add rbd provisioner allPlugins = append(allPlugins, rbd.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, quobyte.ProbeVolumePlugins()...) + if cloud != nil { switch { case aws.ProviderName == cloud.ProviderName(): diff --git a/examples/experimental/persistent-volume-provisioning/README.md b/examples/experimental/persistent-volume-provisioning/README.md index 662c2f09440..48b07913d8b 100644 --- a/examples/experimental/persistent-volume-provisioning/README.md +++ b/examples/experimental/persistent-volume-provisioning/README.md @@ -191,6 +191,94 @@ parameters: * `userId`: Ceph client ID that is used to map the RBD image. Default is the same as `adminId`. * `userSecretName`: The name of Ceph Secret for `userId` to map RBD image. It must exist in the same namespace as PVCs. It is required. +#### Quobyte + + + +```yaml +apiVersion: storage.k8s.io/v1beta1 +kind: StorageClass +metadata: + name: slow +provisioner: kubernetes.io/quobyte +parameters: + quobyteAPIServer: "http://138.68.74.142:7860" + registry: "138.68.74.142:7861" + adminSecretName: "quobyte-admin-secret" + adminSecretNamespace: "kube-system" + user: "root" + group: "root" + quobyteConfig: "BASE" + quobyteTenant: "DEFAULT" +``` + +[Download example](quobyte/quobyte-storage-class.yaml?raw=true) + + +* **quobyteAPIServer** API Server of Quobyte in the format http(s)://api-server:7860 +* **registry** Quobyte registry to use to mount the volume. You can specifiy the registry as : pair or if you want to specify multiple registries you just have to put a comma between them e.q. :,:,:. The host can be an IP address or if you have a working DNS you can also provide the DNS names. +* **adminSecretName** secret that holds information about the Quobyte user and the password to authenticate agains the API server. +* **adminSecretNamespace** The namespace for **adminSecretName**. Default is `default`. +* **user** maps all access to this user. Default is `root`. +* **group** maps all access to this group. Default is `nfsnobody`. +* **quobyteConfig** use the specified configuration to create the volume. You can create a new configuration or modify an existing one with the Web console or the quobyte CLI. Default is `BASE` +* **quobyteTenant** use the specified tenant ID to create/delete the volume. This Quobyte tenant has to be already present in Quobyte. Default is `DEFAULT` + +First create Quobyte admin's Secret in the system namespace. Here the Secret is created in `kube-system`: + +``` +$ kubectl create -f examples/experimental/persistent-volume-provisioning/quobyte/quobyte-admin-secret.yaml --namespace=kube-system +``` + +Then create the Quobyte storage class: + +``` +$ kubectl create -f examples/experimental/persistent-volume-provisioning/quobyte/quobyte-storage-class.yaml +``` + +Now create a PVC + +``` +$ kubectl create -f examples/experimental/persistent-volume-provisioning/claim1.json +``` + +Check the created PVC: + +``` +$ kubectl describe pvc +Name: claim1 +Namespace: default +Status: Bound +Volume: pvc-bdb82652-694a-11e6-b811-080027242396 +Labels: +Capacity: 3Gi +Access Modes: RWO +No events. + +$ kubectl describe pv +Name: pvc-bdb82652-694a-11e6-b811-080027242396 +Labels: +Status: Bound +Claim: default/claim1 +Reclaim Policy: Delete +Access Modes: RWO +Capacity: 3Gi +Message: +Source: + Type: Quobyte (a Quobyte mount on the host that shares a pod's lifetime) + Registry: 138.68.79.14:7861 + Volume: kubernetes-dynamic-pvc-bdb97c58-694a-11e6-91b6-080027242396 + ReadOnly: false +No events. +``` + +Create a Pod to use the PVC: + +``` +$ kubectl create -f examples/experimental/persistent-volume-provisioning/quobyte/example-pod.yaml +``` + + ### User provisioning requests Users request dynamically provisioned storage by including a storage class in their `PersistentVolumeClaim`. diff --git a/examples/experimental/persistent-volume-provisioning/quobyte/example-pod.yaml b/examples/experimental/persistent-volume-provisioning/quobyte/example-pod.yaml new file mode 100644 index 00000000000..eb814f552a8 --- /dev/null +++ b/examples/experimental/persistent-volume-provisioning/quobyte/example-pod.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + name: server +spec: + replicas: 1 + selector: + role: server + template: + metadata: + labels: + role: server + spec: + containers: + - name: server + image: nginx + volumeMounts: + - mountPath: /var/lib/www/html + name: quobytepvc + volumes: + - name: quobytepvc + persistentVolumeClaim: + claimName: claim1 diff --git a/examples/experimental/persistent-volume-provisioning/quobyte/quobyte-admin-secret.yaml b/examples/experimental/persistent-volume-provisioning/quobyte/quobyte-admin-secret.yaml new file mode 100644 index 00000000000..17b269cde52 --- /dev/null +++ b/examples/experimental/persistent-volume-provisioning/quobyte/quobyte-admin-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: quobyte-admin-secret +data: + password: cXVvYnl0ZQ== + user: YWRtaW4= diff --git a/examples/experimental/persistent-volume-provisioning/quobyte/quobyte-storage-class.yaml b/examples/experimental/persistent-volume-provisioning/quobyte/quobyte-storage-class.yaml new file mode 100644 index 00000000000..bf7be9bf27a --- /dev/null +++ b/examples/experimental/persistent-volume-provisioning/quobyte/quobyte-storage-class.yaml @@ -0,0 +1,14 @@ +apiVersion: storage.k8s.io/v1beta1 +kind: StorageClass +metadata: + name: slow +provisioner: kubernetes.io/quobyte +parameters: + quobyteAPIServer: "http://138.68.74.142:7860" + registry: "138.68.74.142:7861" + adminSecretName: "quobyte-admin-secret" + adminSecretNamespace: "kube-system" + user: "root" + group: "root" + quobyteConfig: "BASE" + quobyteTenant: "DEFAULT" diff --git a/examples/volumes/quobyte/Readme.md b/examples/volumes/quobyte/Readme.md index c61e0929910..7c1d219a40d 100644 --- a/examples/volumes/quobyte/Readme.md +++ b/examples/volumes/quobyte/Readme.md @@ -94,11 +94,11 @@ spec: Parameters: -* **registry** Quobyte registry to use to mount the volume. You can specifiy the registry as : pair or if you want to specify multiple registries you just have to put a semicolon between them e.q. :,:,:. The host can be an IP address or if you have a working DNS you can also provide the DNS names. +* **registry** Quobyte registry to use to mount the volume. You can specifiy the registry as : pair or if you want to specify multiple registries you just have to put a comma between them e.q. :,:,:. The host can be an IP address or if you have a working DNS you can also provide the DNS names. * **volume** volume represents a Quobyte volume which must be created before usage. * **readOnly** is the boolean that sets the mountpoint readOnly or readWrite. -* **user** maps all access to this user. Default is root. -* **group** maps all access to this group. Default is empty. +* **user** maps all access to this user. Default is `root`. +* **group** maps all access to this group. Default is `nfsnobody`. Creating the pod: diff --git a/pkg/volume/quobyte/quobyte.go b/pkg/volume/quobyte/quobyte.go index 45883a2b40f..86db28aac98 100644 --- a/pkg/volume/quobyte/quobyte.go +++ b/pkg/volume/quobyte/quobyte.go @@ -20,14 +20,18 @@ import ( "fmt" "os" "path" + goStrings "strings" "github.com/golang/glog" + "github.com/pborman/uuid" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/util/exec" "k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/strings" "k8s.io/kubernetes/pkg/volume" + "k8s.io/kubernetes/pkg/volume/util" ) // ProbeVolumePlugins is the primary entrypoint for volume plugins. @@ -39,11 +43,27 @@ type quobytePlugin struct { host volume.VolumeHost } +// This user is used to authenticate against the +// Quobyte API server and holds all information +type quobyteAPIConfig struct { + quobyteUser string + quobytePassword string + quobyteAPIServer string +} + var _ volume.VolumePlugin = &quobytePlugin{} var _ volume.PersistentVolumePlugin = &quobytePlugin{} +var _ volume.DeletableVolumePlugin = &quobytePlugin{} +var _ volume.ProvisionableVolumePlugin = &quobytePlugin{} +var _ volume.Provisioner = &quobyteVolumeProvisioner{} +var _ volume.Deleter = &quobyteVolumeDeleter{} const ( quobytePluginName = "kubernetes.io/quobyte" + + annotationQuobyteAPIServer = "quobyte.kubernetes.io/api" + annotationQuobyteAPISecret = "quobyte.kubernetes.io/apiuser" + annotationQuobyteAPISecretNamespace = "quobyte.kubernetes.io/apipassword" ) func (plugin *quobytePlugin) Init(host volume.VolumeHost) error { @@ -149,7 +169,8 @@ func (plugin *quobytePlugin) newMounterInternal(spec *volume.Spec, pod *api.Pod, plugin: plugin, }, registry: source.Registry, - readOnly: readOnly}, nil + readOnly: readOnly, + }, nil } func (plugin *quobytePlugin) NewUnmounter(volName string, podUID types.UID) (volume.Unmounter, error) { @@ -157,12 +178,14 @@ func (plugin *quobytePlugin) NewUnmounter(volName string, podUID types.UID) (vol } func (plugin *quobytePlugin) newUnmounterInternal(volName string, podUID types.UID, mounter mount.Interface) (volume.Unmounter, error) { - return &quobyteUnmounter{&quobyte{ - volName: volName, - mounter: mounter, - pod: &api.Pod{ObjectMeta: api.ObjectMeta{UID: podUID}}, - plugin: plugin, - }}, nil + return &quobyteUnmounter{ + &quobyte{ + volName: volName, + mounter: mounter, + pod: &api.Pod{ObjectMeta: api.ObjectMeta{UID: podUID}}, + plugin: plugin, + }, + }, nil } // Quobyte volumes represent a bare host directory mount of an quobyte export. @@ -172,6 +195,8 @@ type quobyte struct { user string group string volume string + tenant string + config string mounter mount.Interface plugin *quobytePlugin volume.MetricsNil @@ -226,22 +251,22 @@ func (mounter *quobyteMounter) SetUpAt(dir string, fsGroup *int64) error { } // GetPath returns the path to the user specific mount of a Quobyte volume -// Returns a path in the format ../user@volume e.g. ../root@MyVolume -// or if a group is set ../user#group@volume +// Returns a path in the format ../user#group@volume func (quobyteVolume *quobyte) GetPath() string { user := quobyteVolume.user if len(user) == 0 { user = "root" } + group := quobyteVolume.group + if len(group) == 0 { + group = "nfsnobody" + } + // Quobyte has only one mount in the PluginDir where all Volumes are mounted // The Quobyte client does a fixed-user mapping pluginDir := quobyteVolume.plugin.host.GetPluginDir(strings.EscapeQualifiedNameForDisk(quobytePluginName)) - if len(quobyteVolume.group) > 0 { - return path.Join(pluginDir, fmt.Sprintf("%s#%s@%s", user, quobyteVolume.group, quobyteVolume.volume)) - } - - return path.Join(pluginDir, fmt.Sprintf("%s@%s", user, quobyteVolume.volume)) + return path.Join(pluginDir, fmt.Sprintf("%s#%s@%s", user, group, quobyteVolume.volume)) } type quobyteUnmounter struct { @@ -258,3 +283,192 @@ func (unmounter *quobyteUnmounter) TearDown() error { func (unmounter *quobyteUnmounter) TearDownAt(dir string) error { return nil } + +type quobyteVolumeDeleter struct { + *quobyteMounter + pv *api.PersistentVolume +} + +func (plugin *quobytePlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, error) { + if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.Quobyte == nil { + return nil, fmt.Errorf("spec.PersistentVolumeSource.Spec.Quobyte is nil") + } + + return plugin.newDeleterInternal(spec) +} + +func (plugin *quobytePlugin) newDeleterInternal(spec *volume.Spec) (volume.Deleter, error) { + source, readOnly, err := getVolumeSource(spec) + if err != nil { + return nil, err + } + + return &quobyteVolumeDeleter{ + quobyteMounter: &quobyteMounter{ + quobyte: &quobyte{ + volName: spec.Name(), + user: source.User, + group: source.Group, + volume: source.Volume, + plugin: plugin, + }, + registry: source.Registry, + readOnly: readOnly, + }, + pv: spec.PersistentVolume, + }, nil +} + +func (plugin *quobytePlugin) NewProvisioner(options volume.VolumeOptions) (volume.Provisioner, error) { + if len(options.AccessModes) == 0 { + options.AccessModes = plugin.GetAccessModes() + } + + return plugin.newProvisionerInternal(options) +} + +func (plugin *quobytePlugin) newProvisionerInternal(options volume.VolumeOptions) (volume.Provisioner, error) { + return &quobyteVolumeProvisioner{ + quobyteMounter: &quobyteMounter{ + quobyte: &quobyte{ + plugin: plugin, + }, + }, + options: options, + }, nil +} + +type quobyteVolumeProvisioner struct { + *quobyteMounter + options volume.VolumeOptions +} + +func (provisioner *quobyteVolumeProvisioner) Provision() (*api.PersistentVolume, error) { + if provisioner.options.Selector != nil { + return nil, fmt.Errorf("claim Selector is not supported") + } + var apiServer, adminSecretName, quobyteUser, quobytePassword string + adminSecretNamespace := "default" + provisioner.config = "BASE" + provisioner.tenant = "DEFAULT" + + for k, v := range provisioner.options.Parameters { + switch goStrings.ToLower(k) { + case "registry": + provisioner.registry = v + case "adminsecretname": + adminSecretName = v + case "adminsecretnamespace": + adminSecretNamespace = v + case "quobyteapiserver": + apiServer = v + case "user": + provisioner.user = v + case "group": + provisioner.group = v + case "quobytetenant": + provisioner.tenant = v + case "quobyteconfig": + provisioner.config = v + default: + return nil, fmt.Errorf("invalid option %q for volume plugin %s", k, provisioner.plugin.GetPluginName()) + } + } + + secretMap, err := util.GetSecret(adminSecretNamespace, adminSecretName, provisioner.plugin.host.GetKubeClient()) + if err != nil { + return nil, err + } + + var ok bool + if quobyteUser, ok = secretMap["user"]; !ok { + return nil, fmt.Errorf("Missing \"user\" in secret") + } + + if quobytePassword, ok = secretMap["password"]; !ok { + return nil, fmt.Errorf("Missing \"password\" in secret") + } + + if !validateRegistry(provisioner.registry) { + return nil, fmt.Errorf("Quoybte registry missing or malformed: must be a host:port pair or multiple pairs seperated by commas") + } + + if len(apiServer) == 0 { + return nil, fmt.Errorf("Quoybte API server missing or malformed: must be a http(s)://host:port pair or multiple pairs seperated by commas") + } + + // create random image name + provisioner.volume = fmt.Sprintf("kubernetes-dynamic-pvc-%s", uuid.NewUUID()) + + cfg := &quobyteAPIConfig{ + quobyteAPIServer: apiServer, + quobyteUser: quobyteUser, + quobytePassword: quobytePassword, + } + manager := &quobyteVolumeManager{ + config: cfg, + } + + vol, sizeGB, err := manager.createVolume(provisioner) + if err != nil { + return nil, err + } + pv := new(api.PersistentVolume) + pv.Spec.PersistentVolumeSource.Quobyte = vol + pv.Spec.PersistentVolumeReclaimPolicy = provisioner.options.PersistentVolumeReclaimPolicy + pv.Spec.AccessModes = provisioner.options.AccessModes + pv.Spec.Capacity = api.ResourceList{ + api.ResourceName(api.ResourceStorage): resource.MustParse(fmt.Sprintf("%dGi", sizeGB)), + } + + util.AddVolumeAnnotations(pv, map[string]string{ + annotationQuobyteAPIServer: apiServer, + annotationQuobyteAPISecret: adminSecretName, + annotationQuobyteAPISecretNamespace: adminSecretNamespace, + }) + + return pv, nil +} + +func (deleter *quobyteVolumeDeleter) GetPath() string { + return deleter.quobyte.GetPath() +} + +func (deleter *quobyteVolumeDeleter) Delete() error { + var quobyteUser, quobytePassword string + annotations, err := util.ParseVolumeAnnotations(deleter.pv, []string{ + annotationQuobyteAPISecret, + annotationQuobyteAPISecretNamespace, + annotationQuobyteAPIServer}) + + if err != nil { + return err + } + + secretMap, err := util.GetSecret( + annotations[annotationQuobyteAPISecretNamespace], + annotations[annotationQuobyteAPISecret], + deleter.plugin.host.GetKubeClient()) + + if err != nil { + return err + } + + var ok bool + if quobyteUser, ok = secretMap["user"]; !ok { + return fmt.Errorf("Missing \"user\" in secret") + } + + if quobytePassword, ok = secretMap["password"]; !ok { + return fmt.Errorf("Missing \"password\" in secret") + } + + manager := &quobyteVolumeManager{ + config: &quobyteAPIConfig{ + quobyteUser: quobyteUser, + quobytePassword: quobytePassword, + quobyteAPIServer: annotations[annotationQuobyteAPIServer], + }, + } + return manager.deleteVolume(deleter) +} diff --git a/pkg/volume/quobyte/quobyte_util.go b/pkg/volume/quobyte/quobyte_util.go index c26febe1a05..4c742309bfe 100644 --- a/pkg/volume/quobyte/quobyte_util.go +++ b/pkg/volume/quobyte/quobyte_util.go @@ -17,12 +17,58 @@ limitations under the License. package quobyte import ( + "net" "path" "strings" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/volume" + "github.com/golang/glog" + quobyte_api "github.com/quobyte/api" ) +type quobyteVolumeManager struct { + config *quobyteAPIConfig +} + +func (manager *quobyteVolumeManager) createVolume(provisioner *quobyteVolumeProvisioner) (quobyte *api.QuobyteVolumeSource, size int, err error) { + volumeSize := int(volume.RoundUpSize(provisioner.options.Capacity.Value(), 1024*1024*1024)) + // Quobyte has the concept of Volumes which doen't have a specific size (they can grow unlimited) + // to simulate a size constraint we could set here a Quota + volumeRequest := &quobyte_api.CreateVolumeRequest{ + Name: provisioner.volume, + RootUserID: provisioner.user, + RootGroupID: provisioner.group, + TenantID: provisioner.tenant, + ConfigurationName: provisioner.config, + } + + if _, err := manager.createQuobyteClient().CreateVolume(volumeRequest); err != nil { + return &api.QuobyteVolumeSource{}, volumeSize, err + } + + glog.V(4).Infof("Created Quobyte volume %s", provisioner.volume) + return &api.QuobyteVolumeSource{ + Registry: provisioner.registry, + Volume: provisioner.volume, + User: provisioner.user, + Group: provisioner.group, + }, volumeSize, nil +} + +func (manager *quobyteVolumeManager) deleteVolume(deleter *quobyteVolumeDeleter) error { + return manager.createQuobyteClient().DeleteVolumeByName(deleter.volume, deleter.tenant) +} + +func (manager *quobyteVolumeManager) createQuobyteClient() *quobyte_api.QuobyteClient { + return quobyte_api.NewQuobyteClient( + manager.config.quobyteAPIServer, + manager.config.quobyteUser, + manager.config.quobytePassword, + ) +} + func (mounter *quobyteMounter) pluginDirIsMounted(pluginDir string) (bool, error) { mounts, err := mounter.mounter.List() if err != nil { @@ -46,3 +92,17 @@ func (mounter *quobyteMounter) pluginDirIsMounted(pluginDir string) (bool, error func (mounter *quobyteMounter) correctTraillingSlash(regStr string) string { return path.Clean(regStr) + "/" } + +func validateRegistry(registry string) bool { + if len(registry) == 0 { + return false + } + + for _, hostPortPair := range strings.Split(registry, ",") { + if _, _, err := net.SplitHostPort(hostPortPair); err != nil { + return false + } + } + + return true +} diff --git a/pkg/volume/util/util.go b/pkg/volume/util/util.go index c607cf6d99e..7e01b5c581b 100644 --- a/pkg/volume/util/util.go +++ b/pkg/volume/util/util.go @@ -22,6 +22,7 @@ import ( "path" "github.com/golang/glog" + "k8s.io/kubernetes/pkg/api" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" "k8s.io/kubernetes/pkg/util/mount" ) @@ -126,3 +127,33 @@ func GetSecret(namespace, secretName string, kubeClient clientset.Interface) (ma } return secret, nil } + +// AddVolumeAnnotations adds a golang Map as annotation to a PersistentVolume +func AddVolumeAnnotations(pv *api.PersistentVolume, annotations map[string]string) { + if pv.Annotations == nil { + pv.Annotations = map[string]string{} + } + + for k, v := range annotations { + pv.Annotations[k] = v + } +} + +// ParseVolumeAnnotations reads the defined annoations from a PersistentVolume +func ParseVolumeAnnotations(pv *api.PersistentVolume, parseAnnotations []string) (map[string]string, error) { + result := map[string]string{} + + if pv.Annotations == nil { + return result, fmt.Errorf("cannot parse volume annotations: no annotations found") + } + + for _, annotation := range parseAnnotations { + if val, ok := pv.Annotations[annotation]; ok { + result[annotation] = val + } else { + return result, fmt.Errorf("cannot parse volume annotations: annotation %s not found", annotation) + } + } + + return result, nil +} diff --git a/vendor/github.com/quobyte/api/LICENSE b/vendor/github.com/quobyte/api/LICENSE new file mode 100644 index 00000000000..bac2de07540 --- /dev/null +++ b/vendor/github.com/quobyte/api/LICENSE @@ -0,0 +1,28 @@ + +Copyright (c) 2016, Quobyte Inc. +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. + +* Neither the name of quobyte-automation nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +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. diff --git a/vendor/github.com/quobyte/api/README.md b/vendor/github.com/quobyte/api/README.md new file mode 100644 index 00000000000..7ed01f10d2c --- /dev/null +++ b/vendor/github.com/quobyte/api/README.md @@ -0,0 +1,35 @@ +# Quobyte API Clients + +Get the quoybte api client + +```bash +go get github.com/quobyte/api +``` + +## Usage + +```go +package main + +import ( + "log" + quobyte_api "github.com/quobyte/api" +) + +func main() { + client := quobyte_api.NewQuobyteClient("http://apiserver:7860", "user", "password") + req := &quobyte_api.CreateVolumeRequest{ + Name: "MyVolume", + RootUserID: "root", + RootGroupID: "root", + ConfigurationName: "base", + } + + volume_uuid, err := client.CreateVolume(req) + if err != nil { + log.Fatalf("Error:", err) + } + + log.Printf("%s", volume_uuid) +} +``` diff --git a/vendor/github.com/quobyte/api/quobyte.go b/vendor/github.com/quobyte/api/quobyte.go new file mode 100644 index 00000000000..88f7c848279 --- /dev/null +++ b/vendor/github.com/quobyte/api/quobyte.go @@ -0,0 +1,79 @@ +// Package quobyte represents a golang API for the Quobyte Storage System +package quobyte + +import "net/http" + +type QuobyteClient struct { + client *http.Client + url string + username string + password string +} + +// NewQuobyteClient creates a new Quobyte API client +func NewQuobyteClient(url string, username string, password string) *QuobyteClient { + return &QuobyteClient{ + client: &http.Client{}, + url: url, + username: username, + password: password, + } +} + +// CreateVolume creates a new Quobyte volume. Its root directory will be owned by given user and group +func (client QuobyteClient) CreateVolume(request *CreateVolumeRequest) (string, error) { + var response volumeUUID + if err := client.sendRequest("createVolume", request, &response); err != nil { + return "", err + } + + return response.VolumeUUID, nil +} + +// ResolveVolumeNameToUUID resolves a volume name to a UUID +func (client *QuobyteClient) ResolveVolumeNameToUUID(volumeName, tenant string) (string, error) { + request := &resolveVolumeNameRequest{ + VolumeName: volumeName, + TenantDomain: tenant, + } + var response volumeUUID + if err := client.sendRequest("resolveVolumeName", request, &response); err != nil { + return "", err + } + + return response.VolumeUUID, nil +} + +// DeleteVolume deletes a Quobyte volume +func (client *QuobyteClient) DeleteVolume(UUID string) error { + return client.sendRequest( + "deleteVolume", + &volumeUUID{ + VolumeUUID: UUID, + }, + nil) +} + +// DeleteVolumeByName deletes a volume by a given name +func (client *QuobyteClient) DeleteVolumeByName(volumeName, tenant string) error { + uuid, err := client.ResolveVolumeNameToUUID(volumeName, tenant) + if err != nil { + return err + } + + return client.DeleteVolume(uuid) +} + +// GetClientList returns a list of all active clients +func (client *QuobyteClient) GetClientList(tenant string) (GetClientListResponse, error) { + request := &getClientListRequest{ + TenantDomain: tenant, + } + + var response GetClientListResponse + if err := client.sendRequest("getClientListRequest", request, &response); err != nil { + return response, err + } + + return response, nil +} diff --git a/vendor/github.com/quobyte/api/rpc_client.go b/vendor/github.com/quobyte/api/rpc_client.go new file mode 100644 index 00000000000..7c7326c57fa --- /dev/null +++ b/vendor/github.com/quobyte/api/rpc_client.go @@ -0,0 +1,108 @@ +package quobyte + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "math/rand" + "net/http" + "strconv" +) + +const ( + emptyResponse string = "Empty result and no error occured" +) + +type request struct { + ID string `json:"id"` + Version string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params"` +} + +type response struct { + ID string `json:"id"` + Version string `json:"jsonrpc"` + Result *json.RawMessage `json:"result"` + Error *json.RawMessage `json:"error"` +} + +type rpcError struct { + Code int64 `json:"code"` + Message string `json:"message"` +} + +func (err *rpcError) decodeErrorCode() string { + switch err.Code { + case -32600: + return "ERROR_CODE_INVALID_REQUEST" + case -32603: + return "ERROR_CODE_JSON_ENCODING_FAILED" + case -32601: + return "ERROR_CODE_METHOD_NOT_FOUND" + case -32700: + return "ERROR_CODE_PARSE_ERROR" + } + + return "" +} + +func encodeRequest(method string, params interface{}) ([]byte, error) { + return json.Marshal(&request{ + // Generate random ID and convert it to a string + ID: strconv.FormatInt(rand.Int63(), 10), + Version: "2.0", + Method: method, + Params: params, + }) +} + +func decodeResponse(ioReader io.Reader, reply interface{}) error { + var resp response + if err := json.NewDecoder(ioReader).Decode(&resp); err != nil { + return err + } + + if resp.Error != nil { + var rpcErr rpcError + if err := json.Unmarshal(*resp.Error, &rpcErr); err != nil { + return err + } + + if rpcErr.Message != "" { + return errors.New(rpcErr.Message) + } + + respError := rpcErr.decodeErrorCode() + if respError != "" { + return errors.New(respError) + } + } + + if resp.Result != nil && reply != nil { + return json.Unmarshal(*resp.Result, reply) + } + + return errors.New(emptyResponse) +} + +func (client QuobyteClient) sendRequest(method string, request interface{}, response interface{}) error { + message, err := encodeRequest(method, request) + if err != nil { + return err + } + req, err := http.NewRequest("POST", client.url, bytes.NewBuffer(message)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.SetBasicAuth(client.username, client.password) + resp, err := client.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + return decodeResponse(resp.Body, &response) +} diff --git a/vendor/github.com/quobyte/api/types.go b/vendor/github.com/quobyte/api/types.go new file mode 100644 index 00000000000..2ed5698aa1e --- /dev/null +++ b/vendor/github.com/quobyte/api/types.go @@ -0,0 +1,34 @@ +package quobyte + +// CreateVolumeRequest represents a CreateVolumeRequest +type CreateVolumeRequest struct { + Name string `json:"name,omitempty"` + RootUserID string `json:"root_user_id,omitempty"` + RootGroupID string `json:"root_group_id,omitempty"` + ReplicaDeviceIDS []uint64 `json:"replica_device_ids,string,omitempty"` + ConfigurationName string `json:"configuration_name,omitempty"` + AccessMode uint32 `json:"access_mode,string,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +type resolveVolumeNameRequest struct { + VolumeName string `json:"volume_name,omitempty"` + TenantDomain string `json:"tenant_domain,omitempty"` +} + +type volumeUUID struct { + VolumeUUID string `json:"volume_uuid,omitempty"` +} + +type getClientListRequest struct { + TenantDomain string `json:"tenant_domain,omitempty"` +} + +type GetClientListResponse struct { + Clients []Client `json:"client,omitempty"` +} + +type Client struct { + MountedUserName string `json:"mount_user_name,omitempty"` + MountedVolumeUUID string `json:"mounted_volume_uuid,omitempty"` +}