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

add support for multiple facilities, metros and reservation IDs #7

Merged
merged 6 commits into from
Aug 25, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tmp/
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,17 @@ Process:
```bash
kubectl apply -f kubernetes/machine-class.yaml
```
1. Fill in the Kubernetes `Secret` with the Equinix Metal API key and deploy:
1. Copy the kubernetes `Secret` template to the temporary directory, then fill it in with your Equinix Metal API key and deploy:
```bash
cp kubernetes/secret.yaml tmp/secret.yaml
vi tmp/secret.yaml # fill in your actual key here
kubectl apply -f tmp/secret.yaml
```
1. Deploy various `Machine` objects and make sure they are created and have the userData you supplied.
```bash
kubectl apply -f kubernetes/secret.yaml
kubectl apply -f kubernetes/machines/
```
1. Deploy a `Machine` object and make sure it is created and has the userData you supplied.
```bash
kubectl apply -f kubernetes/machine.yaml
```
1. Once the `Machine` has passed testing, deploy a `MachineDeployment` and make sure it is created and has the userData you supplied. If you provided it with an actual joinable target cluster and userData to join it, wait until all of the machines join that cluster successfully:
1. Once the `Machine`s have passed testing, deploy a `MachineDeployment` and make sure it is created and has the userData you supplied. If you provided it with an actual joinable target cluster and userData to join it, wait until all of the machines join that cluster successfully:
```bash
kubectl apply -f kubernetes/machine-deployment.yaml
```
Expand Down
62 changes: 58 additions & 4 deletions kubernetes/machine-class.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ provider: EquinixMetal
providerSpec:
projectID: e3db5484-f789-43e1-8aea-a1921cae50dd # UUID of a project with which you have rights
OS: alpine_3 # OS ID or slug goes here
facility:
- ewr1 # Facilities wherein the server can be deployed. Can be one, two or many, or the keyword "any"
- ams1
machineType: x1.small # Type of packet bare-metal machine
metro: ny
machineType: t1.small.x86 # Type of packet bare-metal machine
billingCycle: hourly # billing cycle
tags:
- "Name: sample-machine-name" # Name tag that can be used to identify a machine at Packet
Expand All @@ -22,3 +20,59 @@ providerSpec:
secretRef: # If required
name: test-secret
namespace: default # Namespace where the controller would watch
---
# Sample Equinix Metal machine class
apiVersion: machine.sapcloud.io/v1alpha1
kind: MachineClass
metadata:
name: eqx-mc-ny-facilities
namespace: default # Namespace where the controller would watch
provider: EquinixMetal
providerSpec:
projectID: e3db5484-f789-43e1-8aea-a1921cae50dd # UUID of a project with which you have rights
OS: alpine_3 # OS ID or slug goes here
metro: ny # Metro wherein the server can be deployed, can be one or "any"
facilities:
- ewr1 # Facilities wherein the server can be deployed. Can be zero, one, two or many. MUST be in the metro above
- ny5
machineType: t1.small.x86 # Type of packet bare-metal machine
billingCycle: hourly # billing cycle
tags:
- "Name: sample-machine-name" # Name tag that can be used to identify a machine at Packet
- "kubernetes.io/cluster/YOUR_CLUSTER_NAME: 1" # This is mandatory as the safety controller uses this tag to identify machines created by this controller.
- "kubernetes.io/role/YOUR_ROLE_NAME: 1" # This is mandatory as the safety controller uses this tag to identify machines created by by this controller.
- "tag1: tag1-value" # A set of additional tags attached to a machine (optional)
- "tag2: tag2-value" # A set of additional tags attached to a machine (optional)
secretRef: # If required
name: test-secret
namespace: default # Namespace where the controller would watch
---
# Sample Equinix Metal machine class
apiVersion: machine.sapcloud.io/v1alpha1
kind: MachineClass
metadata:
name: eqx-mc-ny-reserved
namespace: default # Namespace where the controller would watch
provider: EquinixMetal
providerSpec:
projectID: e3db5484-f789-43e1-8aea-a1921cae50dd # UUID of a project with which you have rights
OS: alpine_3 # OS ID or slug goes here
metro: ny
facilities:
- ewr1 # Facilities wherein the server can be deployed. Can be zero, one, two or many. MUST be in the metro above
- ny5
machineType: t1.small.x86 # Type of packet bare-metal machine
billingCycle: hourly # billing cycle
tags:
- "Name: sample-machine-name" # Name tag that can be used to identify a machine at Packet
- "kubernetes.io/cluster/YOUR_CLUSTER_NAME: 1" # This is mandatory as the safety controller uses this tag to identify machines created by this controller.
- "kubernetes.io/role/YOUR_ROLE_NAME: 1" # This is mandatory as the safety controller uses this tag to identify machines created by by this controller.
- "tag1: tag1-value" # A set of additional tags attached to a machine (optional)
- "tag2: tag2-value" # A set of additional tags attached to a machine (optional)
reservationIDs:
- 932eecda-6808-44b9-a3be-3abef49796ef
- 558c4d16-3523-4456-9c3a-73722920a7bb
reservedDevicesOnly: true
secretRef: # If required
name: test-secret
namespace: default # Namespace where the controller would watch
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ spec:
class:
kind: MachineClass
name: eqx-mc
namespace: default
9 changes: 9 additions & 0 deletions kubernetes/machines/machine-reservations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: machine.sapcloud.io/v1alpha1
kind: Machine
metadata:
name: eqx-test-machine-reserved
namespace: default
spec:
class:
kind: MachineClass
name: eqx-mc-ny-reserved
9 changes: 9 additions & 0 deletions kubernetes/machines/machine-specific-facilities.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: machine.sapcloud.io/v1alpha1
kind: Machine
metadata:
name: eqx-test-machine-facility
namespace: default
spec:
class:
kind: MachineClass
name: eqx-mc-ny-facilities
21 changes: 12 additions & 9 deletions pkg/provider/apis/provider_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ const (

// EquinixMetalProviderSpec is the spec to be used while parsing the calls.
type EquinixMetalProviderSpec struct {
APIVersion string `json:"apiVersion,omitempty"`
Facility []string `json:"facility"`
MachineType string `json:"machineType"`
BillingCycle string `json:"billingCycle"`
OS string `json:"OS"`
ProjectID string `json:"projectID"`
Tags []string `json:"tags,omitempty"`
SSHKeys []string `json:"sshKeys,omitempty"`
UserData string `json:"userdata,omitempty"`
APIVersion string `json:"apiVersion,omitempty"`
Metro string `json:"metro,omitempty"`
Facilities []string `json:"facilities"`
rfranzke marked this conversation as resolved.
Show resolved Hide resolved
MachineType string `json:"machineType"`
BillingCycle string `json:"billingCycle"`
OS string `json:"OS"`
ProjectID string `json:"projectID"`
Tags []string `json:"tags,omitempty"`
SSHKeys []string `json:"sshKeys,omitempty"`
UserData string `json:"userdata,omitempty"`
ReservationIDs []string `json:"reservationIDs,omitempty"`
ReservedOnly bool `json:"reservedDevicesOnly,omitempty"`
}
4 changes: 2 additions & 2 deletions pkg/provider/apis/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ func ValidateProviderSpec(spec *api.EquinixMetalProviderSpec, fldPath *field.Pat
if "" == spec.ProjectID {
allErrs = append(allErrs, field.Required(fldPath.Child("projectID"), "Project ID is required"))
}
if len(spec.Facility) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("facility"), "At least one Facility specification is required"))
if "" == spec.Metro {
allErrs = append(allErrs, field.Required(fldPath.Child("metro"), "Metro is required"))
}

allErrs = append(allErrs, validateTags(spec.Tags, field.NewPath("spec.tags"))...)
Expand Down
48 changes: 36 additions & 12 deletions pkg/provider/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -122,17 +123,19 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
Plan: providerSpec.MachineType,
ProjectID: providerSpec.ProjectID,
BillingCycle: providerSpec.BillingCycle,
Facility: providerSpec.Facility,
Metro: providerSpec.Metro,
Facility: providerSpec.Facilities,
rfranzke marked this conversation as resolved.
Show resolved Hide resolved
OS: providerSpec.OS,
ProjectSSHKeys: providerSpec.SSHKeys,
Tags: providerSpec.Tags,
}

device, _, err := svc.Create(createRequest)
klog.V(3).Infof("will create machine with request %#v, reservation IDs %v, reservedOnly %v", createRequest, providerSpec.ReservationIDs, providerSpec.ReservedOnly)
device, err := createDeviceWithReservations(svc, createRequest, providerSpec.ReservationIDs, providerSpec.ReservedOnly)
if err != nil {
klog.Errorf("Could not create machine: %v", err)
return nil, status.Error(codes.Unavailable, fmt.Sprintf("Could not create machine: %v", err))
}

response := &driver.CreateMachineResponse{
ProviderID: encodeMachineID(device),
NodeName: machine.Name,
Expand Down Expand Up @@ -351,18 +354,13 @@ func (p *Provider) GetVolumeIDs(ctx context.Context, req *driver.GetVolumeIDsReq
func (p *Provider) GenerateMachineClassForMigration(ctx context.Context, req *driver.GenerateMachineClassForMigrationRequest) (*driver.GenerateMachineClassForMigrationResponse, error) {
// Log messages to track start and end of request
klog.V(2).Infof("MigrateMachineClass request has been received for %q", req.ClassSpec)
defer klog.V(2).Infof("MigrateMachineClass request has been processed successfully for %q", req.ClassSpec)

// this is the old PacketMachineClass; in the move to out-of-tree, we migrated to the newer Equinix Metal
packetMachineClass := req.ProviderSpecificMachineClass.(*v1alpha1.PacketMachineClass)

// Check if incoming CR is valid CR for migration
// In this case, the MachineClassKind to be matching
if req.ClassSpec.Kind != PacketMachineClassKind {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("Requested for Provider '%s', we only support '%s'", req.MachineClass.Provider, ProviderEquinixMetal))
}
// but the old one had just Facility, the newer requires Metro; we will not attempt to divine the metro from
// the facility. This cut simply is not backwards-compatible.

return &driver.GenerateMachineClassForMigrationResponse{}, fillUpMachineClass(packetMachineClass, req.MachineClass)
klog.V(2).Info("MigrateMachineClass does not support backwards compatibility")
return nil, status.Error(codes.InvalidArgument, "Migration not supported")
}

// create a session
Expand Down Expand Up @@ -398,6 +396,32 @@ func decodeProviderSpec(machineClass *v1alpha1.MachineClass) (*api.EquinixMetalP
return providerSpec, nil
}

func createDeviceWithReservations(svc packngo.DeviceService, createRequest *packngo.DeviceCreateRequest, reservationIDs []string, reservedOnly bool) (device *packngo.Device, err error) {
// if there were no reservation IDs and I didn't ask for reservedOnly, then just create one on-demand and return
if len(reservationIDs) == 0 && !reservedOnly {
device, _, err = svc.Create(createRequest)
return device, err
}

// if we got here, we either had some reservation IDs, or we were asked to do reserved only.
// In both cases, we try reservations first.
for _, resID := range reservationIDs {
rfranzke marked this conversation as resolved.
Show resolved Hide resolved
createRequest.HardwareReservationID = resID
device, _, err = svc.Create(createRequest)
// if no error, we got the device, return it
if err == nil {
return device, err
}
rfranzke marked this conversation as resolved.
Show resolved Hide resolved
}
// if we got here, we failed to get a device with the given hardware reservation
if reservedOnly {
return nil, errors.New("could not get a device with the provided reservation IDs, and reservedOnly is true")
}
// now just create a device on demand
device, _, err = svc.Create(createRequest)
return device, err
}

func validateSecretAPIKey(secret *corev1.Secret) error {
return validateSecret(secret, validation.SecretFieldAPIKey)
}
Expand Down
122 changes: 2 additions & 120 deletions pkg/provider/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ import (
"github.com/gardener/machine-controller-manager-provider-equinix-metal/pkg/mock"
"github.com/gardener/machine-controller-manager-provider-equinix-metal/pkg/provider"
api "github.com/gardener/machine-controller-manager-provider-equinix-metal/pkg/provider/apis"
"github.com/gardener/machine-controller-manager/pkg/apis/machine/v1alpha1"
"github.com/gardener/machine-controller-manager/pkg/util/provider/driver"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)

const (
Expand All @@ -29,7 +26,8 @@ const (
var _ = Describe("MachineServer", func() {
// Some initializations
providerSpecStruct := api.EquinixMetalProviderSpec{
Facility: []string{"ewr1", "ny5"},
Facilities: []string{"ewr1", "ny5"},
Metro: "ny",
MachineType: "c3.small.x86",
BillingCycle: "hourly",
OS: "alpine_3.13",
Expand Down Expand Up @@ -494,120 +492,4 @@ var _ = Describe("MachineServer", func() {
}),
)
})
Describe("#GenerateMachineClassForMigration", func() {
type setup struct {
}
type action struct {
generateMachineClassForMigrationRequest *driver.GenerateMachineClassForMigrationRequest
}
type expect struct {
machineClass *v1alpha1.MachineClass
}
type data struct {
setup setup
action action
expect expect
}
/*
SSHKeys []string `json:"sshKeys,omitempty"`
UserData string `json:"userdata,omitempty"`
*/
validRaw := `{"apiVersion":"mcm.gardener.cloud/v1alpha1","facility":["ewr1"],"machineType":"c3.medium.x86","billingCycle":"hourly","OS":"ubuntu_2004","projectID":"abcdefg","tags":["key1: value1","key2: value2"],"userdata":"dummy-user-data"}`
DescribeTable("##table",
func(data *data) {
plugin := &mock.PluginSPIImpl{}
p := provider.NewProvider(plugin)
ctx := context.Background()

_, _ = p.GenerateMachineClassForMigration(
ctx,
data.action.generateMachineClassForMigrationRequest,
)

fmt.Println(string(data.action.generateMachineClassForMigrationRequest.MachineClass.ProviderSpec.Raw))
fmt.Println(string(data.expect.machineClass.ProviderSpec.Raw))
Expect(data.action.generateMachineClassForMigrationRequest.MachineClass).To(Equal(data.expect.machineClass))
},
Entry("Simple migration request with all fields set", &data{
action: action{
generateMachineClassForMigrationRequest: &driver.GenerateMachineClassForMigrationRequest{
ProviderSpecificMachineClass: &v1alpha1.PacketMachineClass{
ObjectMeta: v1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{
"key1": "value1",
"key2": "value2",
},
Annotations: map[string]string{
"key1": "value1",
"key2": "value2",
},
Finalizers: []string{
"mcm/finalizer",
},
},
TypeMeta: v1.TypeMeta{},
Spec: v1alpha1.PacketMachineClassSpec{
Facility: []string{"ewr1"},
MachineType: "c3.medium.x86",
BillingCycle: "hourly",
OS: "ubuntu_2004",
ProjectID: "abcdefg",
UserData: "dummy-user-data",
Tags: []string{
"key1: value1",
"key2: value2",
},
SecretRef: &corev1.SecretReference{
Name: "test-secret",
Namespace: "test-namespace",
},
CredentialsSecretRef: &corev1.SecretReference{
Name: "test-credentials",
Namespace: "test-namespace",
},
},
},
MachineClass: &v1alpha1.MachineClass{},
ClassSpec: &v1alpha1.ClassSpec{
Kind: provider.PacketMachineClassKind,
Name: "test-mc",
},
},
},
expect: expect{
machineClass: &v1alpha1.MachineClass{
TypeMeta: v1.TypeMeta{},
ObjectMeta: v1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{
"key1": "value1",
"key2": "value2",
},
Annotations: map[string]string{
"key1": "value1",
"key2": "value2",
},
Finalizers: []string{
"mcm/finalizer",
},
},
ProviderSpec: runtime.RawExtension{
Raw: []byte(validRaw),
},
SecretRef: &corev1.SecretReference{
Name: "test-secret",
Namespace: "test-namespace",
},
CredentialsSecretRef: &corev1.SecretReference{
Name: "test-credentials",
Namespace: "test-namespace",
},
Provider: provider.ProviderEquinixMetal,
},
},
}),
)
})

})
Loading