[node operator] Add Azure client

Signed-off-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
Malte Poll 2022-07-18 10:18:29 +02:00 committed by Malte Poll
parent a50cc2b64d
commit c74360bf62
17 changed files with 1341 additions and 2 deletions

View file

@ -0,0 +1,37 @@
package client
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"github.com/edgelesssys/constellation/operators/constellation-node-operator/internal/poller"
)
type virtualMachineScaleSetVMsAPI interface {
Get(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string,
options *armcomputev2.VirtualMachineScaleSetVMsClientGetOptions,
) (armcomputev2.VirtualMachineScaleSetVMsClientGetResponse, error)
GetInstanceView(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string,
options *armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewOptions,
) (armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewResponse, error)
NewListPager(resourceGroupName string, virtualMachineScaleSetName string,
options *armcomputev2.VirtualMachineScaleSetVMsClientListOptions,
) *runtime.Pager[armcomputev2.VirtualMachineScaleSetVMsClientListResponse]
}
type scaleSetsAPI interface {
Get(ctx context.Context, resourceGroupName string, vmScaleSetName string,
options *armcomputev2.VirtualMachineScaleSetsClientGetOptions,
) (armcomputev2.VirtualMachineScaleSetsClientGetResponse, error)
BeginUpdate(ctx context.Context, resourceGroupName string, vmScaleSetName string, parameters armcomputev2.VirtualMachineScaleSetUpdate,
options *armcomputev2.VirtualMachineScaleSetsClientBeginUpdateOptions,
) (*runtime.Poller[armcomputev2.VirtualMachineScaleSetsClientUpdateResponse], error)
BeginDeleteInstances(ctx context.Context, resourceGroupName string, vmScaleSetName string, vmInstanceIDs armcomputev2.VirtualMachineScaleSetVMInstanceRequiredIDs,
options *armcomputev2.VirtualMachineScaleSetsClientBeginDeleteInstancesOptions,
) (*runtime.Poller[armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse], error)
}
type capacityPoller interface {
PollUntilDone(context.Context, *poller.PollUntilDoneOptions) (int64, error)
}

View file

@ -0,0 +1,45 @@
package client
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"github.com/edgelesssys/constellation/operators/constellation-node-operator/internal/poller"
)
// Client is a client for the Azure Cloud.
type Client struct {
scaleSetsAPI
virtualMachineScaleSetVMsAPI
capacityPollerGenerator func(resourceGroup, scaleSet string, wantedCapacity int64) capacityPoller
pollerOptions *poller.PollUntilDoneOptions
}
// NewFromDefault creates a client with initialized clients.
func NewFromDefault(subscriptionID, tenantID string) (*Client, error) {
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, err
}
scaleSetAPI, err := armcomputev2.NewVirtualMachineScaleSetsClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
virtualMachineScaleSetVMsAPI, err := armcomputev2.NewVirtualMachineScaleSetVMsClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
return &Client{
scaleSetsAPI: scaleSetAPI,
virtualMachineScaleSetVMsAPI: virtualMachineScaleSetVMsAPI,
capacityPollerGenerator: func(resourceGroup, scaleSet string, wantedCapacity int64) capacityPoller {
return poller.New[int64](&capacityPollingHandler{
resourceGroup: resourceGroup,
scaleSet: scaleSet,
wantedCapacity: wantedCapacity,
scaleSetsAPI: scaleSetAPI,
})
},
}, nil
}

View file

@ -0,0 +1,129 @@
package client
import (
"context"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
)
type stubScaleSetsAPI struct {
scaleSet armcomputev2.VirtualMachineScaleSetsClientGetResponse
getErr error
updateResponse armcomputev2.VirtualMachineScaleSetsClientUpdateResponse
updateErr error
deleteResponse armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse
deleteErr error
resultErr error
}
func (a *stubScaleSetsAPI) Get(ctx context.Context, resourceGroupName string, vmScaleSetName string,
options *armcomputev2.VirtualMachineScaleSetsClientGetOptions,
) (armcomputev2.VirtualMachineScaleSetsClientGetResponse, error) {
return a.scaleSet, a.getErr
}
func (a *stubScaleSetsAPI) BeginUpdate(ctx context.Context, resourceGroupName string, vmScaleSetName string, parameters armcomputev2.VirtualMachineScaleSetUpdate,
options *armcomputev2.VirtualMachineScaleSetsClientBeginUpdateOptions,
) (*runtime.Poller[armcomputev2.VirtualMachineScaleSetsClientUpdateResponse], error) {
poller, err := runtime.NewPoller(nil, runtime.NewPipeline("", "", runtime.PipelineOptions{}, nil), &runtime.NewPollerOptions[armcomputev2.VirtualMachineScaleSetsClientUpdateResponse]{
Handler: &stubPoller[armcomputev2.VirtualMachineScaleSetsClientUpdateResponse]{
result: a.updateResponse,
resultErr: a.resultErr,
},
})
if err != nil {
panic(err)
}
return poller, a.updateErr
}
func (a *stubScaleSetsAPI) BeginDeleteInstances(ctx context.Context, resourceGroupName string, vmScaleSetName string, vmInstanceIDs armcomputev2.VirtualMachineScaleSetVMInstanceRequiredIDs,
options *armcomputev2.VirtualMachineScaleSetsClientBeginDeleteInstancesOptions,
) (*runtime.Poller[armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse], error) {
poller, err := runtime.NewPoller(nil, runtime.NewPipeline("", "", runtime.PipelineOptions{}, nil), &runtime.NewPollerOptions[armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse]{
Handler: &stubPoller[armcomputev2.VirtualMachineScaleSetsClientDeleteInstancesResponse]{
result: a.deleteResponse,
resultErr: a.resultErr,
},
})
if err != nil {
panic(err)
}
return poller, a.deleteErr
}
type stubvirtualMachineScaleSetVMsAPI struct {
scaleSetVM armcomputev2.VirtualMachineScaleSetVMsClientGetResponse
getErr error
instanceView armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewResponse
instanceViewErr error
pager *stubPager
}
func (a *stubvirtualMachineScaleSetVMsAPI) Get(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string,
options *armcomputev2.VirtualMachineScaleSetVMsClientGetOptions,
) (armcomputev2.VirtualMachineScaleSetVMsClientGetResponse, error) {
return a.scaleSetVM, a.getErr
}
func (a *stubvirtualMachineScaleSetVMsAPI) GetInstanceView(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string,
options *armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewOptions,
) (armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewResponse, error) {
return a.instanceView, a.instanceViewErr
}
func (a *stubvirtualMachineScaleSetVMsAPI) NewListPager(resourceGroupName string, virtualMachineScaleSetName string,
options *armcomputev2.VirtualMachineScaleSetVMsClientListOptions,
) *runtime.Pager[armcomputev2.VirtualMachineScaleSetVMsClientListResponse] {
return runtime.NewPager(runtime.PagingHandler[armcomputev2.VirtualMachineScaleSetVMsClientListResponse]{
More: a.pager.moreFunc(),
Fetcher: a.pager.fetcherFunc(),
})
}
type stubPoller[T any] struct {
result T
pollErr error
resultErr error
}
func (p *stubPoller[T]) Done() bool {
return true
}
func (p *stubPoller[T]) Poll(context.Context) (*http.Response, error) {
return nil, p.pollErr
}
func (p *stubPoller[T]) Result(ctx context.Context, out *T) error {
*out = p.result
return p.resultErr
}
type stubPager struct {
list []armcomputev2.VirtualMachineScaleSetVM
fetchErr error
more bool
}
func (p *stubPager) moreFunc() func(armcomputev2.VirtualMachineScaleSetVMsClientListResponse) bool {
return func(armcomputev2.VirtualMachineScaleSetVMsClientListResponse) bool {
return p.more
}
}
func (p *stubPager) fetcherFunc() func(context.Context, *armcomputev2.VirtualMachineScaleSetVMsClientListResponse) (armcomputev2.VirtualMachineScaleSetVMsClientListResponse, error) {
return func(context.Context, *armcomputev2.VirtualMachineScaleSetVMsClientListResponse) (armcomputev2.VirtualMachineScaleSetVMsClientListResponse, error) {
page := make([]*armcomputev2.VirtualMachineScaleSetVM, len(p.list))
for i := range p.list {
page[i] = &p.list[i]
}
return armcomputev2.VirtualMachineScaleSetVMsClientListResponse{
VirtualMachineScaleSetVMListResult: armcomputev2.VirtualMachineScaleSetVMListResult{
Value: page,
},
}, p.fetchErr
}
}

View file

@ -0,0 +1,57 @@
package client
import (
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
updatev1alpha1 "github.com/edgelesssys/constellation/operators/constellation-node-operator/api/v1alpha1"
)
const (
provisioningStateCreating = "ProvisioningState/creating"
provisioningStateUpdating = "ProvisioningState/updating"
provisioningStateMigrating = "ProvisioningState/migrating"
provisioningStateFailed = "ProvisioningState/failed"
provisioningStateDeleting = "ProvisioningState/deleting"
powerStateStarting = "PowerState/starting"
powerStateStopping = "PowerState/stopping"
powerStateStopped = "PowerState/stopped"
powerStateDeallocating = "PowerState/deallocating"
powerStateDeallocated = "PowerState/deallocated"
powerStateRunning = "PowerState/running"
)
// nodeStateFromStatuses returns the node state from instance view statuses.
// reference:
// - https://docs.microsoft.com/en-us/azure/virtual-machines/states-billing#provisioning-states
// - https://docs.microsoft.com/en-us/azure/virtual-machines/states-billing#power-states-and-billing
func nodeStateFromStatuses(statuses []*armcomputev2.InstanceViewStatus) updatev1alpha1.CSPNodeState {
for _, status := range statuses {
if status == nil || status.Code == nil {
continue
}
switch *status.Code {
case provisioningStateCreating:
return updatev1alpha1.NodeStateCreating
case provisioningStateUpdating:
return updatev1alpha1.NodeStateStopped
case provisioningStateMigrating:
return updatev1alpha1.NodeStateStopped
case provisioningStateFailed:
return updatev1alpha1.NodeStateFailed
case provisioningStateDeleting:
return updatev1alpha1.NodeStateTerminating
case powerStateStarting:
return updatev1alpha1.NodeStateCreating
case powerStateStopping:
return updatev1alpha1.NodeStateStopped
case powerStateStopped:
return updatev1alpha1.NodeStateStopped
case powerStateDeallocating:
return updatev1alpha1.NodeStateStopped
case powerStateDeallocated:
return updatev1alpha1.NodeStateStopped
case powerStateRunning:
return updatev1alpha1.NodeStateReady
}
}
return updatev1alpha1.NodeStateUnknown
}

View file

@ -0,0 +1,104 @@
package client
import (
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"github.com/stretchr/testify/assert"
updatev1alpha1 "github.com/edgelesssys/constellation/operators/constellation-node-operator/api/v1alpha1"
)
// this state is included in most VMs but not needed
// to determine the node state as every provisioned VM also has a power state
const provisioningStateSucceeded = "ProvisioningState/succeeded"
func TestNodeStateFromStatuses(t *testing.T) {
testCases := map[string]struct {
statuses []*armcomputev2.InstanceViewStatus
wantState updatev1alpha1.CSPNodeState
}{
"no statuses": {
wantState: updatev1alpha1.NodeStateUnknown,
},
"invalid status": {
statuses: []*armcomputev2.InstanceViewStatus{nil, {Code: nil}},
wantState: updatev1alpha1.NodeStateUnknown,
},
"provisioning creating": {
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateCreating)}},
wantState: updatev1alpha1.NodeStateCreating,
},
"provisioning updating": {
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateUpdating)}},
wantState: updatev1alpha1.NodeStateStopped,
},
"provisioning migrating": {
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateMigrating)}},
wantState: updatev1alpha1.NodeStateStopped,
},
"provisioning failed": {
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateFailed)}},
wantState: updatev1alpha1.NodeStateFailed,
},
"provisioning deleting": {
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateDeleting)}},
wantState: updatev1alpha1.NodeStateTerminating,
},
"provisioning succeeded (but no power state)": {
statuses: []*armcomputev2.InstanceViewStatus{{Code: to.Ptr(provisioningStateSucceeded)}},
wantState: updatev1alpha1.NodeStateUnknown,
},
"power starting": {
statuses: []*armcomputev2.InstanceViewStatus{
{Code: to.Ptr(provisioningStateSucceeded)},
{Code: to.Ptr(powerStateStarting)},
},
wantState: updatev1alpha1.NodeStateCreating,
},
"power stopping": {
statuses: []*armcomputev2.InstanceViewStatus{
{Code: to.Ptr(provisioningStateSucceeded)},
{Code: to.Ptr(powerStateStopping)},
},
wantState: updatev1alpha1.NodeStateStopped,
},
"power stopped": {
statuses: []*armcomputev2.InstanceViewStatus{
{Code: to.Ptr(provisioningStateSucceeded)},
{Code: to.Ptr(powerStateStopped)},
},
wantState: updatev1alpha1.NodeStateStopped,
},
"power deallocating": {
statuses: []*armcomputev2.InstanceViewStatus{
{Code: to.Ptr(provisioningStateSucceeded)},
{Code: to.Ptr(powerStateDeallocating)},
},
wantState: updatev1alpha1.NodeStateStopped,
},
"power deallocated": {
statuses: []*armcomputev2.InstanceViewStatus{
{Code: to.Ptr(provisioningStateSucceeded)},
{Code: to.Ptr(powerStateDeallocated)},
},
wantState: updatev1alpha1.NodeStateStopped,
},
"power running": {
statuses: []*armcomputev2.InstanceViewStatus{
{Code: to.Ptr(provisioningStateSucceeded)},
{Code: to.Ptr(powerStateRunning)},
},
wantState: updatev1alpha1.NodeStateReady,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
gotState := nodeStateFromStatuses(tc.statuses)
assert.Equal(tc.wantState, gotState)
})
}
}

View file

@ -0,0 +1,156 @@
package client
import (
"context"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
)
// GetNodeImage returns the image name of the node.
func (c *Client) GetNodeImage(ctx context.Context, providerID string) (string, error) {
_, resourceGroup, scaleSet, instanceID, err := scaleSetInformationFromProviderID(providerID)
if err != nil {
return "", err
}
resp, err := c.virtualMachineScaleSetVMsAPI.Get(ctx, resourceGroup, scaleSet, instanceID, nil)
if err != nil {
return "", err
}
if resp.Properties == nil ||
resp.Properties.StorageProfile == nil ||
resp.Properties.StorageProfile.ImageReference == nil ||
resp.Properties.StorageProfile.ImageReference.ID == nil {
return "", fmt.Errorf("node %q does not have valid image reference", providerID)
}
return *resp.Properties.StorageProfile.ImageReference.ID, nil
}
// GetScalingGroupID returns the scaling group ID of the node.
func (c *Client) GetScalingGroupID(ctx context.Context, providerID string) (string, error) {
subscriptionID, resourceGroup, scaleSet, _, err := scaleSetInformationFromProviderID(providerID)
if err != nil {
return "", err
}
return joinVMSSID(subscriptionID, resourceGroup, scaleSet), nil
}
// CreateNode creates a node in the specified scaling group.
func (c *Client) CreateNode(ctx context.Context, scalingGroupID string) (nodeName, providerID string, err error) {
_, resourceGroup, scaleSet, err := splitVMSSID(scalingGroupID)
if err != nil {
return "", "", err
}
// get list of instance IDs before scaling,
var oldVMIDs []string
pager := c.virtualMachineScaleSetVMsAPI.NewListPager(resourceGroup, scaleSet, nil)
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return "", "", err
}
for _, vm := range page.Value {
if vm == nil || vm.ID == nil {
continue
}
oldVMIDs = append(oldVMIDs, *vm.ID)
}
}
// increase the number of instances by one
resp, err := c.scaleSetsAPI.Get(ctx, resourceGroup, scaleSet, nil)
if err != nil {
return "", "", err
}
if resp.SKU == nil || resp.SKU.Capacity == nil {
return "", "", fmt.Errorf("scale set %q does not have valid capacity", scaleSet)
}
wantedCapacity := *resp.SKU.Capacity + 1
_, err = c.scaleSetsAPI.BeginUpdate(ctx, resourceGroup, scaleSet, armcompute.VirtualMachineScaleSetUpdate{
SKU: &armcompute.SKU{
Capacity: &wantedCapacity,
},
}, nil)
if err != nil {
return "", "", err
}
poller := c.capacityPollerGenerator(resourceGroup, scaleSet, wantedCapacity)
if _, err := poller.PollUntilDone(ctx, c.pollerOptions); err != nil {
return "", "", err
}
// get the list of instances again
// and find the new instance id by comparing the old and new lists
pager = c.virtualMachineScaleSetVMsAPI.NewListPager(resourceGroup, scaleSet, nil)
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return "", "", err
}
for _, vm := range page.Value {
// check if the instance already existed in the old list
if !hasKnownVMID(vm, oldVMIDs) {
return strings.ToLower(*vm.Properties.OSProfile.ComputerName), "azure://" + *vm.ID, nil
}
}
}
return "", "", fmt.Errorf("failed to find new node after scaling up")
}
// DeleteNode deletes a node specified by its provider ID.
func (c *Client) DeleteNode(ctx context.Context, providerID string) error {
_, resourceGroup, scaleSet, instanceID, err := scaleSetInformationFromProviderID(providerID)
if err != nil {
return err
}
ids := armcompute.VirtualMachineScaleSetVMInstanceRequiredIDs{InstanceIDs: []*string{&instanceID}}
_, err = c.scaleSetsAPI.BeginDeleteInstances(ctx, resourceGroup, scaleSet, ids, nil)
return err
}
// capacityPollingHandler polls a scale set
// until its capacity reaches the desired value.
type capacityPollingHandler struct {
done bool
wantedCapacity int64
resourceGroup string
scaleSet string
scaleSetsAPI
}
func (h *capacityPollingHandler) Done() bool {
return h.done
}
func (h *capacityPollingHandler) Poll(ctx context.Context) error {
resp, err := h.scaleSetsAPI.Get(ctx, h.resourceGroup, h.scaleSet, nil)
if err != nil {
return err
}
if resp.SKU == nil || resp.SKU.Capacity == nil {
return fmt.Errorf("scale set %q does not have valid capacity", h.scaleSet)
}
h.done = *resp.SKU.Capacity == h.wantedCapacity
return nil
}
func (h *capacityPollingHandler) Result(ctx context.Context, out *int64) error {
if !h.done {
return fmt.Errorf("failed to scale up")
}
*out = h.wantedCapacity
return nil
}
// hasKnownVMID returns true if the vmID is found in the vm ID list.
func hasKnownVMID(vm *armcompute.VirtualMachineScaleSetVM, vmIDs []string) bool {
for _, id := range vmIDs {
if vm != nil && vm.ID != nil && *vm.ID == id {
return true
}
}
return false
}

View file

@ -0,0 +1,353 @@
package client
import (
"context"
"errors"
"sync"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"github.com/edgelesssys/constellation/operators/constellation-node-operator/internal/poller"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetNodeImage(t *testing.T) {
testCases := map[string]struct {
providerID string
vm armcomputev2.VirtualMachineScaleSetVM
getScaleSetVMErr error
wantImage string
wantErr bool
}{
"getting node image works": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
vm: armcomputev2.VirtualMachineScaleSetVM{
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
StorageProfile: &armcomputev2.StorageProfile{
ImageReference: &armcomputev2.ImageReference{
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name"),
},
},
},
},
wantImage: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name",
},
"splitting providerID fails": {
providerID: "invalid",
wantErr: true,
},
"get scale set vm fails": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
getScaleSetVMErr: errors.New("get scale set vm error"),
wantErr: true,
},
"scale set vm does not have valid image reference": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
vm: armcomputev2.VirtualMachineScaleSetVM{},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{
virtualMachineScaleSetVMsAPI: &stubvirtualMachineScaleSetVMsAPI{
scaleSetVM: armcomputev2.VirtualMachineScaleSetVMsClientGetResponse{
VirtualMachineScaleSetVM: tc.vm,
},
getErr: tc.getScaleSetVMErr,
},
}
gotImage, err := client.GetNodeImage(context.Background(), tc.providerID)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantImage, gotImage)
})
}
}
func TestGetScalingGroupID(t *testing.T) {
testCases := map[string]struct {
providerID string
wantScalingGroupID string
wantErr bool
}{
"getting scaling group id works": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
wantScalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
},
"splitting providerID fails": {
providerID: "invalid",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{}
gotScalingGroupID, err := client.GetScalingGroupID(context.Background(), tc.providerID)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantScalingGroupID, gotScalingGroupID)
})
}
}
func TestCreateNode(t *testing.T) {
testCases := map[string]struct {
scalingGroupID string
sku *armcomputev2.SKU
preexistingVMs []armcomputev2.VirtualMachineScaleSetVM
newVM *armcomputev2.VirtualMachineScaleSetVM
fetchErr error
pollErr error
getSKUCapacityErr error
updateScaleSetErr error
wantNodeName string
wantProviderID string
wantEarlyErr bool
wantLateErr bool
}{
"creating node works": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
sku: &armcomputev2.SKU{
Capacity: to.Ptr(int64(0)),
},
newVM: &armcomputev2.VirtualMachineScaleSetVM{
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/node-name"),
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
OSProfile: &armcomputev2.OSProfile{
ComputerName: to.Ptr("node-name"),
},
},
},
wantNodeName: "node-name",
wantProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/node-name",
},
"creating node works with existing nodes": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
sku: &armcomputev2.SKU{
Capacity: to.Ptr(int64(1)),
},
preexistingVMs: []armcomputev2.VirtualMachineScaleSetVM{
{ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/preexisting-node")},
},
newVM: &armcomputev2.VirtualMachineScaleSetVM{
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/new-node"),
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
OSProfile: &armcomputev2.OSProfile{
ComputerName: to.Ptr("new-node"),
},
},
},
wantNodeName: "new-node",
wantProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/new-node",
},
"splitting scalingGroupID fails": {
scalingGroupID: "invalid",
wantEarlyErr: true,
},
"getting node list fails": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
fetchErr: errors.New("get node list error"),
wantEarlyErr: true,
},
"getting SKU capacity fails": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
getSKUCapacityErr: errors.New("get sku capacity error"),
wantEarlyErr: true,
},
"sku is invalid": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
wantEarlyErr: true,
},
"updating scale set capacity fails": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
sku: &armcomputev2.SKU{Capacity: to.Ptr(int64(0))},
updateScaleSetErr: errors.New("update scale set error"),
wantEarlyErr: true,
},
"polling for increased capacity fails": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
sku: &armcomputev2.SKU{Capacity: to.Ptr(int64(0))},
pollErr: errors.New("poll error"),
wantLateErr: true,
},
"new node cannot be found": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
sku: &armcomputev2.SKU{Capacity: to.Ptr(int64(0))},
wantLateErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
pager := &stubPager{
list: tc.preexistingVMs,
fetchErr: tc.fetchErr,
}
poller := NewStubCapacityPoller(tc.pollErr)
client := Client{
virtualMachineScaleSetVMsAPI: &stubvirtualMachineScaleSetVMsAPI{
pager: pager,
},
scaleSetsAPI: &stubScaleSetsAPI{
scaleSet: armcomputev2.VirtualMachineScaleSetsClientGetResponse{
VirtualMachineScaleSet: armcomputev2.VirtualMachineScaleSet{
SKU: tc.sku,
},
},
getErr: tc.getSKUCapacityErr,
updateErr: tc.updateScaleSetErr,
},
capacityPollerGenerator: func(resourceGroup, scaleSet string, wantedCapacity int64) capacityPoller {
return poller
},
}
wg := sync.WaitGroup{}
wg.Add(1)
var gotNodeName, gotProviderID string
var createErr error
go func() {
defer wg.Done()
gotNodeName, gotProviderID, createErr = client.CreateNode(context.Background(), tc.scalingGroupID)
}()
// want error before PollUntilDone is called
if tc.wantEarlyErr {
wg.Wait()
assert.Error(createErr)
return
}
// wait for PollUntilDone to be called
<-poller.pollC
// update list of nodes
if tc.newVM != nil {
pager.list = append(pager.list, *tc.newVM)
}
// let PollUntilDone finish
poller.doneC <- struct{}{}
wg.Wait()
if tc.wantLateErr {
assert.Error(createErr)
return
}
require.NoError(createErr)
assert.Equal(tc.wantNodeName, gotNodeName)
assert.Equal(tc.wantProviderID, gotProviderID)
})
}
}
func TestDeleteNode(t *testing.T) {
testCases := map[string]struct {
providerID string
deleteErr error
wantErr bool
}{
"deleting node works": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
},
"invalid providerID": {
providerID: "invalid",
wantErr: true,
},
"deleting node fails": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
deleteErr: errors.New("delete error"),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
client := Client{
scaleSetsAPI: &stubScaleSetsAPI{deleteErr: tc.deleteErr},
}
err := client.DeleteNode(context.Background(), tc.providerID)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
})
}
}
// TODO: test capacityPollingHandler
func TestCapacityPollingHandler(t *testing.T) {
assert := assert.New(t)
wantCapacity := int64(1)
var gotCapacity int64
handler := capacityPollingHandler{
scaleSetsAPI: &stubScaleSetsAPI{
scaleSet: armcomputev2.VirtualMachineScaleSetsClientGetResponse{
VirtualMachineScaleSet: armcomputev2.VirtualMachineScaleSet{
SKU: &armcomputev2.SKU{Capacity: to.Ptr(int64(0))},
},
},
},
wantedCapacity: wantCapacity,
}
assert.NoError(handler.Poll(context.Background()))
assert.False(handler.Done())
// Calling Result early should error
assert.Error(handler.Result(context.Background(), &gotCapacity))
// let scaleSet API error
handler.scaleSetsAPI.(*stubScaleSetsAPI).getErr = errors.New("get error")
assert.Error(handler.Poll(context.Background()))
handler.scaleSetsAPI.(*stubScaleSetsAPI).getErr = nil
// let scaleSet API return invalid SKU
handler.scaleSetsAPI.(*stubScaleSetsAPI).scaleSet.SKU = nil
assert.Error(handler.Poll(context.Background()))
// let Poll finish
handler.scaleSetsAPI.(*stubScaleSetsAPI).scaleSet.SKU = &armcomputev2.SKU{Capacity: to.Ptr(int64(wantCapacity))}
assert.NoError(handler.Poll(context.Background()))
assert.True(handler.Done())
assert.NoError(handler.Result(context.Background(), &gotCapacity))
assert.Equal(wantCapacity, gotCapacity)
}
type stubCapacityPoller struct {
pollErr error
pollC chan struct{}
doneC chan struct{}
}
func NewStubCapacityPoller(pollErr error) *stubCapacityPoller {
return &stubCapacityPoller{
pollErr: pollErr,
pollC: make(chan struct{}),
doneC: make(chan struct{}),
}
}
func (p *stubCapacityPoller) PollUntilDone(context.Context, *poller.PollUntilDoneOptions) (int64, error) {
p.pollC <- struct{}{}
<-p.doneC
return 0, p.pollErr
}

View file

@ -0,0 +1,27 @@
package client
import (
"context"
"errors"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
updatev1alpha1 "github.com/edgelesssys/constellation/operators/constellation-node-operator/api/v1alpha1"
)
// GetNodeState returns the state of the node.
func (c *Client) GetNodeState(ctx context.Context, providerID string) (updatev1alpha1.CSPNodeState, error) {
_, resourceGroup, scaleSet, instanceID, err := scaleSetInformationFromProviderID(providerID)
if err != nil {
return updatev1alpha1.NodeStateUnknown, err
}
instanceView, err := c.virtualMachineScaleSetVMsAPI.GetInstanceView(ctx, resourceGroup, scaleSet, instanceID, nil)
if err != nil {
var respErr *azcore.ResponseError
if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound {
return updatev1alpha1.NodeStateTerminated, nil
}
return updatev1alpha1.NodeStateUnknown, err
}
return nodeStateFromStatuses(instanceView.Statuses), nil
}

View file

@ -0,0 +1,73 @@
package client
import (
"context"
"errors"
"net/http"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
updatev1alpha1 "github.com/edgelesssys/constellation/operators/constellation-node-operator/api/v1alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetNodeState(t *testing.T) {
testCases := map[string]struct {
providerID string
instanceView armcomputev2.VirtualMachineScaleSetVMInstanceView
getInstanceViewErr error
wantState updatev1alpha1.CSPNodeState
wantErr bool
}{
"getting node state works": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
instanceView: armcomputev2.VirtualMachineScaleSetVMInstanceView{
Statuses: []*armcomputev2.InstanceViewStatus{
{Code: to.Ptr("ProvisioningState/succeeded")},
{Code: to.Ptr("PowerState/running")},
},
},
wantState: updatev1alpha1.NodeStateReady,
},
"splitting providerID fails": {
providerID: "invalid",
wantErr: true,
},
"get instance view fails": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
getInstanceViewErr: errors.New("get instance view error"),
wantErr: true,
},
"get instance view returns 404": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
getInstanceViewErr: &azcore.ResponseError{StatusCode: http.StatusNotFound},
wantState: updatev1alpha1.NodeStateTerminated,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{
virtualMachineScaleSetVMsAPI: &stubvirtualMachineScaleSetVMsAPI{
instanceView: armcomputev2.VirtualMachineScaleSetVMsClientGetInstanceViewResponse{
VirtualMachineScaleSetVMInstanceView: tc.instanceView,
},
instanceViewErr: tc.getInstanceViewErr,
},
}
gotState, err := client.GetNodeState(context.Background(), tc.providerID)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantState, gotState)
})
}
}

View file

@ -0,0 +1,20 @@
package client
import (
"errors"
"regexp"
)
// azureVMSSProviderIDRegexp describes the format of a kubernetes providerID for an azure virtual machine scale set (VMSS) VM.
var azureVMSSProviderIDRegexp = regexp.MustCompile(`^azure:///subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.Compute/virtualMachineScaleSets/([^/]+)/virtualMachines/([^/]+)$`)
// scaleSetInformationFromProviderID splits a provider's id belonging to an azure scale set into core components.
// A providerID for scale set VMs is build after the following schema:
// - 'azure:///subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Compute/virtualMachineScaleSets/<scale-set-name>/virtualMachines/<instance-id>'
func scaleSetInformationFromProviderID(providerID string) (subscriptionID, resourceGroup, scaleSet, instanceID string, err error) {
matches := azureVMSSProviderIDRegexp.FindStringSubmatch(providerID)
if len(matches) != 5 {
return "", "", "", "", errors.New("error splitting providerID")
}
return matches[1], matches[2], matches[3], matches[4], nil
}

View file

@ -0,0 +1,52 @@
package client
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestScaleSetInformationFromProviderID(t *testing.T) {
testCases := map[string]struct {
providerID string
wantSubscriptionID string
wantResourceGroup string
wantScaleSet string
wantInstanceID string
wantErr bool
}{
"providerID for scale set instance works": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
wantSubscriptionID: "subscription-id",
wantResourceGroup: "resource-group",
wantScaleSet: "scale-set-name",
wantInstanceID: "instance-id",
},
"providerID for individual instance must fail": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name",
wantErr: true,
},
"providerID is malformed": {
providerID: "malformed-provider-id",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
subscriptionID, resourceGroup, scaleSet, instanceName, err := scaleSetInformationFromProviderID(tc.providerID)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.Equal(tc.wantSubscriptionID, subscriptionID)
assert.Equal(tc.wantResourceGroup, resourceGroup)
assert.Equal(tc.wantScaleSet, scaleSet)
assert.Equal(tc.wantInstanceID, instanceName)
})
}
}

View file

@ -0,0 +1,54 @@
package client
import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
)
// GetScalingGroupImage returns the image URI of the scaling group.
func (c *Client) GetScalingGroupImage(ctx context.Context, scalingGroupID string) (string, error) {
_, resourceGroup, scaleSet, err := splitVMSSID(scalingGroupID)
if err != nil {
return "", err
}
res, err := c.scaleSetsAPI.Get(ctx, resourceGroup, scaleSet, nil)
if err != nil {
return "", err
}
if res.Properties == nil ||
res.Properties.VirtualMachineProfile == nil ||
res.Properties.VirtualMachineProfile.StorageProfile == nil ||
res.Properties.VirtualMachineProfile.StorageProfile.ImageReference == nil ||
res.Properties.VirtualMachineProfile.StorageProfile.ImageReference.ID == nil {
return "", fmt.Errorf("scalet set %q does not have valid image reference", scalingGroupID)
}
return *res.Properties.VirtualMachineProfile.StorageProfile.ImageReference.ID, nil
}
// SetScalingGroupImage sets the image URI of the scaling group.
func (c *Client) SetScalingGroupImage(ctx context.Context, scalingGroupID, imageURI string) error {
_, resourceGroup, scaleSet, err := splitVMSSID(scalingGroupID)
if err != nil {
return err
}
poller, err := c.scaleSetsAPI.BeginUpdate(ctx, resourceGroup, scaleSet, armcompute.VirtualMachineScaleSetUpdate{
Properties: &armcompute.VirtualMachineScaleSetUpdateProperties{
VirtualMachineProfile: &armcompute.VirtualMachineScaleSetUpdateVMProfile{
StorageProfile: &armcompute.VirtualMachineScaleSetUpdateStorageProfile{
ImageReference: &armcompute.ImageReference{
ID: &imageURI,
},
},
},
},
}, nil)
if err != nil {
return err
}
if _, err := poller.PollUntilDone(ctx, nil); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,125 @@
package client
import (
"context"
"errors"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetScalingGroupImage(t *testing.T) {
testCases := map[string]struct {
scalingGroupID string
scaleSet armcomputev2.VirtualMachineScaleSet
getScaleSetErr error
wantImage string
wantErr bool
}{
"getting image works": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
scaleSet: armcomputev2.VirtualMachineScaleSet{
Properties: &armcomputev2.VirtualMachineScaleSetProperties{
VirtualMachineProfile: &armcomputev2.VirtualMachineScaleSetVMProfile{
StorageProfile: &armcomputev2.VirtualMachineScaleSetStorageProfile{
ImageReference: &armcomputev2.ImageReference{
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name"),
},
},
},
},
},
wantImage: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name",
},
"splitting scalingGroupID fails": {
scalingGroupID: "invalid",
wantErr: true,
},
"get scale set fails": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
getScaleSetErr: errors.New("get scale set error"),
wantErr: true,
},
"scale set is invalid": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{
scaleSetsAPI: &stubScaleSetsAPI{
scaleSet: armcomputev2.VirtualMachineScaleSetsClientGetResponse{
VirtualMachineScaleSet: tc.scaleSet,
},
getErr: tc.getScaleSetErr,
},
}
gotImage, err := client.GetScalingGroupImage(context.Background(), tc.scalingGroupID)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantImage, gotImage)
})
}
}
func TestSetScalingGroupImage(t *testing.T) {
testCases := map[string]struct {
scalingGroupID string
imageURI string
updateErr error
resultErr error
wantErr bool
}{
"setting image works": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
imageURI: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name-2",
},
"splitting scalingGroupID fails": {
scalingGroupID: "invalid",
wantErr: true,
},
"beginning update fails": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
imageURI: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name-2",
updateErr: errors.New("update error"),
wantErr: true,
},
"retrieving polling result fails": {
scalingGroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
imageURI: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/images/image-name-2",
resultErr: errors.New("result error"),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{
scaleSetsAPI: &stubScaleSetsAPI{
updateErr: tc.updateErr,
resultErr: tc.resultErr,
},
}
err := client.SetScalingGroupImage(context.Background(), tc.scalingGroupID, tc.imageURI)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
})
}
}

View file

@ -0,0 +1,24 @@
package client
import (
"fmt"
"regexp"
)
// vmssIDRegexp is describes the format of an azure virtual machine scale set (VMSS) id.
var vmssIDRegexp = regexp.MustCompile(`^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.Compute/virtualMachineScaleSets/([^/]+)$`)
// joinVMSSID joins scale set parameters to generate a virtual machine scale set (VMSS) ID.
// Format: /subscriptions/<subscription>/resourceGroups/<resource-group>/providers/Microsoft.Compute/virtualMachineScaleSets/<scale-set>
func joinVMSSID(subscriptionID, resourceGroup, scaleSet string) string {
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachineScaleSets/%s", subscriptionID, resourceGroup, scaleSet)
}
// splitVMSSID splits a virtual machine scale set (VMSS) ID into its component.
func splitVMSSID(vmssID string) (subscriptionID, resourceGroup, scaleSet string, err error) {
matches := vmssIDRegexp.FindStringSubmatch(vmssID)
if len(matches) != 4 {
return "", "", "", fmt.Errorf("error splitting vmssID")
}
return matches[1], matches[2], matches[3], nil
}

View file

@ -0,0 +1,53 @@
package client
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestJoinVMSSID(t *testing.T) {
assert.Equal(t,
"/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set",
joinVMSSID("subscription-id", "resource-group", "scale-set"))
}
func TestSplitVMSSID(t *testing.T) {
testCases := map[string]struct {
vmssID string
wantSubscriptionID string
wantResourceGroup string
wantScaleSet string
wantInstanceID string
wantErr bool
}{
"vmssID can be splitted": {
vmssID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name",
wantSubscriptionID: "subscription-id",
wantResourceGroup: "resource-group",
wantScaleSet: "scale-set-name",
wantInstanceID: "instance-id",
},
"vmssID is malformed": {
vmssID: "malformed-vmss-id",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
subscriptionID, resourceGroup, scaleSet, err := splitVMSSID(tc.vmssID)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.Equal(tc.wantSubscriptionID, subscriptionID)
assert.Equal(tc.wantResourceGroup, resourceGroup)
assert.Equal(tc.wantScaleSet, scaleSet)
})
}
}