mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 09:22:44 +00:00
Merge pull request #1337 from brendandburns/Sarsate-pd-support
Taking over PD support from sarsate
This commit is contained in:
commit
aa30423dc9
@ -28,6 +28,6 @@ MASTER_TAG="${INSTANCE_PREFIX}-master"
|
|||||||
MINION_TAG="${INSTANCE_PREFIX}-minion"
|
MINION_TAG="${INSTANCE_PREFIX}-minion"
|
||||||
MINION_NAMES=($(eval echo ${INSTANCE_PREFIX}-minion-{1..${NUM_MINIONS}}))
|
MINION_NAMES=($(eval echo ${INSTANCE_PREFIX}-minion-{1..${NUM_MINIONS}}))
|
||||||
MINION_IP_RANGES=($(eval echo "10.244.{1..${NUM_MINIONS}}.0/24"))
|
MINION_IP_RANGES=($(eval echo "10.244.{1..${NUM_MINIONS}}.0/24"))
|
||||||
MINION_SCOPES=""
|
MINION_SCOPES="compute-rw"
|
||||||
# Increase the sleep interval value if concerned about API rate limits. 3, in seconds, is the default.
|
# Increase the sleep interval value if concerned about API rate limits. 3, in seconds, is the default.
|
||||||
POLL_SLEEP_INTERVAL=3
|
POLL_SLEEP_INTERVAL=3
|
||||||
|
@ -92,6 +92,9 @@ type VolumeSource struct {
|
|||||||
HostDir *HostDir `yaml:"hostDir" json:"hostDir"`
|
HostDir *HostDir `yaml:"hostDir" json:"hostDir"`
|
||||||
// EmptyDir represents a temporary directory that shares a pod's lifetime.
|
// EmptyDir represents a temporary directory that shares a pod's lifetime.
|
||||||
EmptyDir *EmptyDir `yaml:"emptyDir" json:"emptyDir"`
|
EmptyDir *EmptyDir `yaml:"emptyDir" json:"emptyDir"`
|
||||||
|
// GCEPersistentDisk represents a GCE Disk resource that is attached to a
|
||||||
|
// kubelet's host machine and then exposed to the pod.
|
||||||
|
GCEPersistentDisk *GCEPersistentDisk `yaml:"persistentDisk" json:"persistentDisk"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostDir represents bare host directory volume.
|
// HostDir represents bare host directory volume.
|
||||||
@ -111,7 +114,28 @@ const (
|
|||||||
ProtocolUDP Protocol = "UDP"
|
ProtocolUDP Protocol = "UDP"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Port represents a network port in a single container.
|
// GCEPersistent Disk resource.
|
||||||
|
// A GCE PD must exist and be formatted before mounting to a container.
|
||||||
|
// The disk must also be in the same GCE project and zone as the kubelet.
|
||||||
|
// A GCE PD can only be mounted as read/write once.
|
||||||
|
type GCEPersistentDisk struct {
|
||||||
|
// Unique name of the PD resource. Used to identify the disk in GCE
|
||||||
|
PDName string `yaml:"pdName" json:"pdName"`
|
||||||
|
// Required: Filesystem type to mount.
|
||||||
|
// Must be a filesystem type supported by the host operating system.
|
||||||
|
// Ex. "ext4", "xfs", "ntfs"
|
||||||
|
// TODO: how do we prevent errors in the filesystem from compromising the machine
|
||||||
|
FSType string `yaml:"fsType,omitempty" json:"fsType,omitempty"`
|
||||||
|
// Optional: Partition on the disk to mount.
|
||||||
|
// If omitted, kubelet will attempt to mount the device name.
|
||||||
|
// Ex. For /dev/sda1, this field is "1", for /dev/sda, this field is 0 or empty.
|
||||||
|
Partition int `yaml:"partition,omitempty" json:"partition,omitempty"`
|
||||||
|
// Optional: Defaults to false (read/write). ReadOnly here will force
|
||||||
|
// the ReadOnly setting in VolumeMounts.
|
||||||
|
ReadOnly bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port represents a network port in a single container
|
||||||
type Port struct {
|
type Port struct {
|
||||||
// Optional: If specified, this must be a DNS_LABEL. Each named port
|
// Optional: If specified, this must be a DNS_LABEL. Each named port
|
||||||
// in a pod must have a unique name.
|
// in a pod must have a unique name.
|
||||||
|
@ -90,6 +90,9 @@ type VolumeSource struct {
|
|||||||
HostDir *HostDir `yaml:"hostDir" json:"hostDir"`
|
HostDir *HostDir `yaml:"hostDir" json:"hostDir"`
|
||||||
// EmptyDir represents a temporary directory that shares a pod's lifetime.
|
// EmptyDir represents a temporary directory that shares a pod's lifetime.
|
||||||
EmptyDir *EmptyDir `yaml:"emptyDir" json:"emptyDir"`
|
EmptyDir *EmptyDir `yaml:"emptyDir" json:"emptyDir"`
|
||||||
|
// GCEPersistentDisk represents a GCE Disk resource that is attached to a
|
||||||
|
// kubelet's host machine and then exposed to the pod.
|
||||||
|
GCEPersistentDisk *GCEPersistentDisk `yaml:"persistentDisk" json:"persistentDisk"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostDir represents bare host directory volume.
|
// HostDir represents bare host directory volume.
|
||||||
@ -109,7 +112,28 @@ const (
|
|||||||
ProtocolUDP Protocol = "UDP"
|
ProtocolUDP Protocol = "UDP"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Port represents a network port in a single container.
|
// GCEPersistent Disk resource.
|
||||||
|
// A GCE PD must exist before mounting to a container. The disk must
|
||||||
|
// also be in the same GCE project and zone as the kubelet.
|
||||||
|
// A GCE PD can only be mounted as read/write once.
|
||||||
|
type GCEPersistentDisk struct {
|
||||||
|
// Unique name of the PD resource. Used to identify the disk in GCE
|
||||||
|
PDName string `yaml:"pdName" json:"pdName"`
|
||||||
|
// Required: Filesystem type to mount.
|
||||||
|
// Must be a filesystem type supported by the host operating system.
|
||||||
|
// Ex. "ext4", "xfs", "ntfs"
|
||||||
|
// TODO: how do we prevent errors in the filesystem from compromising the machine
|
||||||
|
FSType string `yaml:"fsType,omitempty" json:"fsType,omitempty"`
|
||||||
|
// Optional: Partition on the disk to mount.
|
||||||
|
// If omitted, kubelet will attempt to mount the device name.
|
||||||
|
// Ex. For /dev/sda1, this field is "1", for /dev/sda, this field 0 or empty.
|
||||||
|
Partition int `yaml:"partition,omitempty" json:"partition,omitempty"`
|
||||||
|
// Optional: Defaults to false (read/write). ReadOnly here will force
|
||||||
|
// the ReadOnly setting in VolumeMounts.
|
||||||
|
ReadOnly bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port represents a network port in a single container
|
||||||
type Port struct {
|
type Port struct {
|
||||||
// Optional: If specified, this must be a DNS_LABEL. Each named port
|
// Optional: If specified, this must be a DNS_LABEL. Each named port
|
||||||
// in a pod must have a unique name.
|
// in a pod must have a unique name.
|
||||||
|
@ -90,6 +90,9 @@ type VolumeSource struct {
|
|||||||
HostDir *HostDir `yaml:"hostDir" json:"hostDir"`
|
HostDir *HostDir `yaml:"hostDir" json:"hostDir"`
|
||||||
// EmptyDir represents a temporary directory that shares a pod's lifetime.
|
// EmptyDir represents a temporary directory that shares a pod's lifetime.
|
||||||
EmptyDir *EmptyDir `yaml:"emptyDir" json:"emptyDir"`
|
EmptyDir *EmptyDir `yaml:"emptyDir" json:"emptyDir"`
|
||||||
|
// A persistent disk that is mounted to the
|
||||||
|
// kubelet's host machine and then exposed to the pod.
|
||||||
|
GCEPersistentDisk *GCEPersistentDisk `yaml:"persistentDisk" json:"persistentDisk"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostDir represents bare host directory volume.
|
// HostDir represents bare host directory volume.
|
||||||
@ -124,6 +127,27 @@ type Port struct {
|
|||||||
HostIP string `yaml:"hostIP,omitempty" json:"hostIP,omitempty"`
|
HostIP string `yaml:"hostIP,omitempty" json:"hostIP,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GCEPersistent Disk resource.
|
||||||
|
// A GCE PD must exist before mounting to a container. The disk must
|
||||||
|
// also be in the same GCE project and zone as the kubelet.
|
||||||
|
// A GCE PD can only be mounted as read/write once.
|
||||||
|
type GCEPersistentDisk struct {
|
||||||
|
// Unique name of the PD resource. Used to identify the disk in GCE
|
||||||
|
PDName string `yaml:"pdName" json:"pdName"`
|
||||||
|
// Required: Filesystem type to mount.
|
||||||
|
// Must be a filesystem type supported by the host operating system.
|
||||||
|
// Ex. "ext4", "xfs", "ntfs"
|
||||||
|
// TODO: how do we prevent errors in the filesystem from compromising the machine
|
||||||
|
FSType string `yaml:"fsType,omitempty" json:"fsType,omitempty"`
|
||||||
|
// Optional: Partition on the disk to mount.
|
||||||
|
// If omitted, kubelet will attempt to mount the device name.
|
||||||
|
// Ex. For /dev/sda1, this field is "1", for /dev/sda, this field 0 or empty.
|
||||||
|
Partition int `yaml:"partition,omitempty" json:"partition,omitempty"`
|
||||||
|
// Optional: Defaults to false (read/write). ReadOnly here will force
|
||||||
|
// the ReadOnly setting in VolumeMounts.
|
||||||
|
ReadOnly bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// VolumeMount describes a mounting of a Volume within a container.
|
// VolumeMount describes a mounting of a Volume within a container.
|
||||||
type VolumeMount struct {
|
type VolumeMount struct {
|
||||||
// Required: This must match the Name of a Volume [above].
|
// Required: This must match the Name of a Volume [above].
|
||||||
|
@ -175,6 +175,9 @@ type VolumeSource struct {
|
|||||||
HostDir *HostDir `json:"hostDir" yaml:"hostDir"`
|
HostDir *HostDir `json:"hostDir" yaml:"hostDir"`
|
||||||
// EmptyDir represents a temporary directory that shares a pod's lifetime.
|
// EmptyDir represents a temporary directory that shares a pod's lifetime.
|
||||||
EmptyDir *EmptyDir `json:"emptyDir" yaml:"emptyDir"`
|
EmptyDir *EmptyDir `json:"emptyDir" yaml:"emptyDir"`
|
||||||
|
// GCEPersistentDisk represents a GCE Disk resource that is attached to a
|
||||||
|
// kubelet's host machine and then exposed to the pod.
|
||||||
|
GCEPersistentDisk *GCEPersistentDisk `yaml:"persistentDisk" json:"persistentDisk"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostDir represents bare host directory volume.
|
// HostDir represents bare host directory volume.
|
||||||
@ -194,6 +197,27 @@ const (
|
|||||||
ProtocolUDP Protocol = "UDP"
|
ProtocolUDP Protocol = "UDP"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GCEPersistent Disk resource.
|
||||||
|
// A GCE PD must exist and be formatted before mounting to a container.
|
||||||
|
// The disk must also be in the same GCE project and zone as the kubelet.
|
||||||
|
// A GCE PD can only be mounted as read/write once.
|
||||||
|
type GCEPersistentDisk struct {
|
||||||
|
// Unique name of the PD resource. Used to identify the disk in GCE
|
||||||
|
PDName string `yaml:"pdName" json:"pdName"`
|
||||||
|
// Required: Filesystem type to mount.
|
||||||
|
// Must be a filesystem type supported by the host operating system.
|
||||||
|
// Ex. "ext4", "xfs", "ntfs"
|
||||||
|
// TODO: how do we prevent errors in the filesystem from compromising the machine
|
||||||
|
FSType string `yaml:"fsType,omitempty" json:"fsType,omitempty"`
|
||||||
|
// Optional: Partition on the disk to mount.
|
||||||
|
// If omitted, kubelet will attempt to mount the device name.
|
||||||
|
// Ex. For /dev/sda1, this field is "1", for /dev/sda, this field is 0 or empty.
|
||||||
|
Partition int `yaml:"partition,omitempty" json:"partition,omitempty"`
|
||||||
|
// Optional: Defaults to false (read/write). ReadOnly here will force
|
||||||
|
// the ReadOnly setting in VolumeMounts.
|
||||||
|
ReadOnly bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Port represents a network port in a single container.
|
// Port represents a network port in a single container.
|
||||||
type Port struct {
|
type Port struct {
|
||||||
// Optional: If specified, this must be a DNS_LABEL. Each named port
|
// Optional: If specified, this must be a DNS_LABEL. Each named port
|
||||||
|
@ -64,6 +64,10 @@ func validateSource(source *api.VolumeSource) errs.ErrorList {
|
|||||||
numVolumes++
|
numVolumes++
|
||||||
//EmptyDirs have nothing to validate
|
//EmptyDirs have nothing to validate
|
||||||
}
|
}
|
||||||
|
if source.GCEPersistentDisk != nil {
|
||||||
|
numVolumes++
|
||||||
|
allErrs = append(allErrs, validateGCEPersistentDisk(source.GCEPersistentDisk)...)
|
||||||
|
}
|
||||||
if numVolumes != 1 {
|
if numVolumes != 1 {
|
||||||
allErrs = append(allErrs, errs.NewFieldInvalid("", source))
|
allErrs = append(allErrs, errs.NewFieldInvalid("", source))
|
||||||
}
|
}
|
||||||
@ -80,6 +84,20 @@ func validateHostDir(hostDir *api.HostDir) errs.ErrorList {
|
|||||||
|
|
||||||
var supportedPortProtocols = util.NewStringSet(string(api.ProtocolTCP), string(api.ProtocolUDP))
|
var supportedPortProtocols = util.NewStringSet(string(api.ProtocolTCP), string(api.ProtocolUDP))
|
||||||
|
|
||||||
|
func validateGCEPersistentDisk(PD *api.GCEPersistentDisk) errs.ErrorList {
|
||||||
|
allErrs := errs.ErrorList{}
|
||||||
|
if PD.PDName == "" {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("PD.PDName", PD.PDName))
|
||||||
|
}
|
||||||
|
if PD.FSType == "" {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("PD.FSType", PD.FSType))
|
||||||
|
}
|
||||||
|
if PD.Partition < 0 || PD.Partition > 255 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("PD.Partition", PD.Partition))
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
func validatePorts(ports []api.Port) errs.ErrorList {
|
func validatePorts(ports []api.Port) errs.ErrorList {
|
||||||
allErrs := errs.ErrorList{}
|
allErrs := errs.ErrorList{}
|
||||||
|
|
||||||
@ -373,5 +391,17 @@ func ValidateReplicationControllerState(state *api.ReplicationControllerState) e
|
|||||||
allErrs = append(allErrs, errs.NewFieldInvalid("replicas", state.Replicas))
|
allErrs = append(allErrs, errs.NewFieldInvalid("replicas", state.Replicas))
|
||||||
}
|
}
|
||||||
allErrs = append(allErrs, ValidateManifest(&state.PodTemplate.DesiredState.Manifest).Prefix("podTemplate.desiredState.manifest")...)
|
allErrs = append(allErrs, ValidateManifest(&state.PodTemplate.DesiredState.Manifest).Prefix("podTemplate.desiredState.manifest")...)
|
||||||
|
allErrs = append(allErrs, ValidateReadOnlyPersistentDisks(state.PodTemplate.DesiredState.Manifest.Volumes).Prefix("podTemplate.desiredState.manifest")...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
func ValidateReadOnlyPersistentDisks(volumes []api.Volume) errs.ErrorList {
|
||||||
|
allErrs := errs.ErrorList{}
|
||||||
|
for _, vol := range volumes {
|
||||||
|
if vol.Source.GCEPersistentDisk != nil {
|
||||||
|
if vol.Source.GCEPersistentDisk.ReadOnly == false {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("GCEPersistentDisk.ReadOnly", false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
@ -40,12 +40,13 @@ func TestValidateVolumes(t *testing.T) {
|
|||||||
{Name: "123", Source: &api.VolumeSource{HostDir: &api.HostDir{"/mnt/path2"}}},
|
{Name: "123", Source: &api.VolumeSource{HostDir: &api.HostDir{"/mnt/path2"}}},
|
||||||
{Name: "abc-123", Source: &api.VolumeSource{HostDir: &api.HostDir{"/mnt/path3"}}},
|
{Name: "abc-123", Source: &api.VolumeSource{HostDir: &api.HostDir{"/mnt/path3"}}},
|
||||||
{Name: "empty", Source: &api.VolumeSource{EmptyDir: &api.EmptyDir{}}},
|
{Name: "empty", Source: &api.VolumeSource{EmptyDir: &api.EmptyDir{}}},
|
||||||
|
{Name: "gcepd", Source: &api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDisk{"my-PD", "ext4", 1, false}}},
|
||||||
}
|
}
|
||||||
names, errs := validateVolumes(successCase)
|
names, errs := validateVolumes(successCase)
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
t.Errorf("expected success: %v", errs)
|
t.Errorf("expected success: %v", errs)
|
||||||
}
|
}
|
||||||
if len(names) != 4 || !names.HasAll("abc", "123", "abc-123", "empty") {
|
if len(names) != 5 || !names.HasAll("abc", "123", "abc-123", "empty", "gcepd") {
|
||||||
t.Errorf("wrong names result: %v", names)
|
t.Errorf("wrong names result: %v", names)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,7 +553,14 @@ func TestValidateReplicationController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Labels: validSelector,
|
Labels: validSelector,
|
||||||
}
|
}
|
||||||
|
invalidVolumePodTemplate := api.PodTemplate{
|
||||||
|
DesiredState: api.PodState{
|
||||||
|
Manifest: api.ContainerManifest{
|
||||||
|
Version: "v1beta1",
|
||||||
|
Volumes: []api.Volume{{Name: "gcepd", Source: &api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDisk{"my-PD", "ext4", 1, false}}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
successCases := []api.ReplicationController{
|
successCases := []api.ReplicationController{
|
||||||
{
|
{
|
||||||
TypeMeta: api.TypeMeta{ID: "abc", Namespace: api.NamespaceDefault},
|
TypeMeta: api.TypeMeta{ID: "abc", Namespace: api.NamespaceDefault},
|
||||||
@ -609,6 +617,13 @@ func TestValidateReplicationController(t *testing.T) {
|
|||||||
ReplicaSelector: validSelector,
|
ReplicaSelector: validSelector,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"read-write presistent disk": {
|
||||||
|
TypeMeta: api.TypeMeta{ID: "abc"},
|
||||||
|
DesiredState: api.ReplicationControllerState{
|
||||||
|
ReplicaSelector: validSelector,
|
||||||
|
PodTemplate: invalidVolumePodTemplate,
|
||||||
|
},
|
||||||
|
},
|
||||||
"negative_replicas": {
|
"negative_replicas": {
|
||||||
TypeMeta: api.TypeMeta{ID: "abc", Namespace: api.NamespaceDefault},
|
TypeMeta: api.TypeMeta{ID: "abc", Namespace: api.NamespaceDefault},
|
||||||
DesiredState: api.ReplicationControllerState{
|
DesiredState: api.ReplicationControllerState{
|
||||||
@ -628,6 +643,7 @@ func TestValidateReplicationController(t *testing.T) {
|
|||||||
field != "id" &&
|
field != "id" &&
|
||||||
field != "namespace" &&
|
field != "namespace" &&
|
||||||
field != "desiredState.replicaSelector" &&
|
field != "desiredState.replicaSelector" &&
|
||||||
|
field != "GCEPersistentDisk.ReadOnly" &&
|
||||||
field != "desiredState.replicas" {
|
field != "desiredState.replicas" {
|
||||||
t.Errorf("%s: missing prefix for: %v", k, errs[i])
|
t.Errorf("%s: missing prefix for: %v", k, errs[i])
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -41,43 +42,71 @@ type GCECloud struct {
|
|||||||
service *compute.Service
|
service *compute.Service
|
||||||
projectID string
|
projectID string
|
||||||
zone string
|
zone string
|
||||||
instanceRE string
|
instanceID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cloudprovider.RegisterCloudProvider("gce", func(config io.Reader) (cloudprovider.Interface, error) { return newGCECloud() })
|
cloudprovider.RegisterCloudProvider("gce", func(config io.Reader) (cloudprovider.Interface, error) { return newGCECloud() })
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProjectAndZone() (string, string, error) {
|
func getMetadata(url string) (string, error) {
|
||||||
client := http.Client{}
|
client := http.Client{}
|
||||||
url := "http://metadata/computeMetadata/v1/instance/zone"
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", err
|
||||||
}
|
}
|
||||||
req.Header.Add("X-Google-Metadata-Request", "True")
|
req.Header.Add("X-Google-Metadata-Request", "True")
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
data, err := ioutil.ReadAll(res.Body)
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProjectAndZone() (string, string, error) {
|
||||||
|
url := "http://metadata/computeMetadata/v1/instance/zone"
|
||||||
|
result, err := getMetadata(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
parts := strings.Split(string(data), "/")
|
parts := strings.Split(result, "/")
|
||||||
if len(parts) != 4 {
|
if len(parts) != 4 {
|
||||||
return "", "", fmt.Errorf("Unexpected response: %s", string(data))
|
return "", "", fmt.Errorf("Unexpected response: %s", result)
|
||||||
}
|
}
|
||||||
return parts[1], parts[3], nil
|
return parts[1], parts[3], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getInstanceID() (string, error) {
|
||||||
|
url := "http://metadata/computeMetadata/v1/instance/hostname"
|
||||||
|
result, err := getMetadata(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
parts := strings.Split(result, ".")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "", fmt.Errorf("Unexpected response: %s", result)
|
||||||
|
}
|
||||||
|
return parts[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
// newGCECloud creates a new instance of GCECloud.
|
// newGCECloud creates a new instance of GCECloud.
|
||||||
func newGCECloud() (*GCECloud, error) {
|
func newGCECloud() (*GCECloud, error) {
|
||||||
projectID, zone, err := getProjectAndZone()
|
projectID, zone, err := getProjectAndZone()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// TODO: if we want to use this on a machine that doesn't have the http://metadata server
|
||||||
|
// e.g. on a user's machine (not VM) somewhere, we need to have an alternative for
|
||||||
|
// instance id lookup.
|
||||||
|
instanceID, err := getInstanceID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
client, err := serviceaccount.NewClient(&serviceaccount.Options{})
|
client, err := serviceaccount.NewClient(&serviceaccount.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -87,9 +116,10 @@ func newGCECloud() (*GCECloud, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &GCECloud{
|
return &GCECloud{
|
||||||
service: svc,
|
service: svc,
|
||||||
projectID: projectID,
|
projectID: projectID,
|
||||||
zone: zone,
|
zone: zone,
|
||||||
|
instanceID: instanceID,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,6 +340,29 @@ func (gce *GCECloud) GetZone() (cloudprovider.Zone, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gce *GCECloud) AttachDisk(diskName string, readOnly bool) error {
|
||||||
|
disk, err := gce.getDisk(diskName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
readWrite := "READ_WRITE"
|
||||||
|
if readOnly {
|
||||||
|
readWrite = "READ_ONLY"
|
||||||
|
}
|
||||||
|
attachedDisk := gce.convertDiskToAttachedDisk(disk, readWrite)
|
||||||
|
_, err = gce.service.Instances.AttachDisk(gce.projectID, gce.zone, gce.instanceID, attachedDisk).Do()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gce *GCECloud) DetachDisk(devicePath string) error {
|
||||||
|
_, err := gce.service.Instances.DetachDisk(gce.projectID, gce.zone, gce.instanceID, devicePath).Do()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gce *GCECloud) getDisk(diskName string) (*compute.Disk, error) {
|
||||||
|
return gce.service.Disks.Get(gce.projectID, gce.zone, diskName).Do()
|
||||||
|
}
|
||||||
|
|
||||||
// getGceRegion returns region of the gce zone. Zone names
|
// getGceRegion returns region of the gce zone. Zone names
|
||||||
// are of the form: ${region-name}-${ix}.
|
// are of the form: ${region-name}-${ix}.
|
||||||
// For example "us-central1-b" has a region of "us-central1".
|
// For example "us-central1-b" has a region of "us-central1".
|
||||||
@ -321,3 +374,14 @@ func getGceRegion(zone string) (string, error) {
|
|||||||
}
|
}
|
||||||
return zone[:ix], nil
|
return zone[:ix], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converts a Disk resource to an AttachedDisk resource.
|
||||||
|
func (gce *GCECloud) convertDiskToAttachedDisk(disk *compute.Disk, readWrite string) *compute.AttachedDisk {
|
||||||
|
return &compute.AttachedDisk{
|
||||||
|
DeviceName: disk.Name,
|
||||||
|
Kind: disk.Kind,
|
||||||
|
Mode: readWrite,
|
||||||
|
Source: "https://" + path.Join("www.googleapis.com/compute/v1/projects/", gce.projectID, "zones", gce.zone, "disks", disk.Name),
|
||||||
|
Type: "PERSISTENT",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
136
pkg/volume/gce_util.go
Normal file
136
pkg/volume/gce_util.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce"
|
||||||
|
)
|
||||||
|
|
||||||
|
const partitionRegex = "[a-z][a-z]*(?P<partition>[0-9][0-9]*)?"
|
||||||
|
|
||||||
|
var regexMatcher = regexp.MustCompile(partitionRegex)
|
||||||
|
|
||||||
|
type GCEDiskUtil struct{}
|
||||||
|
|
||||||
|
// Attaches a disk specified by a volume.GCEPersistentDisk to the current kubelet.
|
||||||
|
// Mounts the disk to it's global path.
|
||||||
|
func (util *GCEDiskUtil) AttachDisk(GCEPD *GCEPersistentDisk) error {
|
||||||
|
gce, err := cloudprovider.GetCloudProvider("gce", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flags := uintptr(0)
|
||||||
|
if GCEPD.ReadOnly {
|
||||||
|
flags = MOUNT_MS_RDONLY
|
||||||
|
}
|
||||||
|
if err := gce.(*gce_cloud.GCECloud).AttachDisk(GCEPD.PDName, GCEPD.ReadOnly); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
devicePath := path.Join("/dev/disk/by-id/", "google-"+GCEPD.PDName)
|
||||||
|
if GCEPD.Partition != "" {
|
||||||
|
devicePath = devicePath + "-part" + GCEPD.Partition
|
||||||
|
}
|
||||||
|
//TODO(jonesdl) There should probably be better method than busy-waiting here.
|
||||||
|
numTries := 0
|
||||||
|
for {
|
||||||
|
_, err := os.Stat(devicePath)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
numTries++
|
||||||
|
if numTries == 10 {
|
||||||
|
return errors.New("Could not attach disk: Timeout after 10s")
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
globalPDPath := makeGlobalPDName(GCEPD.RootDir, GCEPD.PDName, GCEPD.ReadOnly)
|
||||||
|
// Only mount the PD globally once.
|
||||||
|
_, err = os.Stat(globalPDPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(globalPDPath, 0750)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = GCEPD.mounter.Mount(devicePath, globalPDPath, GCEPD.FSType, flags, "")
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(globalPDPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDeviceName(devicePath, canonicalDevicePath string) (string, error) {
|
||||||
|
isMatch := regexMatcher.MatchString(path.Base(canonicalDevicePath))
|
||||||
|
if !isMatch {
|
||||||
|
return "", fmt.Errorf("unexpected device: %s", canonicalDevicePath)
|
||||||
|
}
|
||||||
|
if isMatch {
|
||||||
|
result := make(map[string]string)
|
||||||
|
substrings := regexMatcher.FindStringSubmatch(path.Base(canonicalDevicePath))
|
||||||
|
for i, label := range regexMatcher.SubexpNames() {
|
||||||
|
result[label] = substrings[i]
|
||||||
|
}
|
||||||
|
partition := result["partition"]
|
||||||
|
devicePath = strings.TrimSuffix(devicePath, "-part"+partition)
|
||||||
|
}
|
||||||
|
return strings.TrimPrefix(path.Base(devicePath), "google-"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmounts the device and detaches the disk from the kubelet's host machine.
|
||||||
|
// Expects a GCE device path symlink. Ex: /dev/disk/by-id/google-mydisk-part1
|
||||||
|
func (util *GCEDiskUtil) DetachDisk(GCEPD *GCEPersistentDisk, devicePath string) error {
|
||||||
|
// Follow the symlink to the actual device path.
|
||||||
|
canonicalDevicePath, err := filepath.EvalSymlinks(devicePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deviceName, err := getDeviceName(devicePath, canonicalDevicePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
globalPDPath := makeGlobalPDName(GCEPD.RootDir, deviceName, GCEPD.ReadOnly)
|
||||||
|
if err := GCEPD.mounter.Unmount(globalPDPath, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(globalPDPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gce, err := cloudprovider.GetCloudProvider("gce", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := gce.(*gce_cloud.GCECloud).DetachDisk(deviceName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
55
pkg/volume/gce_util_test.go
Normal file
55
pkg/volume/gce_util_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetDeviceName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
deviceName string
|
||||||
|
canonicalName string
|
||||||
|
expectedName string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
deviceName: "/dev/google-sd0-part0",
|
||||||
|
canonicalName: "/dev/google/sd0P1",
|
||||||
|
expectedName: "sd0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
canonicalName: "0123456",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
name, err := getDeviceName(test.deviceName, test.canonicalName)
|
||||||
|
if test.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("unexpected non-error")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if name != test.expectedName {
|
||||||
|
t.Errorf("expected: %s, got %s", test.expectedName, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
pkg/volume/mounter_linux.go
Normal file
86
pkg/volume/mounter_linux.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package volume
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MOUNT_MS_BIND = syscall.MS_BIND
|
||||||
|
const MOUNT_MS_RDONLY = syscall.MS_RDONLY
|
||||||
|
|
||||||
|
type DiskMounter struct{}
|
||||||
|
|
||||||
|
// Wraps syscall.Mount()
|
||||||
|
func (mounter *DiskMounter) Mount(source string, target string, fstype string, flags uintptr, data string) error {
|
||||||
|
return syscall.Mount(source, target, fstype, flags, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wraps syscall.Unmount()
|
||||||
|
func (mounter *DiskMounter) Unmount(target string, flags int) error {
|
||||||
|
return syscall.Unmount(target, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Examines /proc/mounts to find the source device of the PD resource and the
|
||||||
|
// number of references to that device. Returns both the full device path under
|
||||||
|
// the /dev tree and the number of references.
|
||||||
|
func (mounter *DiskMounter) RefCount(mount Interface) (string, int, error) {
|
||||||
|
// TODO(jonesdl) This can be split up into two procedures, finding the device path
|
||||||
|
// and finding the number of references. The parsing could also be separated and another
|
||||||
|
// utility could determine if a volume's path is an active mount point.
|
||||||
|
file, err := os.Open("/proc/mounts")
|
||||||
|
if err != nil {
|
||||||
|
return "", -1, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
scanner := bufio.NewReader(file)
|
||||||
|
refCount := 0
|
||||||
|
var deviceName string
|
||||||
|
// Find the actual device path.
|
||||||
|
for {
|
||||||
|
line, err := scanner.ReadString('\n')
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
success, err := regexp.MatchString(mount.GetPath(), line)
|
||||||
|
if err != nil {
|
||||||
|
return "", -1, err
|
||||||
|
}
|
||||||
|
if success {
|
||||||
|
deviceName = strings.Split(line, " ")[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
file, err = os.Open("/proc/mounts")
|
||||||
|
scanner.Reset(bufio.NewReader(file))
|
||||||
|
// Find the number of references to the device.
|
||||||
|
for {
|
||||||
|
line, err := scanner.ReadString('\n')
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.Split(line, " ")[0] == deviceName {
|
||||||
|
refCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deviceName, refCount, nil
|
||||||
|
}
|
36
pkg/volume/mounter_unsupported.go
Normal file
36
pkg/volume/mounter_unsupported.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// +build !linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package volume
|
||||||
|
|
||||||
|
const MOUNT_MS_BIND = 0
|
||||||
|
const MOUNT_MS_RDONLY = 0
|
||||||
|
|
||||||
|
type DiskMounter struct{}
|
||||||
|
|
||||||
|
func (mounter *DiskMounter) Mount(source string, target string, fstype string, flags uintptr, data string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mounter *DiskMounter) Unmount(target string, flags int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mounter *DiskMounter) RefCount(PD Interface) (string, int, error) {
|
||||||
|
return "", 0, nil
|
||||||
|
}
|
@ -21,6 +21,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
@ -49,6 +50,22 @@ type Cleaner interface {
|
|||||||
TearDown() error
|
TearDown() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gcePersistentDiskUtil interface {
|
||||||
|
// Attaches the disk to the kubelet's host machine.
|
||||||
|
AttachDisk(PD *GCEPersistentDisk) error
|
||||||
|
// Detaches the disk from the kubelet's host machine.
|
||||||
|
DetachDisk(PD *GCEPersistentDisk, devicePath string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mounters wrap os/system specific calls to perform mounts.
|
||||||
|
type mounter interface {
|
||||||
|
Mount(source string, target string, fstype string, flags uintptr, data string) error
|
||||||
|
Unmount(target string, flags int) error
|
||||||
|
// RefCount returns the device path for the source disk of a volume, and
|
||||||
|
// the number of references to that target disk.
|
||||||
|
RefCount(vol Interface) (string, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
// HostDir volumes represent a bare host directory mount.
|
// HostDir volumes represent a bare host directory mount.
|
||||||
// The directory in Path will be directly exposed to the container.
|
// The directory in Path will be directly exposed to the container.
|
||||||
type HostDir struct {
|
type HostDir struct {
|
||||||
@ -118,11 +135,128 @@ func createHostDir(volume *api.Volume) *HostDir {
|
|||||||
return &HostDir{volume.Source.HostDir.Path}
|
return &HostDir{volume.Source.HostDir.Path}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GCEPersistentDisk volumes are disk resources provided by Google Compute Engine
|
||||||
|
// that are attached to the kubelet's host machine and exposed to the pod.
|
||||||
|
type GCEPersistentDisk struct {
|
||||||
|
Name string
|
||||||
|
PodID string
|
||||||
|
RootDir string
|
||||||
|
// Unique identifier of the PD, used to find the disk resource in the provider.
|
||||||
|
PDName string
|
||||||
|
// Filesystem type, optional.
|
||||||
|
FSType string
|
||||||
|
// Specifies the partition to mount
|
||||||
|
Partition string
|
||||||
|
// Specifies whether the disk will be attached as ReadOnly.
|
||||||
|
ReadOnly bool
|
||||||
|
// Utility interface that provides API calls to the provider to attach/detach disks.
|
||||||
|
util gcePersistentDiskUtil
|
||||||
|
// Mounter interface that provides system calls to mount the disks.
|
||||||
|
mounter mounter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PD *GCEPersistentDisk) GetPath() string {
|
||||||
|
return path.Join(PD.RootDir, PD.PodID, "volumes", "gce-pd", PD.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches the disk and bind mounts to the volume path.
|
||||||
|
func (PD *GCEPersistentDisk) SetUp() error {
|
||||||
|
// TODO: handle failed mounts here.
|
||||||
|
if _, err := os.Stat(PD.GetPath()); !os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := PD.util.AttachDisk(PD)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flags := uintptr(0)
|
||||||
|
if PD.ReadOnly {
|
||||||
|
flags = MOUNT_MS_RDONLY
|
||||||
|
}
|
||||||
|
//Perform a bind mount to the full path to allow duplicate mounts of the same PD.
|
||||||
|
if _, err = os.Stat(PD.GetPath()); os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(PD.GetPath(), 0750)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
globalPDPath := makeGlobalPDName(PD.RootDir, PD.PDName, PD.ReadOnly)
|
||||||
|
err = PD.mounter.Mount(globalPDPath, PD.GetPath(), "", MOUNT_MS_BIND|flags, "")
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(PD.GetPath())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 (PD *GCEPersistentDisk) TearDown() error {
|
||||||
|
devicePath, refCount, err := PD.mounter.RefCount(PD)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := PD.mounter.Unmount(PD.GetPath(), 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
refCount--
|
||||||
|
if err := os.RemoveAll(PD.GetPath()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// If refCount is 1, then all bind mounts have been removed, and the
|
||||||
|
// remaining reference is the global mount. It is safe to detach.
|
||||||
|
if refCount == 1 {
|
||||||
|
if err := PD.util.DetachDisk(PD, devicePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO(jonesdl) prevent name collisions by using designated pod space as well.
|
||||||
|
// Ex. (ROOT_DIR)/pods/...
|
||||||
|
func makeGlobalPDName(rootDir, devName string, readOnly bool) string {
|
||||||
|
var mode string
|
||||||
|
if readOnly {
|
||||||
|
mode = "ro"
|
||||||
|
} else {
|
||||||
|
mode = "rw"
|
||||||
|
}
|
||||||
|
return path.Join(rootDir, "global", "pd", mode, devName)
|
||||||
|
}
|
||||||
|
|
||||||
// createEmptyDir interprets API volume as an EmptyDir.
|
// createEmptyDir interprets API volume as an EmptyDir.
|
||||||
func createEmptyDir(volume *api.Volume, podID string, rootDir string) *EmptyDir {
|
func createEmptyDir(volume *api.Volume, podID string, rootDir string) *EmptyDir {
|
||||||
return &EmptyDir{volume.Name, podID, rootDir}
|
return &EmptyDir{volume.Name, podID, rootDir}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interprets API volume as a PersistentDisk
|
||||||
|
func createGCEPersistentDisk(volume *api.Volume, podID string, rootDir string) (*GCEPersistentDisk, error) {
|
||||||
|
PDName := volume.Source.GCEPersistentDisk.PDName
|
||||||
|
FSType := volume.Source.GCEPersistentDisk.FSType
|
||||||
|
partition := strconv.Itoa(volume.Source.GCEPersistentDisk.Partition)
|
||||||
|
if partition == "0" {
|
||||||
|
partition = ""
|
||||||
|
}
|
||||||
|
readOnly := volume.Source.GCEPersistentDisk.ReadOnly
|
||||||
|
// TODO: move these up into the Kubelet.
|
||||||
|
util := &GCEDiskUtil{}
|
||||||
|
mounter := &DiskMounter{}
|
||||||
|
return &GCEPersistentDisk{
|
||||||
|
Name: volume.Name,
|
||||||
|
PodID: podID,
|
||||||
|
RootDir: rootDir,
|
||||||
|
PDName: PDName,
|
||||||
|
FSType: FSType,
|
||||||
|
Partition: partition,
|
||||||
|
ReadOnly: readOnly,
|
||||||
|
util: util,
|
||||||
|
mounter: mounter}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateVolumeBuilder returns a Builder capable of mounting a volume described by an
|
// CreateVolumeBuilder returns a Builder capable of mounting a volume described by an
|
||||||
// *api.Volume, or an error.
|
// *api.Volume, or an error.
|
||||||
func CreateVolumeBuilder(volume *api.Volume, podID string, rootDir string) (Builder, error) {
|
func CreateVolumeBuilder(volume *api.Volume, podID string, rootDir string) (Builder, error) {
|
||||||
@ -133,12 +267,18 @@ func CreateVolumeBuilder(volume *api.Volume, podID string, rootDir string) (Buil
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var vol Builder
|
var vol Builder
|
||||||
|
var err error
|
||||||
// TODO(jonesdl) We should probably not check every pointer and directly
|
// TODO(jonesdl) We should probably not check every pointer and directly
|
||||||
// resolve these types instead.
|
// resolve these types instead.
|
||||||
if source.HostDir != nil {
|
if source.HostDir != nil {
|
||||||
vol = createHostDir(volume)
|
vol = createHostDir(volume)
|
||||||
} else if source.EmptyDir != nil {
|
} else if source.EmptyDir != nil {
|
||||||
vol = createEmptyDir(volume, podID, rootDir)
|
vol = createEmptyDir(volume, podID, rootDir)
|
||||||
|
} else if source.GCEPersistentDisk != nil {
|
||||||
|
vol, err = createGCEPersistentDisk(volume, podID, rootDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, ErrUnsupportedVolumeType
|
return nil, ErrUnsupportedVolumeType
|
||||||
}
|
}
|
||||||
@ -150,6 +290,13 @@ func CreateVolumeCleaner(kind string, name string, podID string, rootDir string)
|
|||||||
switch kind {
|
switch kind {
|
||||||
case "empty":
|
case "empty":
|
||||||
return &EmptyDir{name, podID, rootDir}, nil
|
return &EmptyDir{name, podID, rootDir}, nil
|
||||||
|
case "gce-pd":
|
||||||
|
return &GCEPersistentDisk{
|
||||||
|
Name: name,
|
||||||
|
PodID: podID,
|
||||||
|
RootDir: rootDir,
|
||||||
|
util: &GCEDiskUtil{},
|
||||||
|
mounter: &DiskMounter{}}, nil
|
||||||
default:
|
default:
|
||||||
return nil, ErrUnsupportedVolumeType
|
return nil, ErrUnsupportedVolumeType
|
||||||
}
|
}
|
||||||
@ -159,10 +306,9 @@ func CreateVolumeCleaner(kind string, name string, podID string, rootDir string)
|
|||||||
// presently active and mounted. Returns a map of Cleaner types.
|
// presently active and mounted. Returns a map of Cleaner types.
|
||||||
func GetCurrentVolumes(rootDirectory string) map[string]Cleaner {
|
func GetCurrentVolumes(rootDirectory string) map[string]Cleaner {
|
||||||
currentVolumes := make(map[string]Cleaner)
|
currentVolumes := make(map[string]Cleaner)
|
||||||
mountPath := rootDirectory
|
podIDDirs, err := ioutil.ReadDir(rootDirectory)
|
||||||
podIDDirs, err := ioutil.ReadDir(mountPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Could not read directory: %s, (%s)", mountPath, err)
|
glog.Errorf("Could not read directory: %s, (%s)", rootDirectory, err)
|
||||||
}
|
}
|
||||||
// Volume information is extracted from the directory structure:
|
// Volume information is extracted from the directory structure:
|
||||||
// (ROOT_DIR)/(POD_ID)/volumes/(VOLUME_KIND)/(VOLUME_NAME)
|
// (ROOT_DIR)/(POD_ID)/volumes/(VOLUME_KIND)/(VOLUME_NAME)
|
||||||
@ -171,7 +317,10 @@ func GetCurrentVolumes(rootDirectory string) map[string]Cleaner {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
podID := podIDDir.Name()
|
podID := podIDDir.Name()
|
||||||
podIDPath := path.Join(mountPath, podID, "volumes")
|
podIDPath := path.Join(rootDirectory, podID, "volumes")
|
||||||
|
if _, err := os.Stat(podIDPath); os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
volumeKindDirs, err := ioutil.ReadDir(podIDPath)
|
volumeKindDirs, err := ioutil.ReadDir(podIDPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Could not read directory: %s, (%s)", podIDPath, err)
|
glog.Errorf("Could not read directory: %s, (%s)", podIDPath, err)
|
||||||
@ -189,7 +338,7 @@ func GetCurrentVolumes(rootDirectory string) map[string]Cleaner {
|
|||||||
// TODO(thockin) This should instead return a reference to an extant volume object
|
// TODO(thockin) This should instead return a reference to an extant volume object
|
||||||
cleaner, err := CreateVolumeCleaner(volumeKind, volumeName, podID, rootDirectory)
|
cleaner, err := CreateVolumeCleaner(volumeKind, volumeName, podID, rootDirectory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Could not create volume cleaner: %s, (%s)", volumeNameDirs, err)
|
glog.Errorf("Could not create volume cleaner: %s, (%s)", volumeNameDir.Name(), err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
currentVolumes[identifier] = cleaner
|
currentVolumes[identifier] = cleaner
|
||||||
|
@ -20,22 +20,52 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateVolumeBuilders(t *testing.T) {
|
type MockDiskUtil struct{}
|
||||||
tempDir, err := ioutil.TempDir("", "CreateVolumes")
|
|
||||||
|
// TODO(jonesdl) To fully test this, we could create a loopback device
|
||||||
|
// and mount that instead.
|
||||||
|
func (util *MockDiskUtil) AttachDisk(PD *GCEPersistentDisk) error {
|
||||||
|
err := os.MkdirAll(path.Join(PD.RootDir, "global", "pd", PD.PDName), 0750)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tempDir)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (util *MockDiskUtil) DetachDisk(PD *GCEPersistentDisk, devicePath string) error {
|
||||||
|
err := os.RemoveAll(path.Join(PD.RootDir, "global", "pd", PD.PDName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockMounter struct{}
|
||||||
|
|
||||||
|
func (mounter *MockMounter) Mount(source string, target string, fstype string, flags uintptr, data string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mounter *MockMounter) Unmount(target string, flags int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mounter *MockMounter) RefCount(vol Interface) (string, int, error) {
|
||||||
|
return "", 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateVolumeBuilders(t *testing.T) {
|
||||||
|
tempDir := "CreateVolumes"
|
||||||
createVolumesTests := []struct {
|
createVolumesTests := []struct {
|
||||||
volume api.Volume
|
volume api.Volume
|
||||||
path string
|
path string
|
||||||
podID string
|
podID string
|
||||||
kind string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
api.Volume{
|
api.Volume{
|
||||||
@ -45,7 +75,6 @@ func TestCreateVolumeBuilders(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"/dir/path",
|
"/dir/path",
|
||||||
"my-id",
|
|
||||||
"",
|
"",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -57,9 +86,18 @@ func TestCreateVolumeBuilders(t *testing.T) {
|
|||||||
},
|
},
|
||||||
path.Join(tempDir, "/my-id/volumes/empty/empty-dir"),
|
path.Join(tempDir, "/my-id/volumes/empty/empty-dir"),
|
||||||
"my-id",
|
"my-id",
|
||||||
"empty",
|
|
||||||
},
|
},
|
||||||
{api.Volume{}, "", "", ""},
|
{
|
||||||
|
api.Volume{
|
||||||
|
Name: "gce-pd",
|
||||||
|
Source: &api.VolumeSource{
|
||||||
|
GCEPersistentDisk: &api.GCEPersistentDisk{"my-disk", "ext4", 0, false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
path.Join(tempDir, "/my-id/volumes/gce-pd/gce-pd"),
|
||||||
|
"my-id",
|
||||||
|
},
|
||||||
|
{api.Volume{}, "", ""},
|
||||||
{
|
{
|
||||||
api.Volume{
|
api.Volume{
|
||||||
Name: "empty-dir",
|
Name: "empty-dir",
|
||||||
@ -67,7 +105,6 @@ func TestCreateVolumeBuilders(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, createVolumesTest := range createVolumesTests {
|
for _, createVolumesTest := range createVolumesTests {
|
||||||
@ -79,7 +116,7 @@ func TestCreateVolumeBuilders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if tt.volume.Source.HostDir == nil && tt.volume.Source.EmptyDir == nil {
|
if tt.volume.Source.HostDir == nil && tt.volume.Source.EmptyDir == nil && tt.volume.Source.GCEPersistentDisk == nil {
|
||||||
if err != ErrUnsupportedVolumeType {
|
if err != ErrUnsupportedVolumeType {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@ -88,22 +125,68 @@ func TestCreateVolumeBuilders(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
err = vb.SetUp()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
path := vb.GetPath()
|
path := vb.GetPath()
|
||||||
if path != tt.path {
|
if path != tt.path {
|
||||||
t.Errorf("Unexpected bind path. Expected %v, got %v", tt.path, path)
|
t.Errorf("Unexpected bind path. Expected %v, got %v", tt.path, path)
|
||||||
}
|
}
|
||||||
vc, err := CreateVolumeCleaner(tt.kind, tt.volume.Name, tt.podID, tempDir)
|
}
|
||||||
if tt.kind == "" {
|
}
|
||||||
if err != ErrUnsupportedVolumeType {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
func TestCreateVolumeCleaners(t *testing.T) {
|
||||||
}
|
tempDir := "CreateVolumeCleaners"
|
||||||
|
createVolumeCleanerTests := []struct {
|
||||||
|
kind string
|
||||||
|
name string
|
||||||
|
podID string
|
||||||
|
}{
|
||||||
|
{"empty", "empty-vol", "my-id"},
|
||||||
|
{"", "", ""},
|
||||||
|
{"gce-pd", "gce-pd-vol", "my-id"},
|
||||||
|
}
|
||||||
|
for _, tt := range createVolumeCleanerTests {
|
||||||
|
vol, err := CreateVolumeCleaner(tt.kind, tt.name, tt.podID, tempDir)
|
||||||
|
if tt.kind == "" && err != nil && vol == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = vc.TearDown()
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error occured: %s", err)
|
||||||
|
}
|
||||||
|
actualKind := reflect.TypeOf(vol).Elem().Name()
|
||||||
|
if tt.kind == "empty" && actualKind != "EmptyDir" {
|
||||||
|
t.Errorf("CreateVolumeCleaner returned invalid type. Expected EmptyDirectory, got %v, %v", tt.kind, actualKind)
|
||||||
|
}
|
||||||
|
if tt.kind == "gce-pd" && actualKind != "GCEPersistentDisk" {
|
||||||
|
t.Errorf("CreateVolumeCleaner returned invalid type. Expected PersistentDisk, got %v, %v", tt.kind, actualKind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetUpAndTearDown(t *testing.T) {
|
||||||
|
tempDir, err := ioutil.TempDir("", "CreateVolumes")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
fakeID := "my-id"
|
||||||
|
type VolumeTester interface {
|
||||||
|
Builder
|
||||||
|
Cleaner
|
||||||
|
}
|
||||||
|
volumes := []VolumeTester{
|
||||||
|
&EmptyDir{"empty", fakeID, tempDir},
|
||||||
|
&GCEPersistentDisk{"pd", fakeID, tempDir, "pd-disk", "ext4", "", false, &MockDiskUtil{}, &MockMounter{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vol := range volumes {
|
||||||
|
err = vol.SetUp()
|
||||||
|
path := vol.GetPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
t.Errorf("SetUp() failed, volume path not created: %v", path)
|
||||||
|
}
|
||||||
|
err = vol.TearDown()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user