2022-03-22 11:03:15 -04:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
2022-08-01 03:47:39 -04:00
|
|
|
"fmt"
|
2022-08-24 05:31:43 -04:00
|
|
|
"net/http"
|
2022-03-22 11:03:15 -04:00
|
|
|
"strconv"
|
2022-08-01 03:47:39 -04:00
|
|
|
"sync"
|
2022-08-24 05:31:43 -04:00
|
|
|
"time"
|
2022-03-22 11:03:15 -04:00
|
|
|
|
2022-08-24 05:31:43 -04:00
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
2022-07-27 16:02:33 -04:00
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
|
2022-03-22 11:03:15 -04:00
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
2022-06-07 10:30:41 -04:00
|
|
|
"github.com/edgelesssys/constellation/cli/internal/azure"
|
2022-08-24 05:31:43 -04:00
|
|
|
"github.com/edgelesssys/constellation/cli/internal/azure/internal/poller"
|
2022-06-08 02:17:52 -04:00
|
|
|
"github.com/edgelesssys/constellation/internal/cloud/cloudtypes"
|
2022-03-22 11:03:15 -04:00
|
|
|
)
|
|
|
|
|
2022-08-24 05:31:43 -04:00
|
|
|
// scaleSetCreateTimeout maximum timeout to wait for scale set creation.
|
|
|
|
const scaleSetCreateTimeout = 5 * time.Minute
|
|
|
|
|
2022-03-22 11:03:15 -04:00
|
|
|
func (c *Client) CreateInstances(ctx context.Context, input CreateInstancesInput) error {
|
2022-06-29 09:26:29 -04:00
|
|
|
// Create worker scale set
|
|
|
|
createWorkerInput := CreateScaleSetInput{
|
|
|
|
Name: "constellation-scale-set-workers-" + c.uid,
|
2022-05-24 04:04:42 -04:00
|
|
|
NamePrefix: c.name + "-worker-" + c.uid + "-",
|
2022-06-29 09:26:29 -04:00
|
|
|
Count: input.CountWorkers,
|
2022-05-24 04:04:42 -04:00
|
|
|
InstanceType: input.InstanceType,
|
|
|
|
StateDiskSizeGB: int32(input.StateDiskSizeGB),
|
2022-08-02 06:24:55 -04:00
|
|
|
StateDiskType: input.StateDiskType,
|
2022-05-24 04:04:42 -04:00
|
|
|
Image: input.Image,
|
|
|
|
UserAssingedIdentity: input.UserAssingedIdentity,
|
|
|
|
LoadBalancerBackendAddressPool: azure.BackendAddressPoolWorkerName + "-" + c.uid,
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
2022-06-29 09:26:29 -04:00
|
|
|
// Create control plane scale set
|
|
|
|
createControlPlaneInput := CreateScaleSetInput{
|
2022-07-07 05:43:35 -04:00
|
|
|
Name: "constellation-scale-set-controlplanes-" + c.uid,
|
2022-05-24 04:04:42 -04:00
|
|
|
NamePrefix: c.name + "-control-plane-" + c.uid + "-",
|
2022-06-29 09:26:29 -04:00
|
|
|
Count: input.CountControlPlanes,
|
2022-05-24 04:04:42 -04:00
|
|
|
InstanceType: input.InstanceType,
|
|
|
|
StateDiskSizeGB: int32(input.StateDiskSizeGB),
|
2022-08-02 06:24:55 -04:00
|
|
|
StateDiskType: input.StateDiskType,
|
2022-05-24 04:04:42 -04:00
|
|
|
Image: input.Image,
|
|
|
|
UserAssingedIdentity: input.UserAssingedIdentity,
|
|
|
|
LoadBalancerBackendAddressPool: azure.BackendAddressPoolControlPlaneName + "-" + c.uid,
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
2022-08-01 03:47:39 -04:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
var controlPlaneErr, workerErr error
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
workerErr = c.createScaleSet(ctx, createWorkerInput)
|
|
|
|
}()
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
controlPlaneErr = c.createScaleSet(ctx, createControlPlaneInput)
|
|
|
|
}()
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
if controlPlaneErr != nil {
|
|
|
|
return fmt.Errorf("creating control-plane scaleset: %w", controlPlaneErr)
|
|
|
|
}
|
|
|
|
if workerErr != nil {
|
|
|
|
return fmt.Errorf("creating worker scaleset: %w", workerErr)
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
2022-08-01 03:47:39 -04:00
|
|
|
// TODO: Remove getInstanceIPs calls after init has been refactored to not use node IPs
|
2022-06-29 09:26:29 -04:00
|
|
|
// Get worker IPs
|
2022-08-01 03:47:39 -04:00
|
|
|
c.workerScaleSet = createWorkerInput.Name
|
2022-06-29 09:26:29 -04:00
|
|
|
instances, err := c.getInstanceIPs(ctx, createWorkerInput.Name, createWorkerInput.Count)
|
2022-03-22 11:03:15 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-29 09:26:29 -04:00
|
|
|
c.workers = instances
|
2022-03-22 11:03:15 -04:00
|
|
|
|
2022-06-29 09:26:29 -04:00
|
|
|
// Get control plane IPs
|
|
|
|
c.controlPlaneScaleSet = createControlPlaneInput.Name
|
|
|
|
instances, err = c.getInstanceIPs(ctx, createControlPlaneInput.Name, createControlPlaneInput.Count)
|
2022-03-22 11:03:15 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-29 09:26:29 -04:00
|
|
|
c.controlPlanes = instances
|
2022-03-22 11:03:15 -04:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateInstancesInput is the input for a CreateInstances operation.
|
|
|
|
type CreateInstancesInput struct {
|
2022-06-29 09:26:29 -04:00
|
|
|
CountWorkers int
|
|
|
|
CountControlPlanes int
|
2022-03-22 11:03:15 -04:00
|
|
|
InstanceType string
|
2022-04-05 08:25:49 -04:00
|
|
|
StateDiskSizeGB int
|
2022-08-02 06:24:55 -04:00
|
|
|
StateDiskType string
|
2022-03-22 11:03:15 -04:00
|
|
|
Image string
|
|
|
|
UserAssingedIdentity string
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateInstancesVMs creates instances based on standalone VMs.
|
|
|
|
// TODO: deprecate as soon as scale sets are available.
|
|
|
|
func (c *Client) CreateInstancesVMs(ctx context.Context, input CreateInstancesInput) error {
|
|
|
|
pw, err := azure.GeneratePassword()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-06-29 09:26:29 -04:00
|
|
|
for i := 0; i < input.CountControlPlanes; i++ {
|
2022-04-04 10:44:15 -04:00
|
|
|
vm := azure.VMInstance{
|
|
|
|
Name: c.name + "-control-plane-" + c.uid + "-" + strconv.Itoa(i),
|
|
|
|
Username: "constell",
|
|
|
|
Password: pw,
|
|
|
|
Location: c.location,
|
|
|
|
InstanceType: input.InstanceType,
|
|
|
|
Image: input.Image,
|
|
|
|
}
|
|
|
|
instance, err := c.createInstanceVM(ctx, vm)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-29 09:26:29 -04:00
|
|
|
c.controlPlanes[strconv.Itoa(i)] = instance
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
2022-06-29 09:26:29 -04:00
|
|
|
for i := 0; i < input.CountWorkers; i++ {
|
2022-03-22 11:03:15 -04:00
|
|
|
vm := azure.VMInstance{
|
2022-06-29 09:26:29 -04:00
|
|
|
Name: c.name + "-worker-" + c.uid + "-" + strconv.Itoa(i),
|
2022-03-22 11:03:15 -04:00
|
|
|
Username: "constell",
|
|
|
|
Password: pw,
|
|
|
|
Location: c.location,
|
|
|
|
InstanceType: input.InstanceType,
|
|
|
|
Image: input.Image,
|
|
|
|
}
|
|
|
|
instance, err := c.createInstanceVM(ctx, vm)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-29 09:26:29 -04:00
|
|
|
c.workers[strconv.Itoa(i)] = instance
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// createInstanceVM creates a single VM with a public IP address
|
|
|
|
// and a network interface.
|
|
|
|
// TODO: deprecate as soon as scale sets are available.
|
2022-06-07 05:45:36 -04:00
|
|
|
func (c *Client) createInstanceVM(ctx context.Context, input azure.VMInstance) (cloudtypes.Instance, error) {
|
2022-03-22 11:03:15 -04:00
|
|
|
pubIPName := input.Name + "-pubIP"
|
2022-05-24 04:04:42 -04:00
|
|
|
pubIP, err := c.createPublicIPAddress(ctx, pubIPName)
|
2022-03-22 11:03:15 -04:00
|
|
|
if err != nil {
|
2022-06-07 05:45:36 -04:00
|
|
|
return cloudtypes.Instance{}, err
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
nicName := input.Name + "-NIC"
|
2022-05-24 04:04:42 -04:00
|
|
|
privIP, nicID, err := c.createNIC(ctx, nicName, *pubIP.ID)
|
2022-03-22 11:03:15 -04:00
|
|
|
if err != nil {
|
2022-06-07 05:45:36 -04:00
|
|
|
return cloudtypes.Instance{}, err
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
input.NIC = nicID
|
|
|
|
|
|
|
|
poller, err := c.virtualMachinesAPI.BeginCreateOrUpdate(ctx, c.resourceGroup, input.Name, input.Azure(), nil)
|
|
|
|
if err != nil {
|
2022-06-07 05:45:36 -04:00
|
|
|
return cloudtypes.Instance{}, err
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
2022-07-27 16:02:33 -04:00
|
|
|
vm, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{
|
2022-08-01 03:47:39 -04:00
|
|
|
Frequency: c.pollFrequency,
|
2022-07-27 16:02:33 -04:00
|
|
|
})
|
2022-03-22 11:03:15 -04:00
|
|
|
if err != nil {
|
2022-06-07 05:45:36 -04:00
|
|
|
return cloudtypes.Instance{}, err
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if vm.Identity == nil || vm.Identity.PrincipalID == nil {
|
2022-06-07 05:45:36 -04:00
|
|
|
return cloudtypes.Instance{}, errors.New("virtual machine was created without system managed identity")
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.assignResourceGroupRole(ctx, *vm.Identity.PrincipalID, virtualMachineContributorRoleDefinitionID); err != nil {
|
2022-06-07 05:45:36 -04:00
|
|
|
return cloudtypes.Instance{}, err
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
res, err := c.publicIPAddressesAPI.Get(ctx, c.resourceGroup, pubIPName, nil)
|
|
|
|
if err != nil {
|
2022-06-07 05:45:36 -04:00
|
|
|
return cloudtypes.Instance{}, err
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
2022-07-27 16:02:33 -04:00
|
|
|
return cloudtypes.Instance{PublicIP: *res.PublicIPAddress.Properties.IPAddress, PrivateIP: privIP}, nil
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) createScaleSet(ctx context.Context, input CreateScaleSetInput) error {
|
|
|
|
// TODO: Generating a random password to be able
|
|
|
|
// to create the scale set. This is a temporary fix.
|
|
|
|
// We need to think about azure access at some point.
|
|
|
|
pw, err := azure.GeneratePassword()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
scaleSet := azure.ScaleSet{
|
2022-05-24 04:04:42 -04:00
|
|
|
Name: input.Name,
|
|
|
|
NamePrefix: input.NamePrefix,
|
|
|
|
Location: c.location,
|
|
|
|
InstanceType: input.InstanceType,
|
|
|
|
StateDiskSizeGB: input.StateDiskSizeGB,
|
2022-08-02 06:24:55 -04:00
|
|
|
StateDiskType: input.StateDiskType,
|
2022-05-24 04:04:42 -04:00
|
|
|
Count: int64(input.Count),
|
|
|
|
Username: "constellation",
|
|
|
|
SubnetID: c.subnetID,
|
|
|
|
NetworkSecurityGroup: c.networkSecurityGroup,
|
|
|
|
Image: input.Image,
|
|
|
|
Password: pw,
|
|
|
|
UserAssignedIdentity: input.UserAssingedIdentity,
|
|
|
|
Subscription: c.subscriptionID,
|
|
|
|
ResourceGroup: c.resourceGroup,
|
|
|
|
LoadBalancerName: c.loadBalancerName,
|
|
|
|
LoadBalancerBackendAddressPool: input.LoadBalancerBackendAddressPool,
|
2022-03-22 11:03:15 -04:00
|
|
|
}.Azure()
|
|
|
|
|
2022-08-24 05:31:43 -04:00
|
|
|
_, err = c.scaleSetsAPI.BeginCreateOrUpdate(
|
2022-03-22 11:03:15 -04:00
|
|
|
ctx, c.resourceGroup, input.Name,
|
|
|
|
scaleSet,
|
|
|
|
nil,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-08-24 05:31:43 -04:00
|
|
|
// use custom poller to wait for resource creation but skip waiting for OS provisioning.
|
|
|
|
// OS provisioning does not work reliably without the azure guest agent installed.
|
|
|
|
poller := poller.New[bool](&scaleSetCreationPollingHandler{
|
|
|
|
resourceGroup: c.resourceGroup,
|
|
|
|
scaleSet: input.Name,
|
|
|
|
scaleSetsAPI: c.scaleSetsAPI,
|
2022-07-27 16:02:33 -04:00
|
|
|
})
|
2022-03-22 11:03:15 -04:00
|
|
|
|
2022-08-24 05:31:43 -04:00
|
|
|
pollCtx, cancel := context.WithTimeout(ctx, scaleSetCreateTimeout)
|
|
|
|
defer cancel()
|
|
|
|
_, err = poller.PollUntilDone(pollCtx, nil)
|
|
|
|
return err
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
2022-06-07 05:45:36 -04:00
|
|
|
func (c *Client) getInstanceIPs(ctx context.Context, scaleSet string, count int) (cloudtypes.Instances, error) {
|
|
|
|
instances := cloudtypes.Instances{}
|
2022-03-22 11:03:15 -04:00
|
|
|
for i := 0; i < count; i++ {
|
|
|
|
// get public ip address
|
|
|
|
var publicIPAddress string
|
2022-07-27 16:02:33 -04:00
|
|
|
pager := c.publicIPAddressesAPI.NewListVirtualMachineScaleSetVMPublicIPAddressesPager(
|
2022-03-22 11:03:15 -04:00
|
|
|
c.resourceGroup, scaleSet, strconv.Itoa(i), scaleSet, scaleSet, nil)
|
|
|
|
|
2022-07-27 16:02:33 -04:00
|
|
|
for pager.More() {
|
|
|
|
page, err := pager.NextPage(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return cloudtypes.Instances{}, err
|
|
|
|
}
|
|
|
|
for _, v := range page.Value {
|
2022-03-22 11:03:15 -04:00
|
|
|
if v.Properties != nil && v.Properties.IPAddress != nil {
|
|
|
|
publicIPAddress = *v.Properties.IPAddress
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// get private ip address
|
|
|
|
var privateIPAddress string
|
|
|
|
res, err := c.networkInterfacesAPI.GetVirtualMachineScaleSetNetworkInterface(
|
|
|
|
ctx, c.resourceGroup, scaleSet, strconv.Itoa(i), scaleSet, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-07-27 16:02:33 -04:00
|
|
|
configs := res.Interface.Properties.IPConfigurations
|
2022-03-22 11:03:15 -04:00
|
|
|
for _, config := range configs {
|
|
|
|
privateIPAddress = *config.Properties.PrivateIPAddress
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2022-06-07 05:45:36 -04:00
|
|
|
instance := cloudtypes.Instance{
|
2022-03-22 11:03:15 -04:00
|
|
|
PrivateIP: privateIPAddress,
|
|
|
|
PublicIP: publicIPAddress,
|
|
|
|
}
|
|
|
|
instances[strconv.Itoa(i)] = instance
|
|
|
|
}
|
|
|
|
return instances, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateScaleSetInput is the input for a CreateScaleSet operation.
|
|
|
|
type CreateScaleSetInput struct {
|
2022-05-24 04:04:42 -04:00
|
|
|
Name string
|
|
|
|
NamePrefix string
|
|
|
|
Count int
|
|
|
|
InstanceType string
|
|
|
|
StateDiskSizeGB int32
|
2022-08-02 06:24:55 -04:00
|
|
|
StateDiskType string
|
2022-05-24 04:04:42 -04:00
|
|
|
Image string
|
|
|
|
UserAssingedIdentity string
|
|
|
|
LoadBalancerBackendAddressPool string
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// CreateResourceGroup creates a resource group.
|
|
|
|
func (c *Client) CreateResourceGroup(ctx context.Context) error {
|
|
|
|
_, err := c.resourceGroupAPI.CreateOrUpdate(ctx, c.name+"-"+c.uid,
|
|
|
|
armresources.ResourceGroup{
|
|
|
|
Location: &c.location,
|
|
|
|
}, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
c.resourceGroup = c.name + "-" + c.uid
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TerminateResourceGroup terminates a resource group.
|
|
|
|
func (c *Client) TerminateResourceGroup(ctx context.Context) error {
|
|
|
|
if c.resourceGroup == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
poller, err := c.resourceGroupAPI.BeginDelete(ctx, c.resourceGroup, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-07-27 16:02:33 -04:00
|
|
|
if _, err = poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{
|
2022-08-01 03:47:39 -04:00
|
|
|
Frequency: c.pollFrequency,
|
2022-07-27 16:02:33 -04:00
|
|
|
}); err != nil {
|
2022-03-22 11:03:15 -04:00
|
|
|
return err
|
|
|
|
}
|
2022-06-29 09:26:29 -04:00
|
|
|
c.workers = nil
|
|
|
|
c.controlPlanes = nil
|
2022-03-22 11:03:15 -04:00
|
|
|
c.resourceGroup = ""
|
|
|
|
c.subnetID = ""
|
|
|
|
c.networkSecurityGroup = ""
|
2022-06-29 09:26:29 -04:00
|
|
|
c.workerScaleSet = ""
|
|
|
|
c.controlPlaneScaleSet = ""
|
2022-03-22 11:03:15 -04:00
|
|
|
return nil
|
|
|
|
}
|
2022-08-24 05:31:43 -04:00
|
|
|
|
|
|
|
// scaleSetCreationPollingHandler is a custom poller used to check if a scale set was created successfully.
|
|
|
|
type scaleSetCreationPollingHandler struct {
|
|
|
|
done bool
|
|
|
|
resourceGroup string
|
|
|
|
scaleSet string
|
|
|
|
scaleSetsAPI scaleSetsAPI
|
|
|
|
}
|
|
|
|
|
|
|
|
// Done returns true if the condition is met.
|
|
|
|
func (h *scaleSetCreationPollingHandler) Done() bool {
|
|
|
|
return h.done
|
|
|
|
}
|
|
|
|
|
|
|
|
// Poll checks if the scale set resource was created successfully.
|
|
|
|
func (h *scaleSetCreationPollingHandler) Poll(ctx context.Context) error {
|
|
|
|
_, err := h.scaleSetsAPI.Get(ctx, h.resourceGroup, h.scaleSet, nil)
|
|
|
|
if err == nil {
|
|
|
|
h.done = true
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
var respErr *azcore.ResponseError
|
|
|
|
if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound {
|
|
|
|
// resource does not exist yet - retry later
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Result returns the result of the poller if the condition is met.
|
|
|
|
// If the condition is not met, an error is returned.
|
|
|
|
func (h *scaleSetCreationPollingHandler) Result(ctx context.Context, out *bool) error {
|
|
|
|
if !h.done {
|
|
|
|
return fmt.Errorf("failed to create scale set")
|
|
|
|
}
|
|
|
|
*out = h.done
|
|
|
|
return nil
|
|
|
|
}
|