Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ubernetes-Lite GCE: Label volumes with zone information #19995

Merged
3 commits merged into from
Jan 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion cluster/gce/config-default.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ if [[ "${ENABLE_NODE_AUTOSCALER}" == "true" ]]; then
fi

# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota,PersistentVolumeLabel

# Optional: if set to true kube-up will automatically check for existing resources and clean them up.
KUBE_UP_AUTOMATIC_CLEANUP=${KUBE_UP_AUTOMATIC_CLEANUP:-false}
Expand Down
2 changes: 1 addition & 1 deletion cluster/gce/config-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ if [[ "${ENABLE_NODE_AUTOSCALER}" == "true" ]]; then
TARGET_NODE_UTILIZATION="${KUBE_TARGET_NODE_UTILIZATION:-0.7}"
fi

ADMISSION_CONTROL="${KUBE_ADMISSION_CONTROL:-NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota}"
ADMISSION_CONTROL="${KUBE_ADMISSION_CONTROL:-NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota,PersistentVolumeLabel}"

# Optional: if set to true kube-up will automatically check for existing resources and clean them up.
KUBE_UP_AUTOMATIC_CLEANUP=${KUBE_UP_AUTOMATIC_CLEANUP:-false}
Expand Down
38 changes: 36 additions & 2 deletions pkg/cloudprovider/providers/gce/gce.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"time"

"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/cloudprovider"
utilerrors "k8s.io/kubernetes/pkg/util/errors"
"k8s.io/kubernetes/pkg/util/sets"
Expand Down Expand Up @@ -1644,6 +1645,33 @@ func (gce *GCECloud) DeleteDisk(diskToDelete string) error {
return gce.waitForZoneOp(deleteOp, disk.Zone)
}

// Builds the labels that should be automatically added to a PersistentVolume backed by a GCE PD
// Specifically, this builds FailureDomain (zone) and Region labels.
// The PersistentVolumeLabel admission controller calls this and adds the labels when a PV is created.
func (gce *GCECloud) GetAutoLabelsForPD(name string) (map[string]string, error) {
disk, err := gce.getDiskByNameUnknownZone(name)
if err != nil {
return nil, err
}

zone := disk.Zone
region, err := GetGCERegion(zone)
if err != nil {
return nil, err
}

if zone == "" || region == "" {
// Unexpected, but sanity-check
return nil, fmt.Errorf("PD did not have zone/region information: %q", disk.Name)
}

labels := make(map[string]string)
labels[unversioned.LabelZoneFailureDomain] = zone
labels[unversioned.LabelZoneRegion] = region

return labels, nil
}

func (gce *GCECloud) AttachDisk(diskName, instanceID string, readOnly bool) error {
instance, err := gce.getInstanceByName(instanceID)
if err != nil {
Expand Down Expand Up @@ -1736,14 +1764,20 @@ func (gce *GCECloud) getDiskByNameUnknownZone(diskName string) (*gceDisk, error)
// "us-central1-a/mydisk". We could do this for them as part of
// admission control, but that might be a little weird (values changing
// on create)

var found *gceDisk
for _, zone := range gce.managedZones {
disk, err := gce.findDiskByName(diskName, zone)
if err != nil {
return nil, err
}
if disk != nil {
return disk, nil
if found != nil {
return nil, fmt.Errorf("GCE persistent disk name was found in multiple zones: %q", diskName)
}
found = disk
}
if found != nil {
return found, nil
}
return nil, fmt.Errorf("GCE persistent disk not found: %q", diskName)
}
Expand Down
50 changes: 48 additions & 2 deletions plugin/pkg/admission/persistentvolume/label/admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/cloudprovider/providers/aws"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce"
)

func init() {
Expand All @@ -40,8 +41,9 @@ var _ = admission.Interface(&persistentVolumeLabel{})
type persistentVolumeLabel struct {
*admission.Handler

mutex sync.Mutex
ebsVolumes aws.Volumes
mutex sync.Mutex
ebsVolumes aws.Volumes
gceCloudProvider *gce.GCECloud
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid putting provider-specific fields in this provider-independent library?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how - suggestions welcome.

I wanted to implement a Volumes interface, and you can see from the AWS implementation that I did. It was originally intended to be cross-cloud provider, but there were a number of objections raised so I moved Volumes into the aws package. The primary objection was (I believe) that a cloud could implement different types of volumes (e.g. even AWS has NFS & EBS now).

And volume.Spec isn't provider independent either.

One option would be to define aws.Volumes in cloudprovider, but have it take a PersistentVolumeSource. But that isn't quite right either, and would be a fairly large refactor.

In short, I'd love to, but I don't see how to do it in an acceptable way.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I'm happy for us to perhaps try to take on a refactor in a separate PR, after v 1.2.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If volumes are plugins and Kubelet (and others) don't know what plugins exist, anything that escapes those packages is leaky.

Would we instead want to give the same list of plugins to the API server that we give to Kubelet and ControllerManager?

The API server would do the same iteration and CanSupport on each plugin, find the appropriate one, and call Validate(pv) and Admit(pv).

Each volume would know how to validate itself in the API. Missing plugins would cause a real API error. Any business logic regarding labelling and so forth is also internalized to the plugin.

@thockin thoughts?

}

// NewPersistentVolumeLabel returns an admission.Interface implementation which adds labels to PersistentVolume CREATE requests,
Expand Down Expand Up @@ -75,6 +77,13 @@ func (l *persistentVolumeLabel) Admit(a admission.Attributes) (err error) {
}
volumeLabels = labels
}
if volume.Spec.GCEPersistentDisk != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markturansky @saad-ali This should be part of volume plugins, I think. It should NOT be an open-coded list of cases. I'll file a new issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - this came up in review a few lines up ^^^. I agree with the criticism, but it isn't clear to me how best to fix it.

labels, err := l.findGCEPDLabels(volume)
if err != nil {
return admission.NewForbidden(a, fmt.Errorf("error querying GCE PD volume %s: %v", volume.Spec.GCEPersistentDisk.PDName, err))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This broke provisioning (because we don't have an e2e test that would have prevented this change).

The provisioner creates a dummy PV in response to a claim that requires provisioning. The "dummy" ID on the struct was required to pass validation. A controller reconciles this PV with the infrastructure and creates a resource asynchronously.

There is no "dummy" PD in the provider yet, so we can't look it up for labels. This admission controller makes it required that the PD exist. It could be, too, that making a placeholder PV is wrong. I am open to suggestions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened #20564 to track the lack of e2e coverage and #20955 to track the AWS problem, but I didn't realize this had broken it on GCE also. Sorry!

For AWS, I was thinking of creating a volume with a well-known dummy name, which would then be ignored. But that is pretty evil...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can put the "dummy" constant in pkg/volume (as opposed to a plugin) and use that in admission and the plugins themselves.

it is leaky as hell but is far less work than adding plugins and a framework to the API server. Should we consider it as a short term fix until we come up with a better way?

}
volumeLabels = labels
}

if len(volumeLabels) != 0 {
if volume.Labels == nil {
Expand Down Expand Up @@ -129,3 +138,40 @@ func (l *persistentVolumeLabel) getEBSVolumes() (aws.Volumes, error) {
}
return l.ebsVolumes, nil
}

func (l *persistentVolumeLabel) findGCEPDLabels(volume *api.PersistentVolume) (map[string]string, error) {
provider, err := l.getGCECloudProvider()
if err != nil {
return nil, err
}
if provider == nil {
return nil, fmt.Errorf("unable to build GCE cloud provider for PD")
}

labels, err := provider.GetAutoLabelsForPD(volume.Spec.GCEPersistentDisk.PDName)
if err != nil {
return nil, err
}

return labels, err
}

// getGCECloudProvider returns the GCE cloud provider, for use for querying volume labels
func (l *persistentVolumeLabel) getGCECloudProvider() (*gce.GCECloud, error) {
l.mutex.Lock()
defer l.mutex.Unlock()

if l.gceCloudProvider == nil {
cloudProvider, err := cloudprovider.GetCloudProvider("gce", nil)
if err != nil || cloudProvider == nil {
return nil, err
}
gceCloudProvider, ok := cloudProvider.(*gce.GCECloud)
if !ok {
// GetCloudProvider has gone very wrong
return nil, fmt.Errorf("error retrieving GCE cloud provider")
}
l.gceCloudProvider = gceCloudProvider
}
return l.gceCloudProvider, nil
}