Skip to content

Add external storage support for etcd #228

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

Merged
merged 4 commits into from
Jan 12, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/v1alpha3/domachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type DOMachineSpec struct {
Size string `json:"size"`
// Droplet image can be image id or slug. See https://developers.digitalocean.com/documentation/v2/#list-all-images
Image intstr.IntOrString `json:"image"`
// DataDisks specifies the parameters that are used to add one or more data disks to the machine
DataDisks []DataDisk `json:"dataDisks,omitempty"`
// SSHKeys is the ssh key id or fingerprint to attach in DigitalOcean droplet.
// It must be available on DigitalOcean account. See https://developers.digitalocean.com/documentation/v2/#list-all-keys
SSHKeys []intstr.IntOrString `json:"sshKeys"`
Expand Down
27 changes: 26 additions & 1 deletion api/v1alpha3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ limitations under the License.

package v1alpha3

import "strings"
import (
"fmt"
"strings"
)

// APIEndpoint represents a reachable Kubernetes API endpoint.
type APIEndpoint struct {
Expand Down Expand Up @@ -72,6 +75,28 @@ type DOMachineTemplateResource struct {
Spec DOMachineSpec `json:"spec"`
}

// DataDiskName is the volume name used for a data disk of a droplet.
// It's in the form of <dropletName>-<dataDiskNameSuffix>.
func DataDiskName(m *DOMachine, suffix string) string {
return DOSafeName(fmt.Sprintf("%s-%s", m.Name, suffix))
}

// DataDisk specifies the parameters that are used to add a data disk to the machine.
type DataDisk struct {
// NameSuffix is the suffix to be appended to the machine name to generate the disk name.
// Each disk name will be in format <dropletName>-<nameSuffix>.
NameSuffix string `json:"nameSuffix"`
// DiskSizeGB is the size in GB to assign to the data disk.
DiskSizeGB int64 `json:"diskSizeGB"`
// FilesystemType to be used on the volume. When provided the volume will
// be automatically formatted.
FilesystemType string `json:"filesystemType,omitempty"`
// FilesystemLabel is the label that is applied to the created filesystem.
// Character limits apply: 16 for ext4; 12 for xfs.
// May only be used in conjunction with filesystemType.
FilesystemLabel string `json:"filesystemLabel,omitempty"`
}

// DONetwork encapsulates DigitalOcean networking configuration.
type DONetwork struct {
// Configures an API Server loadbalancers
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha3/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cloud/scope/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
type DOClients struct {
Actions godo.ActionsService
Droplets godo.DropletsService
Storage godo.StorageService
Images godo.ImagesService
Keys godo.KeysService
LoadBalancers godo.LoadBalancersService
Expand Down
4 changes: 4 additions & 0 deletions cloud/scope/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ func NewClusterScope(params ClusterScopeParams) (*ClusterScope, error) {
params.DOClients.Droplets = session.Droplets
}

if params.DOClients.Storage == nil {
params.DOClients.Storage = session.Storage
}

if params.DOClients.Images == nil {
params.DOClients.Images = session.Images
}
Expand Down
15 changes: 15 additions & 0 deletions cloud/services/computes/droplets.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package computes

import (
"fmt"
"net/http"
"strconv"

Expand Down Expand Up @@ -82,6 +83,19 @@ func (s *Service) CreateDroplet(scope *scope.MachineScope) (*godo.Droplet, error
})
}

volumes := []godo.DropletCreateVolume{}
for _, disk := range scope.DOMachine.Spec.DataDisks {
volName := infrav1.DataDiskName(scope.DOMachine, disk.NameSuffix)
vol, err := s.GetVolumeByName(volName)
if err != nil {
return nil, fmt.Errorf("could not get volume to attach to droplet: %w", err)
}
if vol == nil {
return nil, fmt.Errorf("volume %q does not exist", volName)
}
volumes = append(volumes, godo.DropletCreateVolume{ID: vol.ID})
}

request := &godo.DropletCreateRequest{
Name: instanceName,
Region: s.scope.Region(),
Expand All @@ -92,6 +106,7 @@ func (s *Service) CreateDroplet(scope *scope.MachineScope) (*godo.Droplet, error
},
UserData: bootstrapData,
PrivateNetworking: true,
Volumes: volumes,
}

request.Tags = infrav1.BuildTags(infrav1.BuildTagParams{
Expand Down
69 changes: 69 additions & 0 deletions cloud/services/computes/volumes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
Copyright 2020 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 computes

import (
"fmt"

"github.com/digitalocean/godo"
"github.com/pkg/errors"

infrav1 "sigs.k8s.io/cluster-api-provider-digitalocean/api/v1alpha3"
)

// GetVolumeByName takes a volume name and returns a Volume if found.
func (s *Service) GetVolumeByName(name string) (*godo.Volume, error) {
vols, _, err := s.scope.Storage.ListVolumes(s.ctx, &godo.ListVolumeParams{
Name: name,
Region: s.scope.Region(),
})
if err != nil {
return nil, fmt.Errorf("failed to list volumes: %w", err)
}
if len(vols) == 0 {
return nil, nil
}
if len(vols) > 1 {
return nil, errors.New("volume names are not unique per region")
}
return &vols[0], nil
}

// CreateVolume creates a block storage volume.
func (s *Service) CreateVolume(disk infrav1.DataDisk, volName string) (*godo.Volume, error) {
r := &godo.VolumeCreateRequest{
Region: s.scope.Region(),
Name: volName,
SizeGigaBytes: disk.DiskSizeGB,
FilesystemType: disk.FilesystemType,
FilesystemLabel: disk.FilesystemLabel,
}
v, _, err := s.scope.Storage.CreateVolume(s.ctx, r)
return v, errors.Wrap(err, "failed to create new volume")
}

// DeleteVolume deletes a block storage volume.
func (s *Service) DeleteVolume(id string) error {
s.scope.V(2).Info("Attempting to delete block storage volume", "volume-id", id)

if _, err := s.scope.Storage.DeleteVolume(s.ctx, id); err != nil {
return fmt.Errorf("failed to delete instance with id %q: %w", id, err)
}

s.scope.V(2).Info("Deleted block storage volume", "volume-id", id)
return nil
}
31 changes: 31 additions & 0 deletions config/crd/bases/infrastructure.cluster.x-k8s.io_domachines.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,37 @@ spec:
items:
type: string
type: array
dataDisks:
description: DataDisks specifies the parameters that are used to add
one or more data disks to the machine
items:
description: DataDisk specifies the parameters that are used to
add a data disk to the machine.
properties:
diskSizeGB:
description: DiskSizeGB is the size in GB to assign to the data
disk.
format: int64
type: integer
filesystemLabel:
description: 'FilesystemLabel is the label that is applied to
the created filesystem. Character limits apply: 16 for ext4;
12 for xfs. May only be used in conjunction with filesystemType.'
type: string
filesystemType:
description: FilesystemType to be used on the volume. When provided
the volume will be automatically formatted.
type: string
nameSuffix:
description: NameSuffix is the suffix to be appended to the
machine name to generate the disk name. Each disk name will
be in format <dropletName>-<nameSuffix>.
type: string
required:
- diskSizeGB
- nameSuffix
type: object
type: array
image:
anyOf:
- type: integer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,38 @@ spec:
items:
type: string
type: array
dataDisks:
description: DataDisks specifies the parameters that are used
to add one or more data disks to the machine
items:
description: DataDisk specifies the parameters that are
used to add a data disk to the machine.
properties:
diskSizeGB:
description: DiskSizeGB is the size in GB to assign
to the data disk.
format: int64
type: integer
filesystemLabel:
description: 'FilesystemLabel is the label that is applied
to the created filesystem. Character limits apply:
16 for ext4; 12 for xfs. May only be used in conjunction
with filesystemType.'
type: string
filesystemType:
description: FilesystemType to be used on the volume.
When provided the volume will be automatically formatted.
type: string
nameSuffix:
description: NameSuffix is the suffix to be appended
to the machine name to generate the disk name. Each
disk name will be in format <dropletName>-<nameSuffix>.
type: string
required:
- diskSizeGB
- nameSuffix
type: object
type: array
image:
anyOf:
- type: integer
Expand Down
63 changes: 55 additions & 8 deletions controllers/domachine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controllers

import (
"context"
"fmt"
"strconv"
"time"

Expand Down Expand Up @@ -194,6 +195,27 @@ func (r *DOMachineReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, reterr
return r.reconcile(ctx, machineScope, clusterScope)
}

func (r *DOMachineReconciler) reconcileVolumes(ctx context.Context, mscope *scope.MachineScope, cscope *scope.ClusterScope) (reconcile.Result, error) {
mscope.Info("Reconciling DOMachine Volumes")
computesvc := computes.NewService(ctx, cscope)
domachine := mscope.DOMachine
for _, disk := range domachine.Spec.DataDisks {
volName := infrav1.DataDiskName(domachine, disk.NameSuffix)
vol, err := computesvc.GetVolumeByName(volName)
if err != nil {
return reconcile.Result{}, err
}
if vol == nil {
_, err = computesvc.CreateVolume(disk, volName)
if err != nil {
return reconcile.Result{}, err
}
}
// TODO(gottwald): reconcile disk resizes here (at least grow)
}
return reconcile.Result{}, nil
}

func (r *DOMachineReconciler) reconcile(ctx context.Context, machineScope *scope.MachineScope, clusterScope *scope.ClusterScope) (reconcile.Result, error) {
machineScope.Info("Reconciling DOMachine")
domachine := machineScope.DOMachine
Expand All @@ -217,6 +239,11 @@ func (r *DOMachineReconciler) reconcile(ctx context.Context, machineScope *scope
return reconcile.Result{}, nil
}

// Make sure the droplet volumes are reconciled
if result, err := r.reconcileVolumes(ctx, machineScope, clusterScope); err != nil {
return result, fmt.Errorf("failed to reconcile volumes: %w", err)
}

computesvc := computes.NewService(ctx, clusterScope)
droplet, err := computesvc.GetDroplet(machineScope.GetInstanceID())
if err != nil {
Expand Down Expand Up @@ -259,6 +286,26 @@ func (r *DOMachineReconciler) reconcile(ctx context.Context, machineScope *scope
return reconcile.Result{}, nil
}
}
func (r *DOMachineReconciler) reconcileDeleteVolumes(ctx context.Context, mscope *scope.MachineScope, cscope *scope.ClusterScope) (reconcile.Result, error) {
mscope.Info("Reconciling delete DOMachine Volumes")
computesvc := computes.NewService(ctx, cscope)
domachine := mscope.DOMachine
for _, disk := range domachine.Spec.DataDisks {
volName := infrav1.DataDiskName(domachine, disk.NameSuffix)
vol, err := computesvc.GetVolumeByName(volName)
if err != nil {
return reconcile.Result{}, err
}
if vol == nil {
continue
}
if err = computesvc.DeleteVolume(vol.ID); err != nil {
return reconcile.Result{}, err
}
r.Recorder.Eventf(domachine, corev1.EventTypeNormal, "VolumeDeleted", "Deleted the storage volume - %s", vol.Name)
}
return reconcile.Result{}, nil
}

func (r *DOMachineReconciler) reconcileDelete(ctx context.Context, machineScope *scope.MachineScope, clusterScope *scope.ClusterScope) (reconcile.Result, error) {
machineScope.Info("Reconciling delete DOMachine")
Expand All @@ -270,18 +317,18 @@ func (r *DOMachineReconciler) reconcileDelete(ctx context.Context, machineScope
return reconcile.Result{}, err
}

if droplet == nil {
if droplet != nil {
if err := computesvc.DeleteDroplet(machineScope.GetInstanceID()); err != nil {
return reconcile.Result{}, err
}
} else {
clusterScope.V(2).Info("Unable to locate droplet instance")
r.Recorder.Eventf(domachine, corev1.EventTypeWarning, "NoInstanceFound", "Skip deleting")
controllerutil.RemoveFinalizer(domachine, infrav1.MachineFinalizer)
return reconcile.Result{}, nil
}

if err := computesvc.DeleteDroplet(machineScope.GetInstanceID()); err != nil {
return reconcile.Result{}, err
if result, err := r.reconcileDeleteVolumes(ctx, machineScope, clusterScope); err != nil {
return result, fmt.Errorf("failed to reconcile delete volumes: %w", err)
}

r.Recorder.Eventf(domachine, corev1.EventTypeNormal, "InstanceDeleted", "Deleted a instance - %s", droplet.Name)
r.Recorder.Eventf(domachine, corev1.EventTypeNormal, "InstanceDeleted", "Deleted a instance - %s", machineScope.Name())
controllerutil.RemoveFinalizer(domachine, infrav1.MachineFinalizer)
return reconcile.Result{}, nil
}
Loading