Commit 4a7b0eec authored by Tim Hockin's avatar Tim Hockin

Merge pull request #5138 from justinsb/cloud_pd

AWS EBS volume support
parents 111e796b 76269143
...@@ -27,6 +27,7 @@ NUM_MINIONS=${NUM_MINIONS:-4} ...@@ -27,6 +27,7 @@ NUM_MINIONS=${NUM_MINIONS:-4}
AWS_S3_REGION=${AWS_S3_REGION:-us-east-1} AWS_S3_REGION=${AWS_S3_REGION:-us-east-1}
INSTANCE_PREFIX="${KUBE_AWS_INSTANCE_PREFIX:-kubernetes}" INSTANCE_PREFIX="${KUBE_AWS_INSTANCE_PREFIX:-kubernetes}"
CLUSTER_ID=${INSTANCE_PREFIX}
AWS_SSH_KEY=${AWS_SSH_KEY:-$HOME/.ssh/kube_aws_rsa} AWS_SSH_KEY=${AWS_SSH_KEY:-$HOME/.ssh/kube_aws_rsa}
IAM_PROFILE_MASTER="kubernetes-master" IAM_PROFILE_MASTER="kubernetes-master"
IAM_PROFILE_MINION="kubernetes-minion" IAM_PROFILE_MINION="kubernetes-minion"
......
...@@ -23,6 +23,7 @@ NUM_MINIONS=${NUM_MINIONS:-2} ...@@ -23,6 +23,7 @@ NUM_MINIONS=${NUM_MINIONS:-2}
AWS_S3_REGION=${AWS_S3_REGION:-us-east-1} AWS_S3_REGION=${AWS_S3_REGION:-us-east-1}
INSTANCE_PREFIX="${KUBE_AWS_INSTANCE_PREFIX:-e2e-test-${USER}}" INSTANCE_PREFIX="${KUBE_AWS_INSTANCE_PREFIX:-e2e-test-${USER}}"
CLUSTER_ID=${INSTANCE_PREFIX}
AWS_SSH_KEY=${AWS_SSH_KEY:-$HOME/.ssh/kube_aws_rsa} AWS_SSH_KEY=${AWS_SSH_KEY:-$HOME/.ssh/kube_aws_rsa}
IAM_PROFILE_MASTER="kubernetes-master" IAM_PROFILE_MASTER="kubernetes-master"
IAM_PROFILE_MINION="kubernetes-minion" IAM_PROFILE_MINION="kubernetes-minion"
......
...@@ -7,6 +7,21 @@ ...@@ -7,6 +7,21 @@
"Resource": [ "Resource": [
"arn:aws:s3:::kubernetes-*" "arn:aws:s3:::kubernetes-*"
] ]
},
{
"Effect": "Allow",
"Action": "ec2:Describe*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "ec2:AttachVolume",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "ec2:DetachVolume",
"Resource": "*"
} }
] ]
} }
...@@ -36,12 +36,12 @@ function json_val { ...@@ -36,12 +36,12 @@ function json_val {
# TODO (ayurchuk) Refactor the get_* functions to use filters # TODO (ayurchuk) Refactor the get_* functions to use filters
# TODO (bburns) Parameterize this for multiple cluster per project # TODO (bburns) Parameterize this for multiple cluster per project
function get_instance_ids {
python -c "import json,sys; lst = [str(instance['InstanceId']) for reservation in json.load(sys.stdin)['Reservations'] for instance in reservation['Instances'] for tag in instance.get('Tags', []) if tag['Value'].startswith('${MASTER_TAG}') or tag['Value'].startswith('${MINION_TAG}')]; print ' '.join(lst)"
}
function get_vpc_id { function get_vpc_id {
python -c 'import json,sys; lst = [str(vpc["VpcId"]) for vpc in json.load(sys.stdin)["Vpcs"] for tag in vpc.get("Tags", []) if tag["Value"] == "kubernetes-vpc"]; print "".join(lst)' $AWS_CMD --output text describe-vpcs \
--filters Name=tag:Name,Values=kubernetes-vpc \
Name=tag:KubernetesCluster,Values=${CLUSTER_ID} \
--query Vpcs[].VpcId
} }
function get_subnet_id { function get_subnet_id {
...@@ -69,7 +69,9 @@ function expect_instance_states { ...@@ -69,7 +69,9 @@ function expect_instance_states {
function get_instance_public_ip { function get_instance_public_ip {
local tagName=$1 local tagName=$1
$AWS_CMD --output text describe-instances \ $AWS_CMD --output text describe-instances \
--filters Name=tag:Name,Values=${tagName} Name=instance-state-name,Values=running \ --filters Name=tag:Name,Values=${tagName} \
Name=instance-state-name,Values=running \
Name=tag:KubernetesCluster,Values=${CLUSTER_ID} \
--query Reservations[].Instances[].NetworkInterfaces[0].Association.PublicIp --query Reservations[].Instances[].NetworkInterfaces[0].Association.PublicIp
} }
...@@ -371,7 +373,7 @@ function kube-up { ...@@ -371,7 +373,7 @@ function kube-up {
$AWS_CMD import-key-pair --key-name kubernetes --public-key-material "file://$AWS_SSH_KEY.pub" > $LOG 2>&1 || true $AWS_CMD import-key-pair --key-name kubernetes --public-key-material "file://$AWS_SSH_KEY.pub" > $LOG 2>&1 || true
VPC_ID=$($AWS_CMD describe-vpcs | get_vpc_id) VPC_ID=$(get_vpc_id)
if [[ -z "$VPC_ID" ]]; then if [[ -z "$VPC_ID" ]]; then
echo "Creating vpc." echo "Creating vpc."
...@@ -379,6 +381,7 @@ function kube-up { ...@@ -379,6 +381,7 @@ function kube-up {
$AWS_CMD modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-support '{"Value": true}' > $LOG $AWS_CMD modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-support '{"Value": true}' > $LOG
$AWS_CMD modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames '{"Value": true}' > $LOG $AWS_CMD modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames '{"Value": true}' > $LOG
add-tag $VPC_ID Name kubernetes-vpc add-tag $VPC_ID Name kubernetes-vpc
add-tag $VPC_ID KubernetesCluster ${CLUSTER_ID}
fi fi
echo "Using VPC $VPC_ID" echo "Using VPC $VPC_ID"
...@@ -467,6 +470,7 @@ function kube-up { ...@@ -467,6 +470,7 @@ function kube-up {
--user-data file://${KUBE_TEMP}/master-start.sh | json_val '["Instances"][0]["InstanceId"]') --user-data file://${KUBE_TEMP}/master-start.sh | json_val '["Instances"][0]["InstanceId"]')
add-tag $master_id Name $MASTER_NAME add-tag $master_id Name $MASTER_NAME
add-tag $master_id Role $MASTER_TAG add-tag $master_id Role $MASTER_TAG
add-tag $master_id KubernetesCluster ${CLUSTER_ID}
echo "Waiting for master to be ready" echo "Waiting for master to be ready"
...@@ -548,6 +552,7 @@ function kube-up { ...@@ -548,6 +552,7 @@ function kube-up {
add-tag $minion_id Name ${MINION_NAMES[$i]} add-tag $minion_id Name ${MINION_NAMES[$i]}
add-tag $minion_id Role $MINION_TAG add-tag $minion_id Role $MINION_TAG
add-tag $minion_id KubernetesCluster ${CLUSTER_ID}
MINION_IDS[$i]=$minion_id MINION_IDS[$i]=$minion_id
done done
...@@ -700,25 +705,7 @@ EOF ...@@ -700,25 +705,7 @@ EOF
} }
function kube-down { function kube-down {
instance_ids=$($AWS_CMD describe-instances | get_instance_ids) vpc_id=$(get_vpc_id)
if [[ -n ${instance_ids} ]]; then
$AWS_CMD terminate-instances --instance-ids $instance_ids > $LOG
echo "Waiting for instances deleted"
while true; do
instance_states=$($AWS_CMD describe-instances --instance-ids $instance_ids | expect_instance_states terminated)
if [[ "$instance_states" == "" ]]; then
echo "All instances terminated"
break
else
echo "Instances not yet terminated: $instance_states"
echo "Sleeping for 3 seconds..."
sleep 3
fi
done
fi
echo "Deleting VPC"
vpc_id=$($AWS_CMD describe-vpcs | get_vpc_id)
if [[ -n "${vpc_id}" ]]; then if [[ -n "${vpc_id}" ]]; then
local elb_ids=$(get_elbs_in_vpc ${vpc_id}) local elb_ids=$(get_elbs_in_vpc ${vpc_id})
if [[ -n ${elb_ids} ]]; then if [[ -n ${elb_ids} ]]; then
...@@ -741,6 +728,27 @@ function kube-down { ...@@ -741,6 +728,27 @@ function kube-down {
done done
fi fi
echo "Deleting instances in VPC: ${vpc_id}"
instance_ids=$($AWS_CMD --output text describe-instances \
--filters Name=vpc-id,Values=${vpc_id} \
Name=tag:KubernetesCluster,Values=${CLUSTER_ID} \
--query Reservations[].Instances[].InstanceId)
if [[ -n ${instance_ids} ]]; then
$AWS_CMD terminate-instances --instance-ids $instance_ids > $LOG
echo "Waiting for instances to be deleted"
while true; do
instance_states=$($AWS_CMD describe-instances --instance-ids $instance_ids | expect_instance_states terminated)
if [[ "$instance_states" == "" ]]; then
echo "All instances deleted"
break
else
echo "Instances not yet deleted: $instance_states"
echo "Sleeping for 3 seconds..."
sleep 3
fi
done
fi
echo "Deleting VPC: ${vpc_id}" echo "Deleting VPC: ${vpc_id}"
default_sg_id=$($AWS_CMD --output text describe-security-groups \ default_sg_id=$($AWS_CMD --output text describe-security-groups \
--filters Name=vpc-id,Values=$vpc_id Name=group-name,Values=default \ --filters Name=vpc-id,Values=$vpc_id Name=group-name,Values=default \
......
{% if grains['cloud'] is defined and grains['cloud'] == 'aws' %}
/usr/share/google:
file.directory:
- user: root
- group: root
- dir_mode: 755
/usr/share/google/safe_format_and_mount:
file.managed:
- source: salt://helpers/safe_format_and_mount
- user: root
- group: root
- mode: 755
{% endif %}
#! /bin/bash
# Copyright 2013 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.
# Mount a disk, formatting it if necessary. If the disk looks like it may
# have been formatted before, we will not format it.
#
# This script uses blkid and file to search for magic "formatted" bytes
# at the beginning of the disk. Furthermore, it attempts to use fsck to
# repair the filesystem before formatting it.
FSCK=fsck.ext4
MOUNT_OPTIONS="discard,defaults"
MKFS="mkfs.ext4 -E lazy_itable_init=0,lazy_journal_init=0 -F"
if grep -q '6\..' /etc/redhat-release; then
# lazy_journal_init is not recognized in redhat 6
MKFS="mkfs.ext4 -E lazy_itable_init=0 -F"
elif grep -q '7\..' /etc/redhat-release; then
FSCK=fsck.xfs
MKFS=mkfs.xfs
fi
LOGTAG=safe_format_and_mount
LOGFACILITY=user
function log() {
local readonly severity=$1; shift;
logger -t ${LOGTAG} -p ${LOGFACILITY}.${severity} -s "$@"
}
function log_command() {
local readonly log_file=$(mktemp)
local readonly retcode
log info "Running: $*"
$* > ${log_file} 2>&1
retcode=$?
# only return the last 1000 lines of the logfile, just in case it's HUGE.
tail -1000 ${log_file} | logger -t ${LOGTAG} -p ${LOGFACILITY}.info -s
rm -f ${log_file}
return ${retcode}
}
function help() {
cat >&2 <<EOF
$0 [-f fsck_cmd] [-m mkfs_cmd] [-o mount_opts] <device> <mountpoint>
EOF
exit 0
}
while getopts ":hf:o:m:" opt; do
case $opt in
h) help;;
f) FSCK=$OPTARG;;
o) MOUNT_OPTIONS=$OPTARG;;
m) MKFS=$OPTARG;;
-) break;;
\?) log error "Invalid option: -${OPTARG}"; exit 1;;
:) log "Option -${OPTARG} requires an argument."; exit 1;;
esac
done
shift $(($OPTIND - 1))
readonly DISK=$1
readonly MOUNTPOINT=$2
[[ -z ${DISK} ]] && help
[[ -z ${MOUNTPOINT} ]] && help
function disk_looks_unformatted() {
blkid ${DISK}
if [[ $? == 0 ]]; then
return 0
fi
local readonly file_type=$(file --special-files ${DISK})
case ${file_type} in
*filesystem*)
return 0;;
esac
return 1
}
function format_disk() {
log_command ${MKFS} ${DISK}
}
function try_repair_disk() {
log_command ${FSCK} -a ${DISK}
local readonly fsck_return=$?
if [[ ${fsck_return} -ge 8 ]]; then
log error "Fsck could not correct errors on ${DISK}"
return 1
fi
if [[ ${fsck_return} -gt 0 ]]; then
log warning "Fsck corrected errors on ${DISK}"
fi
return 0
}
function try_mount() {
local mount_retcode
try_repair_disk
log_command mount -o ${MOUNT_OPTIONS} ${DISK} ${MOUNTPOINT}
mount_retcode=$?
if [[ ${mount_retcode} == 0 ]]; then
return 0
fi
# Check to see if it looks like a filesystem before formatting it.
disk_looks_unformatted ${DISK}
if [[ $? == 0 ]]; then
log error "Disk ${DISK} looks formatted but won't mount. Giving up."
return ${mount_retcode}
fi
# The disk looks like it's not been formatted before.
format_disk
if [[ $? != 0 ]]; then
log error "Format of ${DISK} failed."
fi
log_command mount -o ${MOUNT_OPTIONS} ${DISK} ${MOUNTPOINT}
mount_retcode=$?
if [[ ${mount_retcode} == 0 ]]; then
return 0
fi
log error "Tried everything we could, but could not mount ${DISK}."
return ${mount_retcode}
}
try_mount
exit $?
\ No newline at end of file
...@@ -11,6 +11,7 @@ base: ...@@ -11,6 +11,7 @@ base:
{% else %} {% else %}
- sdn - sdn
{% endif %} {% endif %}
- helpers
- cadvisor - cadvisor
- kubelet - kubelet
- kube-proxy - kube-proxy
......
...@@ -17,10 +17,13 @@ limitations under the License. ...@@ -17,10 +17,13 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"os" "os"
goruntime "runtime" goruntime "runtime"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/test/e2e" "github.com/GoogleCloudPlatform/kubernetes/test/e2e"
"github.com/golang/glog" "github.com/golang/glog"
...@@ -28,8 +31,8 @@ import ( ...@@ -28,8 +31,8 @@ import (
) )
var ( var (
context = &e2e.TestContextType{} context = &e2e.TestContextType{}
gceConfig = &context.GCEConfig cloudConfig = &context.CloudConfig
orderseed = flag.Int64("orderseed", 0, "If non-zero, seed of random test shuffle order. (Otherwise random.)") orderseed = flag.Int64("orderseed", 0, "If non-zero, seed of random test shuffle order. (Otherwise random.)")
reportDir = flag.String("report_dir", "", "Path to the directory where the JUnit XML reports should be saved. Default is empty, which doesn't generate these reports.") reportDir = flag.String("report_dir", "", "Path to the directory where the JUnit XML reports should be saved. Default is empty, which doesn't generate these reports.")
...@@ -47,9 +50,11 @@ func init() { ...@@ -47,9 +50,11 @@ func init() {
flag.StringVar(&context.Host, "host", "", "The host, or apiserver, to connect to") flag.StringVar(&context.Host, "host", "", "The host, or apiserver, to connect to")
flag.StringVar(&context.RepoRoot, "repo_root", "./", "Root directory of kubernetes repository, for finding test files. Default assumes working directory is repository root") flag.StringVar(&context.RepoRoot, "repo_root", "./", "Root directory of kubernetes repository, for finding test files. Default assumes working directory is repository root")
flag.StringVar(&context.Provider, "provider", "", "The name of the Kubernetes provider (gce, gke, local, vagrant, etc.)") flag.StringVar(&context.Provider, "provider", "", "The name of the Kubernetes provider (gce, gke, local, vagrant, etc.)")
flag.StringVar(&gceConfig.MasterName, "kube_master", "", "Name of the kubernetes master. Only required if provider is gce or gke")
flag.StringVar(&gceConfig.ProjectID, "gce_project", "", "The GCE project being used, if applicable") // TODO: Flags per provider? Rename gce_project/gce_zone?
flag.StringVar(&gceConfig.Zone, "gce_zone", "", "GCE zone being used, if applicable") flag.StringVar(&cloudConfig.MasterName, "kube_master", "", "Name of the kubernetes master. Only required if provider is gce or gke")
flag.StringVar(&cloudConfig.ProjectID, "gce_project", "", "The GCE project being used, if applicable")
flag.StringVar(&cloudConfig.Zone, "gce_zone", "", "GCE zone being used, if applicable")
} }
func main() { func main() {
...@@ -63,5 +68,22 @@ func main() { ...@@ -63,5 +68,22 @@ func main() {
glog.Error("Invalid --times (negative or no testing requested)!") glog.Error("Invalid --times (negative or no testing requested)!")
os.Exit(1) os.Exit(1)
} }
if context.Provider == "aws" {
awsConfig := "[Global]\n"
if cloudConfig.Zone == "" {
glog.Error("gce_zone must be specified for AWS")
os.Exit(1)
}
awsConfig += fmt.Sprintf("Zone=%s\n", cloudConfig.Zone)
var err error
cloudConfig.Provider, err = cloudprovider.GetCloudProvider(context.Provider, strings.NewReader(awsConfig))
if err != nil {
glog.Error("Error building AWS provider: ", err)
os.Exit(1)
}
}
e2e.RunE2ETests(context, *orderseed, *times, *reportDir, testList) e2e.RunE2ETests(context, *orderseed, *times, *reportDir, testList)
} }
...@@ -25,6 +25,7 @@ import ( ...@@ -25,6 +25,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/network/exec" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/network/exec"
// Volume plugins // Volume plugins
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume/aws_ebs"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume/empty_dir" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/empty_dir"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume/gce_pd" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/gce_pd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume/git_repo" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume/git_repo"
...@@ -49,6 +50,7 @@ func ProbeVolumePlugins() []volume.VolumePlugin { ...@@ -49,6 +50,7 @@ func ProbeVolumePlugins() []volume.VolumePlugin {
// The list of plugins to probe is decided by the kubelet binary, not // The list of plugins to probe is decided by the kubelet binary, not
// by dynamic linking or other "magic". Plugins will be analyzed and // by dynamic linking or other "magic". Plugins will be analyzed and
// initialized later. // initialized later.
allPlugins = append(allPlugins, aws_ebs.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, empty_dir.ProbeVolumePlugins()...) allPlugins = append(allPlugins, empty_dir.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, gce_pd.ProbeVolumePlugins()...) allPlugins = append(allPlugins, gce_pd.ProbeVolumePlugins()...)
allPlugins = append(allPlugins, git_repo.ProbeVolumePlugins()...) allPlugins = append(allPlugins, git_repo.ProbeVolumePlugins()...)
......
...@@ -173,7 +173,7 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { ...@@ -173,7 +173,7 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer {
func(vs *api.VolumeSource, c fuzz.Continue) { func(vs *api.VolumeSource, c fuzz.Continue) {
// Exactly one of the fields should be set. // Exactly one of the fields should be set.
//FIXME: the fuzz can still end up nil. What if fuzz allowed me to say that? //FIXME: the fuzz can still end up nil. What if fuzz allowed me to say that?
fuzzOneOf(c, &vs.HostPath, &vs.EmptyDir, &vs.GCEPersistentDisk, &vs.GitRepo, &vs.Secret, &vs.NFS, &vs.ISCSI, &vs.Glusterfs) fuzzOneOf(c, &vs.HostPath, &vs.EmptyDir, &vs.GCEPersistentDisk, &vs.AWSElasticBlockStore, &vs.GitRepo, &vs.Secret, &vs.NFS, &vs.ISCSI, &vs.Glusterfs)
}, },
func(d *api.DNSPolicy, c fuzz.Continue) { func(d *api.DNSPolicy, c fuzz.Continue) {
policies := []api.DNSPolicy{api.DNSClusterFirst, api.DNSDefault} policies := []api.DNSPolicy{api.DNSClusterFirst, api.DNSDefault}
......
...@@ -189,6 +189,9 @@ type VolumeSource struct { ...@@ -189,6 +189,9 @@ type VolumeSource struct {
// GCEPersistentDisk represents a GCE Disk resource that is attached to a // GCEPersistentDisk represents a GCE Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod. // kubelet's host machine and then exposed to the pod.
GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk"` GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk"`
// AWSElasticBlockStore represents an AWS EBS disk that is attached to a
// kubelet's host machine and then exposed to the pod.
AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore"`
// GitRepo represents a git repository at a particular revision. // GitRepo represents a git repository at a particular revision.
GitRepo *GitRepoVolumeSource `json:"gitRepo"` GitRepo *GitRepoVolumeSource `json:"gitRepo"`
// Secret represents a secret that should populate this volume. // Secret represents a secret that should populate this volume.
...@@ -208,6 +211,9 @@ type PersistentVolumeSource struct { ...@@ -208,6 +211,9 @@ type PersistentVolumeSource struct {
// GCEPersistentDisk represents a GCE Disk resource that is attached to a // GCEPersistentDisk represents a GCE Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod. // kubelet's host machine and then exposed to the pod.
GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk"` GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk"`
// AWSElasticBlockStore represents an AWS EBS disk that is attached to a
// kubelet's host machine and then exposed to the pod.
AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore"`
// HostPath represents a directory on the host. // HostPath represents a directory on the host.
// This is useful for development and testing only. // This is useful for development and testing only.
// on-host storage is not supported in any way // on-host storage is not supported in any way
...@@ -394,6 +400,28 @@ type ISCSIVolumeSource struct { ...@@ -394,6 +400,28 @@ type ISCSIVolumeSource struct {
ReadOnly bool `json:"readOnly,omitempty"` ReadOnly bool `json:"readOnly,omitempty"`
} }
// AWSElasticBlockStoreVolumeSource represents a Persistent Disk resource in AWS.
//
// An AWS EBS disk must exist and be formatted before mounting to a container.
// The disk must also be in the same AWS zone as the kubelet.
// A AWS EBS disk can only be mounted as read/write once.
type AWSElasticBlockStoreVolumeSource struct {
// Unique id of the persistent disk resource. Used to identify the disk in AWS
VolumeID string `json:"volumeID"`
// 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 `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 `json:"partition,omitempty"`
// Optional: Defaults to false (read/write). ReadOnly here will force
// the ReadOnly setting in VolumeMounts.
ReadOnly bool `json:"readOnly,omitempty"`
}
// GitRepoVolumeSource represents a volume that is pulled from git when the pod is created. // GitRepoVolumeSource represents a volume that is pulled from git when the pod is created.
type GitRepoVolumeSource struct { type GitRepoVolumeSource struct {
// Repository URL // Repository URL
......
...@@ -1170,6 +1170,9 @@ func init() { ...@@ -1170,6 +1170,9 @@ func init() {
if err := s.Convert(&in.ISCSI, &out.ISCSI, 0); err != nil { if err := s.Convert(&in.ISCSI, &out.ISCSI, 0); err != nil {
return err return err
} }
if err := s.Convert(&in.AWSElasticBlockStore, &out.AWSElasticBlockStore, 0); err != nil {
return err
}
if err := s.Convert(&in.HostPath, &out.HostDir, 0); err != nil { if err := s.Convert(&in.HostPath, &out.HostDir, 0); err != nil {
return err return err
} }
...@@ -1197,6 +1200,9 @@ func init() { ...@@ -1197,6 +1200,9 @@ func init() {
if err := s.Convert(&in.ISCSI, &out.ISCSI, 0); err != nil { if err := s.Convert(&in.ISCSI, &out.ISCSI, 0); err != nil {
return err return err
} }
if err := s.Convert(&in.AWSElasticBlockStore, &out.AWSElasticBlockStore, 0); err != nil {
return err
}
if err := s.Convert(&in.HostDir, &out.HostPath, 0); err != nil { if err := s.Convert(&in.HostDir, &out.HostPath, 0); err != nil {
return err return err
} }
......
...@@ -88,7 +88,7 @@ type Volume struct { ...@@ -88,7 +88,7 @@ type Volume struct {
// Source represents the location and type of a volume to mount. // Source represents the location and type of a volume to mount.
// This is optional for now. If not specified, the Volume is implied to be an EmptyDir. // This is optional for now. If not specified, the Volume is implied to be an EmptyDir.
// This implied behavior is deprecated and will be removed in a future version. // This implied behavior is deprecated and will be removed in a future version.
Source VolumeSource `json:"source,omitempty" description:"location and type of volume to mount; at most one of HostDir, EmptyDir, GCEPersistentDisk, or GitRepo; default is EmptyDir"` Source VolumeSource `json:"source,omitempty" description:"location and type of volume to mount; at most one of HostDir, EmptyDir, GCEPersistentDisk, AWSElasticBlockStore, or GitRepo; default is EmptyDir"`
} }
// VolumeSource represents the source location of a volume to mount. // VolumeSource represents the source location of a volume to mount.
...@@ -105,6 +105,9 @@ type VolumeSource struct { ...@@ -105,6 +105,9 @@ type VolumeSource struct {
// GCEPersistentDisk represents a GCE Disk resource that is attached to a // GCEPersistentDisk represents a GCE Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod. // kubelet's host machine and then exposed to the pod.
GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"persistentDisk" description:"GCE disk resource attached to the host machine on demand"` GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"persistentDisk" description:"GCE disk resource attached to the host machine on demand"`
// AWSElasticBlockStore represents an AWS Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod.
AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore" description:"AWS disk resource attached to the host machine on demand"`
// GitRepo represents a git repository at a particular revision. // GitRepo represents a git repository at a particular revision.
GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"` GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"`
// Secret represents a secret to populate the volume with // Secret represents a secret to populate the volume with
...@@ -124,6 +127,9 @@ type PersistentVolumeSource struct { ...@@ -124,6 +127,9 @@ type PersistentVolumeSource struct {
// GCEPersistentDisk represents a GCE Disk resource that is attached to a // GCEPersistentDisk represents a GCE Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod. // kubelet's host machine and then exposed to the pod.
GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"persistentDisk" description:"GCE disk resource provisioned by an admin"` GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"persistentDisk" description:"GCE disk resource provisioned by an admin"`
// AWSElasticBlockStore represents an AWS EBS volume that is attached to a
// kubelet's host machine and then exposed to the pod.
AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore" description:"AWS disk resource provisioned by an admin"`
// HostPath represents a directory on the host. // HostPath represents a directory on the host.
// This is useful for development and testing only. // This is useful for development and testing only.
// on-host storage is not supported in any way. // on-host storage is not supported in any way.
...@@ -302,6 +308,29 @@ type ISCSIVolumeSource struct { ...@@ -302,6 +308,29 @@ type ISCSIVolumeSource struct {
ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"` ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"`
} }
// AWSElasticBlockStoreVolumeSource represents a Persistent Disk resource in AWS.
//
// An AWS PD must exist and be formatted before mounting to a container.
// The disk must also be in the same AWS zone as the kubelet.
// A AWS PD can only be mounted on a single machine.
type AWSElasticBlockStoreVolumeSource struct {
// Unique id of the PD resource. Used to identify the disk in AWS
VolumeID string `json:"volumeID" description:"unique id of the PD resource in AWS"`
// 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
// TODO: why omitempty if required?
FSType string `json:"fsType,omitempty" description:"file system type to mount, such as ext4, xfs, ntfs"`
// 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 `json:"partition,omitempty" description:"partition on the disk to mount (e.g., '1' for /dev/sda1); if omitted the plain device name (e.g., /dev/sda) will be mounted"`
// Optional: Defaults to false (read/write). ReadOnly here will force
// the ReadOnly setting in VolumeMounts.
ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"`
}
// GitRepoVolumeSource represents a volume that is pulled from git when the pod is created. // GitRepoVolumeSource represents a volume that is pulled from git when the pod is created.
type GitRepoVolumeSource struct { type GitRepoVolumeSource struct {
// Repository URL // Repository URL
......
...@@ -1097,6 +1097,9 @@ func init() { ...@@ -1097,6 +1097,9 @@ func init() {
if err := s.Convert(&in.GCEPersistentDisk, &out.GCEPersistentDisk, 0); err != nil { if err := s.Convert(&in.GCEPersistentDisk, &out.GCEPersistentDisk, 0); err != nil {
return err return err
} }
if err := s.Convert(&in.AWSElasticBlockStore, &out.AWSElasticBlockStore, 0); err != nil {
return err
}
if err := s.Convert(&in.HostPath, &out.HostDir, 0); err != nil { if err := s.Convert(&in.HostPath, &out.HostDir, 0); err != nil {
return err return err
} }
...@@ -1121,6 +1124,9 @@ func init() { ...@@ -1121,6 +1124,9 @@ func init() {
if err := s.Convert(&in.GCEPersistentDisk, &out.GCEPersistentDisk, 0); err != nil { if err := s.Convert(&in.GCEPersistentDisk, &out.GCEPersistentDisk, 0); err != nil {
return err return err
} }
if err := s.Convert(&in.AWSElasticBlockStore, &out.AWSElasticBlockStore, 0); err != nil {
return err
}
if err := s.Convert(&in.ISCSI, &out.ISCSI, 0); err != nil { if err := s.Convert(&in.ISCSI, &out.ISCSI, 0); err != nil {
return err return err
} }
......
...@@ -55,7 +55,7 @@ type Volume struct { ...@@ -55,7 +55,7 @@ type Volume struct {
// Source represents the location and type of a volume to mount. // Source represents the location and type of a volume to mount.
// This is optional for now. If not specified, the Volume is implied to be an EmptyDir. // This is optional for now. If not specified, the Volume is implied to be an EmptyDir.
// This implied behavior is deprecated and will be removed in a future version. // This implied behavior is deprecated and will be removed in a future version.
Source VolumeSource `json:"source,omitempty" description:"location and type of volume to mount; at most one of HostDir, EmptyDir, GCEPersistentDisk, or GitRepo; default is EmptyDir"` Source VolumeSource `json:"source,omitempty" description:"location and type of volume to mount; at most one of HostDir, EmptyDir, GCEPersistentDisk, AWSElasticBlockStore, or GitRepo; default is EmptyDir"`
} }
// VolumeSource represents the source location of a volume to mount. // VolumeSource represents the source location of a volume to mount.
...@@ -74,6 +74,9 @@ type VolumeSource struct { ...@@ -74,6 +74,9 @@ type VolumeSource struct {
// A persistent disk that is mounted to the // A persistent disk that is mounted to the
// kubelet's host machine and then exposed to the pod. // kubelet's host machine and then exposed to the pod.
GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"persistentDisk" description:"GCE disk resource attached to the host machine on demand"` GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"persistentDisk" description:"GCE disk resource attached to the host machine on demand"`
// An AWS persistent disk that is mounted to the
// kubelet's host machine and then exposed to the pod.
AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore" description:"AWS disk resource attached to the host machine on demand"`
// GitRepo represents a git repository at a particular revision. // GitRepo represents a git repository at a particular revision.
GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"` GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"`
// Secret is a secret to populate the volume with // Secret is a secret to populate the volume with
...@@ -93,6 +96,9 @@ type PersistentVolumeSource struct { ...@@ -93,6 +96,9 @@ type PersistentVolumeSource struct {
// GCEPersistentDisk represents a GCE Disk resource that is attached to a // GCEPersistentDisk represents a GCE Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod. // kubelet's host machine and then exposed to the pod.
GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"persistentDisk" description:"GCE disk resource provisioned by an admin"` GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"persistentDisk" description:"GCE disk resource provisioned by an admin"`
// AWSElasticBlockStore represents an AWS Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod.
AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore" description:"AWS disk resource provisioned by an admin"`
// HostPath represents a directory on the host. // HostPath represents a directory on the host.
// This is useful for development and testing only. // This is useful for development and testing only.
// on-host storage is not supported in any way. // on-host storage is not supported in any way.
...@@ -284,6 +290,29 @@ type GCEPersistentDiskVolumeSource struct { ...@@ -284,6 +290,29 @@ type GCEPersistentDiskVolumeSource struct {
ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"` ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"`
} }
// AWSElasticBlockStoreVolumeSource represents a Persistent Disk resource in AWS.
//
// An AWS PD must exist and be formatted before mounting to a container.
// The disk must also be in the same AWS zone as the kubelet.
// A AWS PD can only be mounted on a single machine.
type AWSElasticBlockStoreVolumeSource struct {
// Unique id of the PD resource. Used to identify the disk in AWS
VolumeID string `json:"volumeID" description:"unique id of the PD resource in AWS"`
// 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
// TODO: why omitempty if required?
FSType string `json:"fsType,omitempty" description:"file system type to mount, such as ext4, xfs, ntfs"`
// 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 `json:"partition,omitempty" description:"partition on the disk to mount (e.g., '1' for /dev/sda1); if omitted the plain device name (e.g., /dev/sda) will be mounted"`
// Optional: Defaults to false (read/write). ReadOnly here will force
// the ReadOnly setting in VolumeMounts.
ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"`
}
// GitRepoVolumeSource represents a volume that is pulled from git when the pod is created. // GitRepoVolumeSource represents a volume that is pulled from git when the pod is created.
type GitRepoVolumeSource struct { type GitRepoVolumeSource struct {
// Repository URL // Repository URL
......
...@@ -206,6 +206,9 @@ type VolumeSource struct { ...@@ -206,6 +206,9 @@ type VolumeSource struct {
// GCEPersistentDisk represents a GCE Disk resource that is attached to a // GCEPersistentDisk represents a GCE Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod. // kubelet's host machine and then exposed to the pod.
GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk" description:"GCE disk resource attached to the host machine on demand"` GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk" description:"GCE disk resource attached to the host machine on demand"`
// AWSElasticBlockStore represents an AWS Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod.
AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore" description:"AWS disk resource attached to the host machine on demand"`
// GitRepo represents a git repository at a particular revision. // GitRepo represents a git repository at a particular revision.
GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"` GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"`
// Secret represents a secret that should populate this volume. // Secret represents a secret that should populate this volume.
...@@ -225,6 +228,9 @@ type PersistentVolumeSource struct { ...@@ -225,6 +228,9 @@ type PersistentVolumeSource struct {
// GCEPersistentDisk represents a GCE Disk resource that is attached to a // GCEPersistentDisk represents a GCE Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod. // kubelet's host machine and then exposed to the pod.
GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk" description:"GCE disk resource provisioned by an admin"` GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk" description:"GCE disk resource provisioned by an admin"`
// AWSElasticBlockStore represents an AWS Disk resource that is attached to a
// kubelet's host machine and then exposed to the pod.
AWSElasticBlockStore *AWSElasticBlockStoreVolumeSource `json:"awsElasticBlockStore" description:"AWS disk resource provisioned by an admin"`
// HostPath represents a directory on the host. // HostPath represents a directory on the host.
// This is useful for development and testing only. // This is useful for development and testing only.
// on-host storage is not supported in any way. // on-host storage is not supported in any way.
...@@ -400,6 +406,29 @@ type GCEPersistentDiskVolumeSource struct { ...@@ -400,6 +406,29 @@ type GCEPersistentDiskVolumeSource struct {
ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"` ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"`
} }
// AWSElasticBlockStoreVolumeSource represents a Persistent Disk resource in AWS.
//
// An AWS PD must exist and be formatted before mounting to a container.
// The disk must also be in the same AWS zone as the kubelet.
// A AWS PD can only be mounted on a single machine.
type AWSElasticBlockStoreVolumeSource struct {
// Unique id of the PD resource. Used to identify the disk in AWS
VolumeID string `json:"volumeID" description:"unique id of the PD resource in AWS"`
// 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
// TODO: why omitempty if required?
FSType string `json:"fsType,omitempty" description:"file system type to mount, such as ext4, xfs, ntfs"`
// 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 `json:"partition,omitempty" description:"partition on the disk to mount (e.g., '1' for /dev/sda1); if omitted the plain device name (e.g., /dev/sda) will be mounted"`
// Optional: Defaults to false (read/write). ReadOnly here will force
// the ReadOnly setting in VolumeMounts.
ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"`
}
// GitRepoVolumeSource represents a volume that is pulled from git when the pod is created. // GitRepoVolumeSource represents a volume that is pulled from git when the pod is created.
type GitRepoVolumeSource struct { type GitRepoVolumeSource struct {
// Repository URL // Repository URL
......
...@@ -299,6 +299,10 @@ func validateSource(source *api.VolumeSource) errs.ValidationErrorList { ...@@ -299,6 +299,10 @@ func validateSource(source *api.VolumeSource) errs.ValidationErrorList {
numVolumes++ numVolumes++
allErrs = append(allErrs, validateGCEPersistentDiskVolumeSource(source.GCEPersistentDisk).Prefix("persistentDisk")...) allErrs = append(allErrs, validateGCEPersistentDiskVolumeSource(source.GCEPersistentDisk).Prefix("persistentDisk")...)
} }
if source.AWSElasticBlockStore != nil {
numVolumes++
allErrs = append(allErrs, validateAWSElasticBlockStoreVolumeSource(source.AWSElasticBlockStore).Prefix("awsElasticBlockStore")...)
}
if source.Secret != nil { if source.Secret != nil {
numVolumes++ numVolumes++
allErrs = append(allErrs, validateSecretVolumeSource(source.Secret).Prefix("secret")...) allErrs = append(allErrs, validateSecretVolumeSource(source.Secret).Prefix("secret")...)
...@@ -368,6 +372,20 @@ func validateGCEPersistentDiskVolumeSource(PD *api.GCEPersistentDiskVolumeSource ...@@ -368,6 +372,20 @@ func validateGCEPersistentDiskVolumeSource(PD *api.GCEPersistentDiskVolumeSource
return allErrs return allErrs
} }
func validateAWSElasticBlockStoreVolumeSource(PD *api.AWSElasticBlockStoreVolumeSource) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
if PD.VolumeID == "" {
allErrs = append(allErrs, errs.NewFieldRequired("volumeID"))
}
if PD.FSType == "" {
allErrs = append(allErrs, errs.NewFieldRequired("fsType"))
}
if PD.Partition < 0 || PD.Partition > 255 {
allErrs = append(allErrs, errs.NewFieldInvalid("partition", PD.Partition, pdPartitionErrorMsg))
}
return allErrs
}
func validateSecretVolumeSource(secretSource *api.SecretVolumeSource) errs.ValidationErrorList { func validateSecretVolumeSource(secretSource *api.SecretVolumeSource) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{} allErrs := errs.ValidationErrorList{}
if secretSource.SecretName == "" { if secretSource.SecretName == "" {
...@@ -426,6 +444,10 @@ func ValidatePersistentVolume(pv *api.PersistentVolume) errs.ValidationErrorList ...@@ -426,6 +444,10 @@ func ValidatePersistentVolume(pv *api.PersistentVolume) errs.ValidationErrorList
numVolumes++ numVolumes++
allErrs = append(allErrs, validateGCEPersistentDiskVolumeSource(pv.Spec.GCEPersistentDisk).Prefix("persistentDisk")...) allErrs = append(allErrs, validateGCEPersistentDiskVolumeSource(pv.Spec.GCEPersistentDisk).Prefix("persistentDisk")...)
} }
if pv.Spec.AWSElasticBlockStore != nil {
numVolumes++
allErrs = append(allErrs, validateAWSElasticBlockStoreVolumeSource(pv.Spec.AWSElasticBlockStore).Prefix("awsElasticBlockStore")...)
}
if numVolumes != 1 { if numVolumes != 1 {
allErrs = append(allErrs, errs.NewFieldInvalid("", pv.Spec.PersistentVolumeSource, "exactly 1 volume type is required")) allErrs = append(allErrs, errs.NewFieldInvalid("", pv.Spec.PersistentVolumeSource, "exactly 1 volume type is required"))
} }
...@@ -1021,6 +1043,7 @@ func ValidateReadOnlyPersistentDisks(volumes []api.Volume) errs.ValidationErrorL ...@@ -1021,6 +1043,7 @@ func ValidateReadOnlyPersistentDisks(volumes []api.Volume) errs.ValidationErrorL
allErrs = append(allErrs, errs.NewFieldInvalid("GCEPersistentDisk.ReadOnly", false, "ReadOnly must be true for replicated pods > 1, as GCE PD can only be mounted on multiple machines if it is read-only.")) allErrs = append(allErrs, errs.NewFieldInvalid("GCEPersistentDisk.ReadOnly", false, "ReadOnly must be true for replicated pods > 1, as GCE PD can only be mounted on multiple machines if it is read-only."))
} }
} }
// TODO: What to do for AWS? It doesn't support replicas
} }
return allErrs return allErrs
} }
......
...@@ -516,6 +516,7 @@ func TestValidateVolumes(t *testing.T) { ...@@ -516,6 +516,7 @@ func TestValidateVolumes(t *testing.T) {
{Name: "abc-123", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path3"}}}, {Name: "abc-123", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path3"}}},
{Name: "empty", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, {Name: "empty", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}},
{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{"my-PD", "ext4", 1, false}}}, {Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{"my-PD", "ext4", 1, false}}},
{Name: "awsebs", VolumeSource: api.VolumeSource{AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{"my-PD", "ext4", 1, false}}},
{Name: "gitrepo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{"my-repo", "hashstring"}}}, {Name: "gitrepo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{"my-repo", "hashstring"}}},
{Name: "iscsidisk", VolumeSource: api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{"127.0.0.1", "iqn.2015-02.example.com:test", 1, "ext4", false}}}, {Name: "iscsidisk", VolumeSource: api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{"127.0.0.1", "iqn.2015-02.example.com:test", 1, "ext4", false}}},
{Name: "secret", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{"my-secret"}}}, {Name: "secret", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{"my-secret"}}},
......
...@@ -112,17 +112,17 @@ func TestNewAWSCloud(t *testing.T) { ...@@ -112,17 +112,17 @@ func TestNewAWSCloud(t *testing.T) {
}{ }{
{ {
"No config reader", "No config reader",
nil, fakeAuthFunc, nil, nil, fakeAuthFunc, &FakeMetadata{},
true, "", true, "",
}, },
{ {
"Config specified invalid zone", "Config specified invalid zone",
strings.NewReader("[global]\nzone = blahonga"), fakeAuthFunc, nil, strings.NewReader("[global]\nzone = blahonga"), fakeAuthFunc, &FakeMetadata{},
true, "", true, "",
}, },
{ {
"Config specifies valid zone", "Config specifies valid zone",
strings.NewReader("[global]\nzone = eu-west-1a"), fakeAuthFunc, nil, strings.NewReader("[global]\nzone = eu-west-1a"), fakeAuthFunc, &FakeMetadata{},
false, "eu-west-1a", false, "eu-west-1a",
}, },
{ {
...@@ -178,16 +178,39 @@ func (self *FakeEC2) Instances(instanceIds []string, filter *ec2InstanceFilter) ...@@ -178,16 +178,39 @@ func (self *FakeEC2) Instances(instanceIds []string, filter *ec2InstanceFilter)
type FakeMetadata struct { type FakeMetadata struct {
availabilityZone string availabilityZone string
instanceId string
} }
func (self *FakeMetadata) GetMetaData(key string) ([]byte, error) { func (self *FakeMetadata) GetMetaData(key string) ([]byte, error) {
if key == "placement/availability-zone" { if key == "placement/availability-zone" {
return []byte(self.availabilityZone), nil return []byte(self.availabilityZone), nil
} else if key == "instance-id" {
return []byte(self.instanceId), nil
} else { } else {
return nil, nil return nil, nil
} }
} }
func (ec2 *FakeEC2) AttachVolume(volumeID string, instanceId string, mountDevice string) (resp *ec2.AttachVolumeResp, err error) {
panic("Not implemented")
}
func (ec2 *FakeEC2) DetachVolume(volumeID string) (resp *ec2.SimpleResp, err error) {
panic("Not implemented")
}
func (ec2 *FakeEC2) Volumes(volumeIDs []string, filter *ec2.Filter) (resp *ec2.VolumesResp, err error) {
panic("Not implemented")
}
func (ec2 *FakeEC2) CreateVolume(request *ec2.CreateVolume) (resp *ec2.CreateVolumeResp, err error) {
panic("Not implemented")
}
func (ec2 *FakeEC2) DeleteVolume(volumeID string) (resp *ec2.SimpleResp, err error) {
panic("Not implemented")
}
func mockInstancesResp(instances []ec2.Instance) (aws *AWSCloud) { func mockInstancesResp(instances []ec2.Instance) (aws *AWSCloud) {
availabilityZone := "us-west-2d" availabilityZone := "us-west-2d"
return &AWSCloud{ return &AWSCloud{
......
...@@ -50,16 +50,26 @@ func (nodes ClientNodeInfo) GetNodeInfo(nodeID string) (*api.Node, error) { ...@@ -50,16 +50,26 @@ func (nodes ClientNodeInfo) GetNodeInfo(nodeID string) (*api.Node, error) {
} }
func isVolumeConflict(volume api.Volume, pod *api.Pod) bool { func isVolumeConflict(volume api.Volume, pod *api.Pod) bool {
if volume.GCEPersistentDisk == nil { if volume.GCEPersistentDisk != nil {
return false pdName := volume.GCEPersistentDisk.PDName
manifest := &(pod.Spec)
for ix := range manifest.Volumes {
if manifest.Volumes[ix].GCEPersistentDisk != nil &&
manifest.Volumes[ix].GCEPersistentDisk.PDName == pdName {
return true
}
}
} }
pdName := volume.GCEPersistentDisk.PDName if volume.AWSElasticBlockStore != nil {
volumeID := volume.AWSElasticBlockStore.VolumeID
manifest := &(pod.Spec)
for ix := range manifest.Volumes { manifest := &(pod.Spec)
if manifest.Volumes[ix].GCEPersistentDisk != nil && for ix := range manifest.Volumes {
manifest.Volumes[ix].GCEPersistentDisk.PDName == pdName { if manifest.Volumes[ix].AWSElasticBlockStore != nil &&
return true manifest.Volumes[ix].AWSElasticBlockStore.VolumeID == volumeID {
return true
}
} }
} }
return false return false
......
...@@ -321,6 +321,55 @@ func TestDiskConflicts(t *testing.T) { ...@@ -321,6 +321,55 @@ func TestDiskConflicts(t *testing.T) {
} }
} }
func TestAWSDiskConflicts(t *testing.T) {
volState := api.PodSpec{
Volumes: []api.Volume{
{
VolumeSource: api.VolumeSource{
AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
VolumeID: "foo",
},
},
},
},
}
volState2 := api.PodSpec{
Volumes: []api.Volume{
{
VolumeSource: api.VolumeSource{
AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
VolumeID: "bar",
},
},
},
},
}
tests := []struct {
pod api.Pod
existingPods []api.Pod
isOk bool
test string
}{
{api.Pod{}, []api.Pod{}, true, "nothing"},
{api.Pod{}, []api.Pod{{Spec: volState}}, true, "one state"},
{api.Pod{Spec: volState}, []api.Pod{{Spec: volState}}, false, "same state"},
{api.Pod{Spec: volState2}, []api.Pod{{Spec: volState}}, true, "different state"},
}
for _, test := range tests {
ok, err := NoDiskConflict(test.pod, test.existingPods, "machine")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if test.isOk && !ok {
t.Errorf("expected ok, got none. %v %v %s", test.pod, test.existingPods, test.test)
}
if !test.isOk && ok {
t.Errorf("expected no ok, got one. %v %v %s", test.pod, test.existingPods, test.test)
}
}
}
func TestPodFitsSelector(t *testing.T) { func TestPodFitsSelector(t *testing.T) {
tests := []struct { tests := []struct {
pod api.Pod pod api.Pod
......
/*
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 aws_ebs
import (
"os"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume"
)
func TestCanSupport(t *testing.T) {
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, nil))
plug, err := plugMgr.FindPluginByName("kubernetes.io/aws-ebs")
if err != nil {
t.Errorf("Can't find the plugin by name")
}
if plug.Name() != "kubernetes.io/aws-ebs" {
t.Errorf("Wrong name: %s", plug.Name())
}
if !plug.CanSupport(&api.Volume{VolumeSource: api.VolumeSource{AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{}}}) {
t.Errorf("Expected true")
}
}
func TestGetAccessModes(t *testing.T) {
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, nil))
plug, err := plugMgr.FindPersistentPluginByName("kubernetes.io/aws-ebs")
if err != nil {
t.Errorf("Can't find the plugin by name")
}
if !contains(plug.GetAccessModes(), api.ReadWriteOnce) {
t.Errorf("Expected to find AccessMode: %s", api.ReadWriteOnce)
}
if len(plug.GetAccessModes()) != 1 {
t.Errorf("Expected to find exactly one AccessMode")
}
}
func contains(modes []api.AccessModeType, mode api.AccessModeType) bool {
for _, m := range modes {
if m == mode {
return true
}
}
return false
}
type fakePDManager struct{}
// TODO(jonesdl) To fully test this, we could create a loopback device
// and mount that instead.
func (fake *fakePDManager) AttachAndMountDisk(pd *awsElasticBlockStore, globalPDPath string) error {
globalPath := makeGlobalPDPath(pd.plugin.host, pd.volumeID)
err := os.MkdirAll(globalPath, 0750)
if err != nil {
return err
}
return nil
}
func (fake *fakePDManager) DetachDisk(pd *awsElasticBlockStore) error {
globalPath := makeGlobalPDPath(pd.plugin.host, pd.volumeID)
err := os.RemoveAll(globalPath)
if err != nil {
return err
}
return nil
}
func TestPlugin(t *testing.T) {
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, nil))
plug, err := plugMgr.FindPluginByName("kubernetes.io/aws-ebs")
if err != nil {
t.Errorf("Can't find the plugin by name")
}
spec := &api.Volume{
Name: "vol1",
VolumeSource: api.VolumeSource{
AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
VolumeID: "pd",
FSType: "ext4",
},
},
}
builder, err := plug.(*awsElasticBlockStorePlugin).newBuilderInternal(spec, types.UID("poduid"), &fakePDManager{}, &mount.FakeMounter{})
if err != nil {
t.Errorf("Failed to make a new Builder: %v", err)
}
if builder == nil {
t.Errorf("Got a nil Builder")
}
path := builder.GetPath()
if path != "/tmp/fake/pods/poduid/volumes/kubernetes.io~aws-ebs/vol1" {
t.Errorf("Got unexpected path: %s", path)
}
if err := builder.SetUp(); err != nil {
t.Errorf("Expected success, got: %v", err)
}
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
t.Errorf("SetUp() failed, volume path not created: %s", path)
} else {
t.Errorf("SetUp() failed: %v", err)
}
}
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
t.Errorf("SetUp() failed, volume path not created: %s", path)
} else {
t.Errorf("SetUp() failed: %v", err)
}
}
cleaner, err := plug.(*awsElasticBlockStorePlugin).newCleanerInternal("vol1", types.UID("poduid"), &fakePDManager{}, &mount.FakeMounter{})
if err != nil {
t.Errorf("Failed to make a new Cleaner: %v", err)
}
if cleaner == nil {
t.Errorf("Got a nil Cleaner")
}
if err := cleaner.TearDown(); err != nil {
t.Errorf("Expected success, got: %v", err)
}
if _, err := os.Stat(path); err == nil {
t.Errorf("TearDown() failed, volume path still exists: %s", path)
} else if !os.IsNotExist(err) {
t.Errorf("SetUp() failed: %v", err)
}
}
/*
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 aws_ebs
import (
"errors"
"fmt"
"os"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/exec"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
"github.com/golang/glog"
)
type AWSDiskUtil struct{}
// Attaches a disk specified by a volume.AWSElasticBlockStore to the current kubelet.
// Mounts the disk to it's global path.
func (util *AWSDiskUtil) AttachAndMountDisk(pd *awsElasticBlockStore, globalPDPath string) error {
volumes, err := pd.getVolumeProvider()
if err != nil {
return err
}
flags := uintptr(0)
if pd.readOnly {
flags = mount.FlagReadOnly
}
devicePath, err := volumes.AttachDisk("", pd.volumeID, pd.readOnly)
if err != nil {
return err
}
if pd.partition != "" {
devicePath = devicePath + pd.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 (" + devicePath + ")")
}
time.Sleep(time.Second)
}
// Only mount the PD globally once.
mountpoint, err := pd.mounter.IsMountPoint(globalPDPath)
if err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(globalPDPath, 0750); err != nil {
return err
}
mountpoint = false
} else {
return err
}
}
if !mountpoint {
err = pd.diskMounter.Mount(devicePath, globalPDPath, pd.fsType, flags, "")
if err != nil {
os.Remove(globalPDPath)
return err
}
}
return nil
}
// Unmounts the device and detaches the disk from the kubelet's host machine.
func (util *AWSDiskUtil) DetachDisk(pd *awsElasticBlockStore) error {
// Unmount the global PD mount, which should be the only one.
globalPDPath := makeGlobalPDPath(pd.plugin.host, pd.volumeID)
if err := pd.mounter.Unmount(globalPDPath, 0); err != nil {
glog.V(2).Info("Error unmount dir ", globalPDPath, ": ", err)
return err
}
if err := os.Remove(globalPDPath); err != nil {
glog.V(2).Info("Error removing dir ", globalPDPath, ": ", err)
return err
}
// Detach the disk
volumes, err := pd.getVolumeProvider()
if err != nil {
glog.V(2).Info("Error getting volume provider for volumeID ", pd.volumeID, ": ", err)
return err
}
if err := volumes.DetachDisk("", pd.volumeID); err != nil {
glog.V(2).Info("Error detaching disk ", pd.volumeID, ": ", err)
return err
}
return nil
}
// safe_format_and_mount is a utility script on AWS VMs that probes a persistent disk, and if
// necessary formats it before mounting it.
// This eliminates the necessity to format a PD before it is used with a Pod on AWS.
// TODO: port this script into Go and use it for all Linux platforms
type awsSafeFormatAndMount struct {
mount.Interface
runner exec.Interface
}
// uses /usr/share/google/safe_format_and_mount to optionally mount, and format a disk
func (mounter *awsSafeFormatAndMount) Mount(source string, target string, fstype string, flags uintptr, data string) error {
// Don't attempt to format if mounting as readonly. Go straight to mounting.
if (flags & mount.FlagReadOnly) != 0 {
return mounter.Interface.Mount(source, target, fstype, flags, data)
}
args := []string{}
// ext4 is the default for safe_format_and_mount
if len(fstype) > 0 && fstype != "ext4" {
args = append(args, "-m", fmt.Sprintf("mkfs.%s", fstype))
}
args = append(args, source, target)
// TODO: Accept other options here?
glog.V(5).Infof("exec-ing: /usr/share/google/safe_format_and_mount %v", args)
cmd := mounter.runner.Command("/usr/share/google/safe_format_and_mount", args...)
dataOut, err := cmd.CombinedOutput()
if err != nil {
glog.V(5).Infof("error running /usr/share/google/safe_format_and_mount\n%s", string(dataOut))
}
return err
}
/*
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 aws_ebs
import (
"fmt"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/exec"
)
func TestSafeFormatAndMount(t *testing.T) {
tests := []struct {
fstype string
expectedArgs []string
err error
}{
{
fstype: "ext4",
expectedArgs: []string{"/dev/foo", "/mnt/bar"},
},
{
fstype: "vfat",
expectedArgs: []string{"-m", "mkfs.vfat", "/dev/foo", "/mnt/bar"},
},
{
err: fmt.Errorf("test error"),
},
}
for _, test := range tests {
var cmdOut string
var argsOut []string
fake := exec.FakeExec{
CommandScript: []exec.FakeCommandAction{
func(cmd string, args ...string) exec.Cmd {
cmdOut = cmd
argsOut = args
fake := exec.FakeCmd{
CombinedOutputScript: []exec.FakeCombinedOutputAction{
func() ([]byte, error) { return []byte{}, test.err },
},
}
return exec.InitFakeCmd(&fake, cmd, args...)
},
},
}
mounter := awsSafeFormatAndMount{
runner: &fake,
}
err := mounter.Mount("/dev/foo", "/mnt/bar", test.fstype, 0, "")
if test.err == nil && err != nil {
t.Errorf("unexpected error: %v", err)
}
if test.err != nil {
if err == nil {
t.Errorf("unexpected non-error")
}
return
}
if cmdOut != "/usr/share/google/safe_format_and_mount" {
t.Errorf("unexpected command: %s", cmdOut)
}
if len(argsOut) != len(test.expectedArgs) {
t.Errorf("unexpected args: %v, expected: %v", argsOut, test.expectedArgs)
}
}
}
...@@ -38,8 +38,8 @@ var _ = Describe("MasterCerts", func() { ...@@ -38,8 +38,8 @@ var _ = Describe("MasterCerts", func() {
} }
for _, certFile := range []string{"kubecfg.key", "kubecfg.crt", "ca.crt"} { for _, certFile := range []string{"kubecfg.key", "kubecfg.crt", "ca.crt"} {
cmd := exec.Command("gcloud", "compute", "ssh", "--project", testContext.GCEConfig.ProjectID, cmd := exec.Command("gcloud", "compute", "ssh", "--project", testContext.CloudConfig.ProjectID,
"--zone", testContext.GCEConfig.Zone, testContext.GCEConfig.MasterName, "--zone", testContext.CloudConfig.Zone, testContext.CloudConfig.MasterName,
"--command", fmt.Sprintf("ls /srv/kubernetes/%s", certFile)) "--command", fmt.Sprintf("ls /srv/kubernetes/%s", certFile))
if _, err := cmd.CombinedOutput(); err != nil { if _, err := cmd.CombinedOutput(); err != nil {
Fail(fmt.Sprintf("Error checking for cert file %s on master: %v", certFile, err)) Fail(fmt.Sprintf("Error checking for cert file %s on master: %v", certFile, err))
......
...@@ -25,6 +25,7 @@ import ( ...@@ -25,6 +25,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/onsi/ginkgo" "github.com/onsi/ginkgo"
...@@ -35,10 +36,12 @@ import ( ...@@ -35,10 +36,12 @@ import (
type testResult bool type testResult bool
type GCEConfig struct { type CloudConfig struct {
ProjectID string ProjectID string
Zone string Zone string
MasterName string MasterName string
Provider cloudprovider.Interface
} }
func init() { func init() {
......
...@@ -24,6 +24,7 @@ import ( ...@@ -24,6 +24,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
...@@ -35,7 +36,6 @@ var _ = Describe("PD", func() { ...@@ -35,7 +36,6 @@ var _ = Describe("PD", func() {
var ( var (
c *client.Client c *client.Client
podClient client.PodInterface podClient client.PodInterface
diskName string
host0Name string host0Name string
host1Name string host1Name string
) )
...@@ -51,37 +51,37 @@ var _ = Describe("PD", func() { ...@@ -51,37 +51,37 @@ var _ = Describe("PD", func() {
expectNoError(err, "Failed to list nodes for e2e cluster.") expectNoError(err, "Failed to list nodes for e2e cluster.")
Expect(len(nodes.Items) >= 2).To(BeTrue()) Expect(len(nodes.Items) >= 2).To(BeTrue())
diskName = fmt.Sprintf("e2e-%s", string(util.NewUUID()))
host0Name = nodes.Items[0].ObjectMeta.Name host0Name = nodes.Items[0].ObjectMeta.Name
host1Name = nodes.Items[1].ObjectMeta.Name host1Name = nodes.Items[1].ObjectMeta.Name
}) })
It("should schedule a pod w/ a RW PD, remove it, then schedule it on another host", func() { It("should schedule a pod w/ a RW PD, remove it, then schedule it on another host", func() {
if testContext.Provider != "gce" { if testContext.Provider != "gce" && testContext.Provider != "aws" {
By(fmt.Sprintf("Skipping PD test, which is only supported for provider gce (not %s)", By(fmt.Sprintf("Skipping PD test, which is only supported for providers gce & aws (not %s)",
testContext.Provider)) testContext.Provider))
return return
} }
By("creating PD")
diskName, err := createPD()
expectNoError(err, "Error creating PD")
host0Pod := testPDPod(diskName, host0Name, false) host0Pod := testPDPod(diskName, host0Name, false)
host1Pod := testPDPod(diskName, host1Name, false) host1Pod := testPDPod(diskName, host1Name, false)
By(fmt.Sprintf("creating PD %q", diskName))
expectNoError(createPD(diskName, testContext.GCEConfig.Zone), "Error creating PD")
defer func() { defer func() {
By("cleaning up PD-RW test environment") By("cleaning up PD-RW test environment")
// Teardown pods, PD. Ignore errors. // Teardown pods, PD. Ignore errors.
// Teardown should do nothing unless test failed. // Teardown should do nothing unless test failed.
podClient.Delete(host0Pod.Name) podClient.Delete(host0Pod.Name)
podClient.Delete(host1Pod.Name) podClient.Delete(host1Pod.Name)
detachPD(host0Name, diskName, testContext.GCEConfig.Zone) detachPD(host0Name, diskName)
detachPD(host1Name, diskName, testContext.GCEConfig.Zone) detachPD(host1Name, diskName)
deletePD(diskName, testContext.GCEConfig.Zone) deletePD(diskName)
}() }()
By("submitting host0Pod to kubernetes") By("submitting host0Pod to kubernetes")
_, err := podClient.Create(host0Pod) _, err = podClient.Create(host0Pod)
expectNoError(err, fmt.Sprintf("Failed to create host0Pod: %v", err)) expectNoError(err, fmt.Sprintf("Failed to create host0Pod: %v", err))
expectNoError(waitForPodRunning(c, host0Pod.Name)) expectNoError(waitForPodRunning(c, host0Pod.Name))
...@@ -100,10 +100,11 @@ var _ = Describe("PD", func() { ...@@ -100,10 +100,11 @@ var _ = Describe("PD", func() {
By(fmt.Sprintf("deleting PD %q", diskName)) By(fmt.Sprintf("deleting PD %q", diskName))
for start := time.Now(); time.Since(start) < 180*time.Second; time.Sleep(5 * time.Second) { for start := time.Now(); time.Since(start) < 180*time.Second; time.Sleep(5 * time.Second) {
if err = deletePD(diskName, testContext.GCEConfig.Zone); err != nil { if err = deletePD(diskName); err != nil {
Logf("Couldn't delete PD. Sleeping 5 seconds") Logf("Couldn't delete PD. Sleeping 5 seconds (%v)", err)
continue continue
} }
Logf("Deleted PD %v", diskName)
break break
} }
expectNoError(err, "Error deleting PD") expectNoError(err, "Error deleting PD")
...@@ -118,6 +119,10 @@ var _ = Describe("PD", func() { ...@@ -118,6 +119,10 @@ var _ = Describe("PD", func() {
return return
} }
By("creating PD")
diskName, err := createPD()
expectNoError(err, "Error creating PD")
rwPod := testPDPod(diskName, host0Name, false) rwPod := testPDPod(diskName, host0Name, false)
host0ROPod := testPDPod(diskName, host0Name, true) host0ROPod := testPDPod(diskName, host0Name, true)
host1ROPod := testPDPod(diskName, host1Name, true) host1ROPod := testPDPod(diskName, host1Name, true)
...@@ -129,16 +134,14 @@ var _ = Describe("PD", func() { ...@@ -129,16 +134,14 @@ var _ = Describe("PD", func() {
podClient.Delete(rwPod.Name) podClient.Delete(rwPod.Name)
podClient.Delete(host0ROPod.Name) podClient.Delete(host0ROPod.Name)
podClient.Delete(host1ROPod.Name) podClient.Delete(host1ROPod.Name)
detachPD(host0Name, diskName, testContext.GCEConfig.Zone)
detachPD(host1Name, diskName, testContext.GCEConfig.Zone)
deletePD(diskName, testContext.GCEConfig.Zone)
}()
By(fmt.Sprintf("creating PD %q", diskName)) detachPD(host0Name, diskName)
expectNoError(createPD(diskName, testContext.GCEConfig.Zone), "Error creating PD") detachPD(host1Name, diskName)
deletePD(diskName)
}()
By("submitting rwPod to ensure PD is formatted") By("submitting rwPod to ensure PD is formatted")
_, err := podClient.Create(rwPod) _, err = podClient.Create(rwPod)
expectNoError(err, "Failed to create rwPod") expectNoError(err, "Failed to create rwPod")
expectNoError(waitForPodRunning(c, rwPod.Name)) expectNoError(waitForPodRunning(c, rwPod.Name))
expectNoError(podClient.Delete(rwPod.Name), "Failed to delete host0Pod") expectNoError(podClient.Delete(rwPod.Name), "Failed to delete host0Pod")
...@@ -163,7 +166,7 @@ var _ = Describe("PD", func() { ...@@ -163,7 +166,7 @@ var _ = Describe("PD", func() {
By(fmt.Sprintf("deleting PD %q", diskName)) By(fmt.Sprintf("deleting PD %q", diskName))
for start := time.Now(); time.Since(start) < 180*time.Second; time.Sleep(5 * time.Second) { for start := time.Now(); time.Since(start) < 180*time.Second; time.Sleep(5 * time.Second) {
if err = deletePD(diskName, testContext.GCEConfig.Zone); err != nil { if err = deletePD(diskName); err != nil {
Logf("Couldn't delete PD. Sleeping 5 seconds") Logf("Couldn't delete PD. Sleeping 5 seconds")
continue continue
} }
...@@ -173,24 +176,62 @@ var _ = Describe("PD", func() { ...@@ -173,24 +176,62 @@ var _ = Describe("PD", func() {
}) })
}) })
func createPD(pdName, zone string) error { func createPD() (string, error) {
// TODO: make this hit the compute API directly instread of shelling out to gcloud. if testContext.Provider == "gce" {
return exec.Command("gcloud", "compute", "disks", "create", "--zone="+zone, "--size=10GB", pdName).Run() pdName := fmt.Sprintf("e2e-%s", string(util.NewUUID()))
zone := testContext.CloudConfig.Zone
// TODO: make this hit the compute API directly instread of shelling out to gcloud.
err := exec.Command("gcloud", "compute", "disks", "create", "--zone="+zone, "--size=10GB", pdName).Run()
if err != nil {
return "", err
}
return pdName, nil
} else {
volumes, ok := testContext.CloudConfig.Provider.(aws_cloud.Volumes)
if !ok {
return "", fmt.Errorf("Provider does not support volumes")
}
volumeOptions := &aws_cloud.VolumeOptions{}
volumeOptions.CapacityMB = 10 * 1024
return volumes.CreateVolume(volumeOptions)
}
} }
func deletePD(pdName, zone string) error { func deletePD(pdName string) error {
// TODO: make this hit the compute API directly. if testContext.Provider == "gce" {
return exec.Command("gcloud", "compute", "disks", "delete", "--zone="+zone, pdName).Run() zone := testContext.CloudConfig.Zone
// TODO: make this hit the compute API directly.
return exec.Command("gcloud", "compute", "disks", "delete", "--zone="+zone, pdName).Run()
} else {
volumes, ok := testContext.CloudConfig.Provider.(aws_cloud.Volumes)
if !ok {
return fmt.Errorf("Provider does not support volumes")
}
return volumes.DeleteVolume(pdName)
}
} }
func detachPD(hostName, pdName, zone string) error { func detachPD(hostName, pdName string) error {
instanceName := strings.Split(hostName, ".")[0] if testContext.Provider == "gce" {
// TODO: make this hit the compute API directly. instanceName := strings.Split(hostName, ".")[0]
return exec.Command("gcloud", "compute", "instances", "detach-disk", "--zone="+zone, "--disk="+pdName, instanceName).Run()
zone := testContext.CloudConfig.Zone
// TODO: make this hit the compute API directly.
return exec.Command("gcloud", "compute", "instances", "detach-disk", "--zone="+zone, "--disk="+pdName, instanceName).Run()
} else {
volumes, ok := testContext.CloudConfig.Provider.(aws_cloud.Volumes)
if !ok {
return fmt.Errorf("Provider does not support volumes")
}
return volumes.DetachDisk(hostName, pdName)
}
} }
func testPDPod(diskName, targetHost string, readOnly bool) *api.Pod { func testPDPod(diskName, targetHost string, readOnly bool) *api.Pod {
return &api.Pod{ pod := &api.Pod{
TypeMeta: api.TypeMeta{ TypeMeta: api.TypeMeta{
Kind: "Pod", Kind: "Pod",
APIVersion: "v1beta1", APIVersion: "v1beta1",
...@@ -199,18 +240,6 @@ func testPDPod(diskName, targetHost string, readOnly bool) *api.Pod { ...@@ -199,18 +240,6 @@ func testPDPod(diskName, targetHost string, readOnly bool) *api.Pod {
Name: "pd-test-" + string(util.NewUUID()), Name: "pd-test-" + string(util.NewUUID()),
}, },
Spec: api.PodSpec{ Spec: api.PodSpec{
Volumes: []api.Volume{
{
Name: "testpd",
VolumeSource: api.VolumeSource{
GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
PDName: diskName,
FSType: "ext4",
ReadOnly: readOnly,
},
},
},
},
Containers: []api.Container{ Containers: []api.Container{
{ {
Name: "testpd", Name: "testpd",
...@@ -226,4 +255,36 @@ func testPDPod(diskName, targetHost string, readOnly bool) *api.Pod { ...@@ -226,4 +255,36 @@ func testPDPod(diskName, targetHost string, readOnly bool) *api.Pod {
Host: targetHost, Host: targetHost,
}, },
} }
if testContext.Provider == "gce" {
pod.Spec.Volumes = []api.Volume{
{
Name: "testpd",
VolumeSource: api.VolumeSource{
GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
PDName: diskName,
FSType: "ext4",
ReadOnly: readOnly,
},
},
},
}
} else if testContext.Provider == "aws" {
pod.Spec.Volumes = []api.Volume{
{
Name: "testpd",
VolumeSource: api.VolumeSource{
AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
VolumeID: diskName,
FSType: "ext4",
ReadOnly: readOnly,
},
},
},
}
} else {
panic("Unknown provider: " + testContext.Provider)
}
return pod
} }
...@@ -49,7 +49,7 @@ type TestContextType struct { ...@@ -49,7 +49,7 @@ type TestContextType struct {
Host string Host string
RepoRoot string RepoRoot string
Provider string Provider string
GCEConfig GCEConfig CloudConfig CloudConfig
} }
var testContext TestContextType var testContext TestContextType
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment