diff --git a/Gopkg.lock b/Gopkg.lock index 527b295a83..e7741b7daa 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -263,12 +263,13 @@ [[projects]] branch = "master" - digest = "1:57d11d6e542db0131ec25a7271b7605479695a4e7df206481e70ef4874c948bd" + digest = "1:f483805cee5b4708a599d5e761ae3b2c0af2b657fd93405d5d7fd428f096bf9f" name = "github.com/gophercloud/gophercloud" packages = [ ".", "internal", "openstack", + "openstack/blockstorage/v3/volumes", "openstack/common/extensions", "openstack/compute/v2/extensions/attachinterfaces", "openstack/compute/v2/extensions/bootfromvolume", @@ -294,7 +295,7 @@ "pagination", ] pruneopts = "NUT" - revision = "2c55d17f707cc8333ca4f49690cb2970d12a25f6" + revision = "36aaa4d3437eaaebf8722753597c2fc8e12618b7" [[projects]] digest = "1:e27049cb48bc2850a6c00f590a83ee81fc05703543088851c9d6628841844a6d" @@ -461,20 +462,13 @@ "pkg/controller/error", "pkg/controller/machine", "pkg/controller/noderefutil", + "pkg/drain", "pkg/errors", "pkg/util", ] pruneopts = "T" revision = "655e2d6ccdd5774442da004081f933cd8f1f3273" -[[projects]] - branch = "master" - digest = "1:f7646c654e93258958dba300641f8f674d5a9ed015c11119793ba1156e2acbe9" - name = "github.com/openshift/kubernetes-drain" - packages = ["."] - pruneopts = "NUT" - revision = "c2e51be1758efa30d71a4d30dc4e2db86b70a4df" - [[projects]] digest = "1:93b1d84c5fa6d1ea52f4114c37714cddd84d5b78f151b62bb101128dd51399bf" name = "github.com/pborman/uuid" @@ -1190,6 +1184,7 @@ "github.com/coreos/container-linux-config-transpiler/config", "github.com/gophercloud/gophercloud", "github.com/gophercloud/gophercloud/openstack", + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes", "github.com/gophercloud/gophercloud/openstack/common/extensions", "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces", "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume", diff --git a/pkg/apis/openstackproviderconfig/v1alpha1/types.go b/pkg/apis/openstackproviderconfig/v1alpha1/types.go index 2e6347eb99..8cb14be922 100644 --- a/pkg/apis/openstackproviderconfig/v1alpha1/types.go +++ b/pkg/apis/openstackproviderconfig/v1alpha1/types.go @@ -175,6 +175,7 @@ type RootVolume struct { SourceType string `json:"sourceType,omitempty"` SourceUUID string `json:"sourceUUID,omitempty"` DeviceType string `json:"deviceType"` + VolumeType string `json:"volumeType,omitempty"` Size int `json:"diskSize,omitempty"` } diff --git a/pkg/cloud/openstack/clients/machineservice.go b/pkg/cloud/openstack/clients/machineservice.go index e6223f3f6e..f9d11c9ae4 100644 --- a/pkg/cloud/openstack/clients/machineservice.go +++ b/pkg/cloud/openstack/clients/machineservice.go @@ -28,6 +28,7 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" "github.com/gophercloud/gophercloud/openstack/common/extensions" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" @@ -78,6 +79,7 @@ type InstanceService struct { identityClient *gophercloud.ServiceClient networkClient *gophercloud.ServiceClient imagesClient *gophercloud.ServiceClient + volumeClient *gophercloud.ServiceClient regionName string } @@ -224,12 +226,20 @@ func NewInstanceServiceFromCloud(cloud clientconfig.Cloud) (*InstanceService, er return nil, fmt.Errorf("Create ImageClient err: %v", err) } + volumeClient, err := openstack.NewBlockStorageV3(provider, gophercloud.EndpointOpts{ + Region: clientOpts.RegionName, + }) + if err != nil { + return nil, fmt.Errorf("Create VolumeClient err: %v", err) + } + return &InstanceService{ provider: provider, identityClient: identityClient, computeClient: serverClient, networkClient: networkingClient, imagesClient: imagesClient, + volumeClient: volumeClient, regionName: clientOpts.RegionName, }, nil } @@ -413,7 +423,6 @@ func getImageID(is *InstanceService, imageName string) (string, error) { // InstanceCreate creates a compute instance func (is *InstanceService) InstanceCreate(clusterName string, name string, clusterSpec *openstackconfigv1.OpenstackClusterProviderSpec, config *openstackconfigv1.OpenstackProviderSpec, cmd string, keyName string, configClient configclient.ConfigV1Interface) (instance *Instance, err error) { - var createOpts servers.CreateOptsBuilder if config == nil { return nil, fmt.Errorf("create Options need be specified to create instace") } @@ -590,7 +599,7 @@ func (is *InstanceService) InstanceCreate(clusterName string, name string, clust return nil, fmt.Errorf("Create new server err: %v", err) } - serverCreateOpts := servers.CreateOpts{ + var serverCreateOpts servers.CreateOptsBuilder = servers.CreateOpts{ Name: name, ImageRef: imageID, FlavorName: config.Flavor, @@ -608,21 +617,75 @@ func (is *InstanceService) InstanceCreate(clusterName string, name string, clust if config.RootVolume != nil && config.RootVolume.Size != 0 { var blocks []bootfromvolume.BlockDevice + volumeID := config.RootVolume.SourceUUID + + // change serverCreateOpts to exclude imageRef from them + serverCreateOpts = servers.CreateOpts{ + Name: name, + FlavorName: config.Flavor, + AvailabilityZone: config.AvailabilityZone, + Networks: ports_list, + UserData: []byte(userData), + SecurityGroups: securityGroups, + ServiceClient: is.computeClient, + Tags: serverTags, + Metadata: config.ServerMetadata, + ConfigDrive: config.ConfigDrive, + } + + if bootfromvolume.SourceType(config.RootVolume.SourceType) == bootfromvolume.SourceImage { + // if source type is "image" then we have to create a volume from the image first + klog.Infof("Creating a bootable volume from image %v.", config.RootVolume.SourceUUID) + + imageID, err := getImageID(is, config.RootVolume.SourceUUID) + if err != nil { + return nil, fmt.Errorf("Create new server err: %v", err) + } + + // Create a volume first + volumeCreateOpts := volumes.CreateOpts{ + Size: config.RootVolume.Size, + VolumeType: config.RootVolume.VolumeType, + ImageID: imageID, + // The same name as the instance + Name: name, + } + + volume, err := volumes.Create(is.volumeClient, volumeCreateOpts).Extract() + if err != nil { + return nil, fmt.Errorf("Create bootable volume err: %v", err) + } + + volumeID = volume.ID + + err = volumes.WaitForStatus(is.volumeClient, volumeID, "available", 300) + if err != nil { + klog.Infof("Bootable volume %v creation failed. Removing...", volumeID) + err = volumes.Delete(is.volumeClient, volumeID, volumes.DeleteOpts{}).ExtractErr() + if err != nil { + return nil, fmt.Errorf("Bootable volume deletion err: %v", err) + } + + return nil, fmt.Errorf("Bootable volume %v is not available err: %v", volumeID, err) + } + + klog.Infof("Bootable volume %v was created successfully.", volumeID) + } + block := bootfromvolume.BlockDevice{ - SourceType: bootfromvolume.SourceType(config.RootVolume.SourceType), + SourceType: bootfromvolume.SourceVolume, BootIndex: 0, - UUID: config.RootVolume.SourceUUID, + UUID: volumeID, DeleteOnTermination: true, DestinationType: bootfromvolume.DestinationVolume, - VolumeSize: config.RootVolume.Size, - DeviceType: config.RootVolume.DeviceType, } blocks = append(blocks, block) - createOpts = bootfromvolume.CreateOptsExt{ - CreateOptsBuilder: createOpts, + serverCreateOpts = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, BlockDevice: blocks, } + } server, err := servers.Create(is.computeClient, keypairs.CreateOptsExt{ @@ -632,6 +695,7 @@ func (is *InstanceService) InstanceCreate(clusterName string, name string, clust if err != nil { return nil, fmt.Errorf("Create new server err: %v", err) } + is.computeClient.Microversion = "" return serverToInstance(server), nil } diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/doc.go new file mode 100644 index 0000000000..307b8b12d2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/doc.go @@ -0,0 +1,5 @@ +// Package volumes provides information and interaction with volumes in the +// OpenStack Block Storage service. A volume is a detachable block storage +// device, akin to a USB hard drive. It can only be attached to one instance at +// a time. +package volumes diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/requests.go new file mode 100644 index 0000000000..25f70b27c1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/requests.go @@ -0,0 +1,237 @@ +package volumes + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Volume. This object is passed to +// the volumes.Create function. For more information about these parameters, +// see the Volume object. +type CreateOpts struct { + // The size of the volume, in GB + Size int `json:"size" required:"true"` + // The availability zone + AvailabilityZone string `json:"availability_zone,omitempty"` + // ConsistencyGroupID is the ID of a consistency group + ConsistencyGroupID string `json:"consistencygroup_id,omitempty"` + // The volume description + Description string `json:"description,omitempty"` + // One or more metadata key and value pairs to associate with the volume + Metadata map[string]string `json:"metadata,omitempty"` + // The volume name + Name string `json:"name,omitempty"` + // the ID of the existing volume snapshot + SnapshotID string `json:"snapshot_id,omitempty"` + // SourceReplica is a UUID of an existing volume to replicate with + SourceReplica string `json:"source_replica,omitempty"` + // the ID of the existing volume + SourceVolID string `json:"source_volid,omitempty"` + // The ID of the image from which you want to create the volume. + // Required to create a bootable volume. + ImageID string `json:"imageRef,omitempty"` + // The associated volume type + VolumeType string `json:"volume_type,omitempty"` + // Multiattach denotes if the volume is multi-attach capable. + Multiattach bool `json:"multiattach,omitempty"` +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToVolumeDeleteQuery() (string, error) +} + +// DeleteOpts contains options for deleting a Volume. This object is passed to +// the volumes.Delete function. +type DeleteOpts struct { + // Delete all snapshots of this volume as well. + Cascade bool `q:"cascade"` +} + +// ToLoadBalancerDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToVolumeDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder) (r DeleteResult) { + url := deleteURL(client, id) + if opts != nil { + query, err := opts.ToVolumeDeleteQuery() + if err != nil { + r.Err = err + return + } + url += query + } + _, r.Err = client.Delete(url, nil) + return +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeListQuery() (string, error) +} + +// ListOpts holds options for listing Volumes. It is passed to the volumes.List +// function. +type ListOpts struct { + // AllTenants will retrieve volumes of all tenants/projects. + AllTenants bool `q:"all_tenants"` + + // Metadata will filter results based on specified metadata. + Metadata map[string]string `q:"metadata"` + + // Name will filter by the specified volume name. + Name string `q:"name"` + + // Status will filter by the specified status. + Status string `q:"status"` + + // TenantID will filter by a specific tenant/project ID. + // Setting AllTenants is required for this. + TenantID string `q:"project_id"` + + // Comma-separated list of sort keys and optional sort directions in the + // form of [:]. + Sort string `q:"sort"` + + // Requests a page size of items. + Limit int `q:"limit"` + + // Used in conjunction with limit to return a slice of items. + Offset int `q:"offset"` + + // The ID of the last-seen item. + Marker string `q:"marker"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVolumeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVolumeUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + + listOpts := ListOpts{ + Name: name, + } + + pages, err := List(client, listOpts).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractVolumes(pages) + if err != nil { + return "", err + } + + for _, s := range all { + if s.Name == name { + count++ + id = s.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "volume"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"} + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/results.go new file mode 100644 index 0000000000..3a33b5864b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/results.go @@ -0,0 +1,172 @@ +package volumes + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Attachment represents a Volume Attachment record +type Attachment struct { + AttachedAt time.Time `json:"-"` + AttachmentID string `json:"attachment_id"` + Device string `json:"device"` + HostName string `json:"host_name"` + ID string `json:"id"` + ServerID string `json:"server_id"` + VolumeID string `json:"volume_id"` +} + +// UnmarshalJSON is our unmarshalling helper +func (r *Attachment) UnmarshalJSON(b []byte) error { + type tmp Attachment + var s struct { + tmp + AttachedAt gophercloud.JSONRFC3339MilliNoZ `json:"attached_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Attachment(s.tmp) + + r.AttachedAt = time.Time(s.AttachedAt) + + return err +} + +// Volume contains all the information associated with an OpenStack Volume. +type Volume struct { + // Unique identifier for the volume. + ID string `json:"id"` + // Current status of the volume. + Status string `json:"status"` + // Size of the volume in GB. + Size int `json:"size"` + // AvailabilityZone is which availability zone the volume is in. + AvailabilityZone string `json:"availability_zone"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Instances onto which the volume is attached. + Attachments []Attachment `json:"attachments"` + // Human-readable display name for the volume. + Name string `json:"name"` + // Human-readable description for the volume. + Description string `json:"description"` + // The type of volume to create, either SATA or SSD. + VolumeType string `json:"volume_type"` + // The ID of the snapshot from which the volume was created + SnapshotID string `json:"snapshot_id"` + // The ID of another block storage volume from which the current volume was created + SourceVolID string `json:"source_volid"` + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `json:"metadata"` + // UserID is the id of the user who created the volume. + UserID string `json:"user_id"` + // Indicates whether this is a bootable volume. + Bootable string `json:"bootable"` + // Encrypted denotes if the volume is encrypted. + Encrypted bool `json:"encrypted"` + // ReplicationStatus is the status of replication. + ReplicationStatus string `json:"replication_status"` + // ConsistencyGroupID is the consistency group ID. + ConsistencyGroupID string `json:"consistencygroup_id"` + // Multiattach denotes if the volume is multi-attach capable. + Multiattach bool `json:"multiattach"` + // Image metadata entries, only included for volumes that were created from an image, or from a snapshot of a volume originally created from an image. + VolumeImageMetadata map[string]string `json:"volume_image_metadata"` +} + +// UnmarshalJSON another unmarshalling function +func (r *Volume) UnmarshalJSON(b []byte) error { + type tmp Volume + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VolumePage is a pagination.pager that is returned from a call to the List function. +type VolumePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r VolumePage) IsEmpty() (bool, error) { + volumes, err := ExtractVolumes(r) + return len(volumes) == 0, err +} + +func (page VolumePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"volumes_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(r pagination.Page) ([]Volume, error) { + var s []Volume + err := ExtractVolumesInto(r, &s) + return s, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + var s Volume + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractInto converts our response data into a volume struct +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "volume") +} + +// ExtractVolumesInto similar to ExtractInto but operates on a `list` of volumes +func ExtractVolumesInto(r pagination.Page, v interface{}) error { + return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes") +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/urls.go new file mode 100644 index 0000000000..170724905a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes", "detail") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/util.go b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/util.go new file mode 100644 index 0000000000..e86c1b4b4e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/gophercloud/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go index 30c6170117..d2346d4b42 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go @@ -75,6 +75,10 @@ type BlockDevice struct { // DiskBus is the bus type of the block devices. // Examples of this are ide, usb, virtio, scsi, etc. DiskBus string `json:"disk_bus,omitempty"` + + // VolumeType is the volume type of the block device. + // This requires Compute API microversion 2.67 or later. + VolumeType string `json:"volume_type,omitempty"` } // CreateOptsExt is a structure that extends the server `CreateOpts` structure diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go index 2d20fa6f4b..e4d766b232 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go @@ -134,9 +134,9 @@ func Get(c *gophercloud.ServiceClient, token string) (r GetResult) { OkCodes: []int{200, 203}, }) if resp != nil { - r.Err = err r.Header = resp.Header } + r.Err = err return } diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go index 6f26c96bcd..8af4d634cf 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go @@ -144,6 +144,15 @@ func (r commonResult) ExtractProject() (*Project, error) { return s.Project, err } +// ExtractDomain returns Domain to which User is authorized. +func (r commonResult) ExtractDomain() (*Domain, error) { + var s struct { + Domain *Domain `json:"domain"` + } + err := r.ExtractInto(&s) + return s.Domain, err +} + // CreateResult is the response from a Create request. Use ExtractToken() // to interpret it as a Token, or ExtractServiceCatalog() to interpret it // as a service catalog. diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go index 468952b3e4..960862bb38 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go @@ -1,6 +1,9 @@ package groups import ( + "encoding/json" + "time" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" "github.com/gophercloud/gophercloud/pagination" @@ -25,6 +28,11 @@ type SecGroup struct { // TenantID is the project owner of the security group. TenantID string `json:"tenant_id"` + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // security group last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + // ProjectID is the project owner of the security group. ProjectID string `json:"project_id"` @@ -32,6 +40,44 @@ type SecGroup struct { Tags []string `json:"tags"` } +func (r *SecGroup) UnmarshalJSON(b []byte) error { + type tmp SecGroup + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = SecGroup(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = SecGroup(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + // SecGroupPage is the page returned by a pager when traversing over a // collection of security groups. type SecGroupPage struct { diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go index f03067415f..80ca45c06e 100644 --- a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go @@ -1,6 +1,9 @@ package networks import ( + "encoding/json" + "time" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/pagination" ) @@ -70,6 +73,11 @@ type Network struct { // TenantID is the project owner of the network. TenantID string `json:"tenant_id"` + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // network last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + // ProjectID is the project owner of the network. ProjectID string `json:"project_id"` @@ -84,6 +92,44 @@ type Network struct { Tags []string `json:"tags"` } +func (r *Network) UnmarshalJSON(b []byte) error { + type tmp Network + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Network(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Network(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + // NetworkPage is the page returned by a pager when traversing over a // collection of networks. type NetworkPage struct { diff --git a/vendor/github.com/openshift/kubernetes-drain/LICENSE b/vendor/github.com/openshift/kubernetes-drain/LICENSE deleted file mode 100644 index d645695673..0000000000 --- a/vendor/github.com/openshift/kubernetes-drain/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/vendor/github.com/openshift/kubernetes-drain/drain.go b/vendor/github.com/openshift/kubernetes-drain/drain.go deleted file mode 100644 index a6e21ee774..0000000000 --- a/vendor/github.com/openshift/kubernetes-drain/drain.go +++ /dev/null @@ -1,594 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package drain - -import ( - "errors" - "fmt" - "math" - "sort" - "strings" - "time" - - golog "github.com/go-log/log" - - corev1 "k8s.io/api/core/v1" - policyv1beta1 "k8s.io/api/policy/v1beta1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" - utilerrors "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/kubernetes" - typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" - typedextensionsv1beta1 "k8s.io/client-go/kubernetes/typed/extensions/v1beta1" - typedpolicyv1beta1 "k8s.io/client-go/kubernetes/typed/policy/v1beta1" -) - -type DrainOptions struct { - // Continue even if there are pods not managed by a ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet. - Force bool - - // Ignore DaemonSet-managed pods. - IgnoreDaemonsets bool - - // Period of time in seconds given to each pod to terminate - // gracefully. If negative, the default value specified in the pod - // will be used. - GracePeriodSeconds int - - // The length of time to wait before giving up on deletion or - // eviction. Zero means infinite. - Timeout time.Duration - - // Continue even if there are pods using emptyDir (local data that - // will be deleted when the node is drained). - DeleteLocalData bool - - // Namespace to filter pods on the node. - Namespace string - - // Label selector to filter pods on the node. - Selector labels.Selector - - // Logger allows callers to plug in their preferred logger. - Logger golog.Logger -} - -// Takes a pod and returns a bool indicating whether or not to operate on the -// pod, an optional warning message, and an optional fatal error. -type podFilter func(corev1.Pod) (include bool, w *warning, f *fatal) -type warning struct { - string -} -type fatal struct { - string -} - -const ( - EvictionKind = "Eviction" - EvictionSubresource = "pods/eviction" - - kDaemonsetFatal = "DaemonSet-managed pods (use IgnoreDaemonsets to ignore)" - kDaemonsetWarning = "ignoring DaemonSet-managed pods" - kLocalStorageFatal = "pods with local storage (use DeleteLocalData to override)" - kLocalStorageWarning = "deleting pods with local storage" - kUnmanagedFatal = "pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet (use Force to override)" - kUnmanagedWarning = "deleting pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet" -) - -// GetNodes looks up the nodes (either given by name as arguments or -// by the Selector option). -func GetNodes(client typedcorev1.NodeInterface, nodes []string, selector string) (out []*corev1.Node, err error) { - if len(nodes) == 0 && len(selector) == 0 { - return nil, nil - } - - if len(selector) > 0 && len(nodes) > 0 { - return nil, errors.New("cannot specify both node names and a selector option") - } - - out = []*corev1.Node{} - - for _, node := range nodes { - node, err := client.Get(node, metav1.GetOptions{}) - if err != nil { - return nil, err - } - out = append(out, node) - } - - if len(selector) > 0 { - nodes, err := client.List(metav1.ListOptions{ - LabelSelector: selector, - }) - if err != nil { - return nil, err - } - for _, node := range nodes.Items { - out = append(out, &node) - } - } - - return out, nil -} - -// Drain nodes in preparation for maintenance. -// -// The given nodes will be marked unschedulable to prevent new pods from arriving. -// Drain evicts the pods if the APIServer supports eviction -// (http://kubernetes.io/docs/admin/disruptions/). Otherwise, it will use normal DELETE -// to delete the pods. -// Drain evicts or deletes all pods except mirror pods (which cannot be deleted through -// the API server). If there are DaemonSet-managed pods, Drain will not proceed -// without IgnoreDaemonsets, and regardless it will not delete any -// DaemonSet-managed pods, because those pods would be immediately replaced by the -// DaemonSet controller, which ignores unschedulable markings. If there are any -// pods that are neither mirror pods nor managed by ReplicationController, -// ReplicaSet, DaemonSet, StatefulSet or Job, then Drain will not delete any pods unless you -// use Force. Force will also allow deletion to proceed if the managing resource of one -// or more pods is missing. -// -// Drain waits for graceful termination. You should not operate on the machine until -// the command completes. -// -// When you are ready to put the nodes back into service, use Uncordon, which -// will make the nodes schedulable again. -// -// ![Workflow](http://kubernetes.io/images/docs/kubectl_drain.svg) -func Drain(client kubernetes.Interface, nodes []*corev1.Node, options *DrainOptions) (err error) { - nodeInterface := client.CoreV1().Nodes() - for _, node := range nodes { - if err := Cordon(nodeInterface, node, options.Logger); err != nil { - return err - } - } - - drainedNodes := sets.NewString() - var fatal error - - for _, node := range nodes { - err := DeleteOrEvictPods(client, node, options) - if err == nil { - drainedNodes.Insert(node.Name) - logf(options.Logger, "drained node %q", node.Name) - } else { - log(options.Logger, err) - logf(options.Logger, "unable to drain node %q", node.Name) - remainingNodes := []string{} - fatal = err - for _, remainingNode := range nodes { - if drainedNodes.Has(remainingNode.Name) { - continue - } - remainingNodes = append(remainingNodes, remainingNode.Name) - } - - if len(remainingNodes) > 0 { - sort.Strings(remainingNodes) - logf(options.Logger, "there are pending nodes to be drained: %s", strings.Join(remainingNodes, ",")) - } - } - } - - return fatal -} - -// DeleteOrEvictPods deletes or (where supported) evicts pods from the -// target node and waits until the deletion/eviction completes, -// Timeout elapses, or an error occurs. -func DeleteOrEvictPods(client kubernetes.Interface, node *corev1.Node, options *DrainOptions) error { - pods, err := getPodsForDeletion(client, node, options) - if err != nil { - return err - } - - err = deleteOrEvictPods(client, pods, options) - if err != nil { - pendingPods, newErr := getPodsForDeletion(client, node, options) - if newErr != nil { - return newErr - } - pendingNames := make([]string, len(pendingPods)) - for i, pendingPod := range pendingPods { - pendingNames[i] = pendingPod.Name - } - sort.Strings(pendingNames) - logf(options.Logger, "failed to evict pods from node %q (pending pods: %s): %v", node.Name, strings.Join(pendingNames, ","), err) - } - return err -} - -func getPodController(pod corev1.Pod) *metav1.OwnerReference { - return metav1.GetControllerOf(&pod) -} - -func (o *DrainOptions) unreplicatedFilter(pod corev1.Pod) (bool, *warning, *fatal) { - // any finished pod can be removed - if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { - return true, nil, nil - } - - controllerRef := getPodController(pod) - if controllerRef != nil { - return true, nil, nil - } - if o.Force { - return true, &warning{kUnmanagedWarning}, nil - } - - return false, nil, &fatal{kUnmanagedFatal} -} - -type DaemonSetFilterOptions struct { - client typedextensionsv1beta1.ExtensionsV1beta1Interface - force bool - ignoreDaemonSets bool -} - -func (o *DaemonSetFilterOptions) daemonSetFilter(pod corev1.Pod) (bool, *warning, *fatal) { - // Note that we return false in cases where the pod is DaemonSet managed, - // regardless of flags. We never delete them, the only question is whether - // their presence constitutes an error. - // - // The exception is for pods that are orphaned (the referencing - // management resource - including DaemonSet - is not found). - // Such pods will be deleted if Force is used. - controllerRef := getPodController(pod) - if controllerRef == nil || controllerRef.Kind != "DaemonSet" { - return true, nil, nil - } - - if _, err := o.client.DaemonSets(pod.Namespace).Get(controllerRef.Name, metav1.GetOptions{}); err != nil { - // remove orphaned pods with a warning if Force is used - if apierrors.IsNotFound(err) && o.force { - return true, &warning{err.Error()}, nil - } - return false, nil, &fatal{err.Error()} - } - - if !o.ignoreDaemonSets { - return false, nil, &fatal{kDaemonsetFatal} - } - - return false, &warning{kDaemonsetWarning}, nil -} - -func mirrorPodFilter(pod corev1.Pod) (bool, *warning, *fatal) { - if _, found := pod.ObjectMeta.Annotations[corev1.MirrorPodAnnotationKey]; found { - return false, nil, nil - } - return true, nil, nil -} - -func hasLocalStorage(pod corev1.Pod) bool { - for _, volume := range pod.Spec.Volumes { - if volume.EmptyDir != nil { - return true - } - } - - return false -} - -func (o *DrainOptions) localStorageFilter(pod corev1.Pod) (bool, *warning, *fatal) { - if !hasLocalStorage(pod) { - return true, nil, nil - } - if !o.DeleteLocalData { - return false, nil, &fatal{kLocalStorageFatal} - } - return true, &warning{kLocalStorageWarning}, nil -} - -// Map of status message to a list of pod names having that status. -type podStatuses map[string][]string - -func (ps podStatuses) message() string { - msgs := []string{} - - for key, pods := range ps { - msgs = append(msgs, fmt.Sprintf("%s: %s", key, strings.Join(pods, ", "))) - } - return strings.Join(msgs, "; ") -} - -// getPodsForDeletion receives resource info for a node, and returns all the pods from the given node that we -// are planning on deleting. If there are any pods preventing us from deleting, we return that list in an error. -func getPodsForDeletion(client kubernetes.Interface, node *corev1.Node, options *DrainOptions) (pods []corev1.Pod, err error) { - listOptions := metav1.ListOptions{ - FieldSelector: fields.SelectorFromSet(fields.Set{"spec.nodeName": node.Name}).String(), - } - if options.Selector != nil { - listOptions.LabelSelector = options.Selector.String() - } - podList, err := client.CoreV1().Pods(options.Namespace).List(listOptions) - if err != nil { - return pods, err - } - - ws := podStatuses{} - fs := podStatuses{} - - daemonSetOptions := &DaemonSetFilterOptions{ - client: client.ExtensionsV1beta1(), - force: options.Force, - ignoreDaemonSets: options.IgnoreDaemonsets, - } - - for _, pod := range podList.Items { - podOk := true - for _, filt := range []podFilter{daemonSetOptions.daemonSetFilter, mirrorPodFilter, options.localStorageFilter, options.unreplicatedFilter} { - filterOk, w, f := filt(pod) - - podOk = podOk && filterOk - if w != nil { - ws[w.string] = append(ws[w.string], pod.Name) - } - if f != nil { - fs[f.string] = append(fs[f.string], pod.Name) - } - - // short-circuit as soon as pod not ok - // at that point, there is no reason to run pod - // through any additional filters - if !podOk { - break - } - } - if podOk { - pods = append(pods, pod) - } - } - - if len(fs) > 0 { - return []corev1.Pod{}, errors.New(fs.message()) - } - if len(ws) > 0 { - log(options.Logger, ws.message()) - } - return pods, nil -} - -func evictPod(client typedpolicyv1beta1.PolicyV1beta1Interface, pod corev1.Pod, policyGroupVersion string, gracePeriodSeconds int) error { - deleteOptions := &metav1.DeleteOptions{} - if gracePeriodSeconds >= 0 { - gracePeriod := int64(gracePeriodSeconds) - deleteOptions.GracePeriodSeconds = &gracePeriod - } - eviction := &policyv1beta1.Eviction{ - TypeMeta: metav1.TypeMeta{ - APIVersion: policyGroupVersion, - Kind: EvictionKind, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: pod.Name, - Namespace: pod.Namespace, - }, - DeleteOptions: deleteOptions, - } - return client.Evictions(eviction.Namespace).Evict(eviction) -} - -// deleteOrEvictPods deletes or evicts the pods on the api server -func deleteOrEvictPods(client kubernetes.Interface, pods []corev1.Pod, options *DrainOptions) error { - if len(pods) == 0 { - return nil - } - - policyGroupVersion, err := SupportEviction(client) - if err != nil { - return err - } - - getPodFn := func(namespace, name string) (*corev1.Pod, error) { - return client.CoreV1().Pods(options.Namespace).Get(name, metav1.GetOptions{}) - } - - if len(policyGroupVersion) > 0 { - // Remember to change change the URL manipulation func when Evction's version change - return evictPods(client.PolicyV1beta1(), pods, policyGroupVersion, options, getPodFn) - } else { - return deletePods(client.CoreV1(), pods, options, getPodFn) - } -} - -func evictPods(client typedpolicyv1beta1.PolicyV1beta1Interface, pods []corev1.Pod, policyGroupVersion string, options *DrainOptions, getPodFn func(namespace, name string) (*corev1.Pod, error)) error { - returnCh := make(chan error, 1) - - for _, pod := range pods { - go func(pod corev1.Pod, returnCh chan error) { - var err error - for { - err = evictPod(client, pod, policyGroupVersion, options.GracePeriodSeconds) - if err == nil { - break - } else if apierrors.IsNotFound(err) { - returnCh <- nil - return - } else if apierrors.IsTooManyRequests(err) { - logf(options.Logger, "error when evicting pod %q (will retry after 5s): %v", pod.Name, err) - time.Sleep(5 * time.Second) - } else { - returnCh <- fmt.Errorf("error when evicting pod %q: %v", pod.Name, err) - return - } - } - podArray := []corev1.Pod{pod} - _, err = waitForDelete(podArray, 1*time.Second, time.Duration(math.MaxInt64), true, options.Logger, getPodFn) - if err == nil { - returnCh <- nil - } else { - returnCh <- fmt.Errorf("error when waiting for pod %q terminating: %v", pod.Name, err) - } - }(pod, returnCh) - } - - doneCount := 0 - var errors []error - - // 0 timeout means infinite, we use MaxInt64 to represent it. - var globalTimeout time.Duration - if options.Timeout == 0 { - globalTimeout = time.Duration(math.MaxInt64) - } else { - globalTimeout = options.Timeout - } - globalTimeoutCh := time.After(globalTimeout) - numPods := len(pods) - for doneCount < numPods { - select { - case err := <-returnCh: - doneCount++ - if err != nil { - errors = append(errors, err) - } - case <-globalTimeoutCh: - return fmt.Errorf("Drain did not complete within %v", globalTimeout) - } - } - return utilerrors.NewAggregate(errors) -} - -func deletePods(client typedcorev1.CoreV1Interface, pods []corev1.Pod, options *DrainOptions, getPodFn func(namespace, name string) (*corev1.Pod, error)) error { - // 0 timeout means infinite, we use MaxInt64 to represent it. - var globalTimeout time.Duration - if options.Timeout == 0 { - globalTimeout = time.Duration(math.MaxInt64) - } else { - globalTimeout = options.Timeout - } - deleteOptions := &metav1.DeleteOptions{} - if options.GracePeriodSeconds >= 0 { - gracePeriodSeconds := int64(options.GracePeriodSeconds) - deleteOptions.GracePeriodSeconds = &gracePeriodSeconds - } - for _, pod := range pods { - err := client.Pods(pod.Namespace).Delete(pod.Name, deleteOptions) - if err != nil && !apierrors.IsNotFound(err) { - return err - } - } - _, err := waitForDelete(pods, 1*time.Second, globalTimeout, false, options.Logger, getPodFn) - return err -} - -func waitForDelete(pods []corev1.Pod, interval, timeout time.Duration, usingEviction bool, logger golog.Logger, getPodFn func(string, string) (*corev1.Pod, error)) ([]corev1.Pod, error) { - var verbStr string - if usingEviction { - verbStr = "evicted" - } else { - verbStr = "deleted" - } - - err := wait.PollImmediate(interval, timeout, func() (bool, error) { - pendingPods := []corev1.Pod{} - for i, pod := range pods { - p, err := getPodFn(pod.Namespace, pod.Name) - if apierrors.IsNotFound(err) || (p != nil && p.ObjectMeta.UID != pod.ObjectMeta.UID) { - logf(logger, "pod %q removed (%s)", pod.Name, verbStr) - continue - } else if err != nil { - return false, err - } else { - pendingPods = append(pendingPods, pods[i]) - } - } - pods = pendingPods - if len(pendingPods) > 0 { - return false, nil - } - return true, nil - }) - return pods, err -} - -// SupportEviction uses Discovery API to find out if the server -// supports the eviction subresource. If supported, it will return -// its groupVersion; otherwise it will return an empty string. -func SupportEviction(clientset kubernetes.Interface) (string, error) { - discoveryClient := clientset.Discovery() - groupList, err := discoveryClient.ServerGroups() - if err != nil { - return "", err - } - foundPolicyGroup := false - var policyGroupVersion string - for _, group := range groupList.Groups { - if group.Name == "policy" { - foundPolicyGroup = true - policyGroupVersion = group.PreferredVersion.GroupVersion - break - } - } - if !foundPolicyGroup { - return "", nil - } - resourceList, err := discoveryClient.ServerResourcesForGroupVersion("v1") - if err != nil { - return "", err - } - for _, resource := range resourceList.APIResources { - if resource.Name == EvictionSubresource && resource.Kind == EvictionKind { - return policyGroupVersion, nil - } - } - return "", nil -} - -// Cordon marks a node "Unschedulable". This method is idempotent. -func Cordon(client typedcorev1.NodeInterface, node *corev1.Node, logger golog.Logger) error { - return cordonOrUncordon(client, node, logger, true) -} - -// Uncordon marks a node "Schedulable". This method is idempotent. -func Uncordon(client typedcorev1.NodeInterface, node *corev1.Node, logger golog.Logger) error { - return cordonOrUncordon(client, node, logger, false) -} - -func cordonOrUncordon(client typedcorev1.NodeInterface, node *corev1.Node, logger golog.Logger, desired bool) error { - unsched := node.Spec.Unschedulable - if unsched == desired { - return nil - } - - patch := []byte(fmt.Sprintf("{\"spec\":{\"unschedulable\":%t}}", desired)) - _, err := client.Patch(node.Name, types.StrategicMergePatchType, patch) - if err == nil { - verbStr := "cordoned" - if !desired { - verbStr = "un" + verbStr - } - logf(logger, "%s node %q", verbStr, node.Name) - } - return err -} - -func log(logger golog.Logger, v ...interface{}) { - if logger != nil { - logger.Log(v...) - } -} - -func logf(logger golog.Logger, format string, v ...interface{}) { - if logger != nil { - logger.Logf(format, v...) - } -}