diff --git a/pkg/provider/aws/action/mac/bootstrap.sh b/pkg/provider/aws/action/mac/bootstrap.sh index 1f59275f4..0629cd64b 100644 --- a/pkg/provider/aws/action/mac/bootstrap.sh +++ b/pkg/provider/aws/action/mac/bootstrap.sh @@ -1,6 +1,16 @@ #!/bin/sh +# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-mac-instances.html +# Internal disk will be accessible on workspace dir +# mkdir -p "/Users/{{.Username}}/workspace" +echo 'if [[ ! -d /Volumes/InternalDisk ]]; then' | tee -a "/Users/{{.Username}}/.zshrc" +echo 'APFSVolumeName="InternalDisk" ; SSDContainer=$(diskutil list | grep "Physical Store disk0" -B 3 | grep "/dev/disk" | awk {"print $1"} ) ; diskutil apfs addVolume $SSDContainer APFS $APFSVolumeName' | tee -a "/Users/{{.Username}}/.zshrc" +echo 'sudo chown {{.Username}}:staff "/Volumes/InternalDisk"' | tee -a "/Users/{{.Username}}/.zshrc" +echo 'sudo chmod 0750 "/Volumes/InternalDisk"' | tee -a "/Users/{{.Username}}/.zshrc" +echo 'fi' | tee -a "/Users/{{.Username}}/.zshrc" + # Allow run x86 binaries on arm64 +# TODO review if still required for CrC sudo softwareupdate --install-rosetta --agree-to-license # Enable remote control (vnc) @@ -18,12 +28,12 @@ sudo defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "{{ sudo defaults write /Library/Preferences/.GlobalPreferences.plist com.apple.securitypref.logoutvalue -int 0 sudo defaults write /Library/Preferences/.GlobalPreferences.plist com.apple.autologout.AutoLogOutDelay -int 0 +sysadminctl -screenLock off -password "{{.Password}}" # Override authorized key +mkdir /Users/{{.Username}}/.ssh echo "{{.AuthorizedKey}}" | tee /Users/{{.Username}}/.ssh/authorized_keys -# TODO need to change the authorized key - # autologin to take effect # run reboot on background to successfully finish the remote exec of the script (sleep 2 && sudo reboot)& \ No newline at end of file diff --git a/pkg/provider/aws/action/mac/constants.go b/pkg/provider/aws/action/mac/constants.go index 5470c658a..0fecf6358 100644 --- a/pkg/provider/aws/action/mac/constants.go +++ b/pkg/provider/aws/action/mac/constants.go @@ -17,19 +17,14 @@ const ( outputDedicatedHostID = "ammDedicatedHostID" outputDedicatedHostAZ = "ammDedicatedHostAZ" outputRegion = "ammRegion" - // outputAdminUsername = "ammAdminUsername" - // outputAdminUserPassword = "ammAdminUserPassword" - // outputDHBackedURL = "ammDHBackedURL" - // outputDHProjectName = "ammDHProjectName" - - amiRegex = "amzn-ec2-macos-%s*" - // amiOwner = "628277914472" + amiRegex = "amzn-ec2-macos-%s*" archDefault = "m2" osVersionDefault = "14" vncDefaultPort int = 5900 diskSize int = 100 + blockDeviceType string = "gp3" defaultUsername string = "ec2-user" defaultSSHPort int = 22 diff --git a/pkg/provider/aws/action/mac/mac-dh.go b/pkg/provider/aws/action/mac/mac-dh.go index 75c3ed22b..29238da0a 100644 --- a/pkg/provider/aws/action/mac/mac-dh.go +++ b/pkg/provider/aws/action/mac/mac-dh.go @@ -57,7 +57,7 @@ func (r *MacRequest) createDedicatedHost() (dhi *HostInformation, err error) { return nil, err } i := getHostInformation(*host) - dhi = &i + dhi = i return } diff --git a/pkg/provider/aws/action/mac/mac-machine.go b/pkg/provider/aws/action/mac/mac-machine.go index 13a1f2bcb..e4d407a61 100644 --- a/pkg/provider/aws/action/mac/mac-machine.go +++ b/pkg/provider/aws/action/mac/mac-machine.go @@ -11,7 +11,6 @@ import ( "github.com/adrianriobo/qenvs/pkg/provider/aws/data" "github.com/adrianriobo/qenvs/pkg/provider/aws/modules/bastion" "github.com/adrianriobo/qenvs/pkg/provider/aws/modules/network" - "github.com/adrianriobo/qenvs/pkg/provider/aws/services/ec2/ami" qEC2 "github.com/adrianriobo/qenvs/pkg/provider/aws/services/ec2/compute" "github.com/adrianriobo/qenvs/pkg/provider/aws/services/ec2/keypair" securityGroup "github.com/adrianriobo/qenvs/pkg/provider/aws/services/ec2/security-group" @@ -46,7 +45,7 @@ type locked struct { Lock bool } -func isMachineLocked(prefix string, h HostInformation) (bool, error) { +func isMachineLocked(prefix string, h *HostInformation) (bool, error) { s, err := manager.CheckStack(manager.Stack{ StackName: qenvsContext.StackNameByProject(stackMacMachine), ProjectName: qenvsContext.ProjectName(), @@ -68,20 +67,15 @@ func isMachineLocked(prefix string, h HostInformation) (bool, error) { // This function will use the information from the // dedicated host holding the mac machine will check if stack exists // if exists will get the lock value from it -func (r *MacRequest) replaceMachine(h HostInformation) error { - machineLocked, err := isMachineLocked(r.Prefix, h) - if err != nil { - return err - } - if machineLocked { - return fmt.Errorf("can not replace machine is currently locked") - } - r.lock = true - if err := r.manageMacMachine(h); err != nil { - return err - } +func (r *MacRequest) replaceMachine(h *HostInformation) error { aN := fmt.Sprintf(amiRegex, r.Version) - ami, err := data.GetAMI(&aN, h.Arch, h.Region, nil) + bdt := blockDeviceType + ami, err := data.GetAMI( + data.ImageRequest{ + Name: &aN, + Arch: h.Arch, + Region: h.Region, + BlockDeviceType: &bdt}) if err != nil { return err } @@ -96,6 +90,10 @@ func (r *MacRequest) replaceMachine(h HostInformation) error { if err != nil { return err } + r.lock = true + if err := r.manageMacMachine(h); err != nil { + return err + } // replace will run again the boostrap script to generate // and set new keys to access the machine r.replace = true @@ -103,7 +101,7 @@ func (r *MacRequest) replaceMachine(h HostInformation) error { } // Release will set the lock as false -func (r *MacRequest) releaseLock(h HostInformation) error { +func (r *MacRequest) releaseLock(h *HostInformation) error { r.lock = false lockURN := fmt.Sprintf("urn:pulumi:%s::%s::%s::%s", qenvsContext.StackNameByProject(stackMacMachine), @@ -117,20 +115,20 @@ func (r *MacRequest) releaseLock(h HostInformation) error { } // Release will set the lock as false -func (r *MacRequest) createMacMachine(h HostInformation) error { +func (r *MacRequest) createMacMachine(h *HostInformation) error { r.lock = true return r.manageMacMachine(h) } // this creates the stack for the mac machine -func (r *MacRequest) manageMacMachine(h HostInformation) error { +func (r *MacRequest) manageMacMachine(h *HostInformation) error { return r.manageMacMachineTargets(h, nil) } // this creates the stack for the mac machine -func (r *MacRequest) manageMacMachineTargets(h HostInformation, targetURNs []string) error { +func (r *MacRequest) manageMacMachineTargets(h *HostInformation, targetURNs []string) error { r.AvailabilityZone = h.Host.AvailabilityZone - r.dedicatedHost = &h + r.dedicatedHost = h r.Region = h.Region cs := manager.Stack{ StackName: fmt.Sprintf("%s-%s", @@ -152,7 +150,7 @@ func (r *MacRequest) manageMacMachineTargets(h HostInformation, targetURNs []str } // this creates the stack for the mac machine -func (r *MacRequest) createAirgapMacMachine(h HostInformation) error { +func (r *MacRequest) createAirgapMacMachine(h *HostInformation) error { r.airgapPhaseConnectivity = network.ON err := r.createMacMachine(h) if err != nil { @@ -168,11 +166,15 @@ func (r *MacRequest) deployerMachine(ctx *pulumi.Context) error { ctx.Export(fmt.Sprintf("%s-%s", r.Prefix, outputRegion), pulumi.String(*r.Region)) ctx.Export(fmt.Sprintf("%s-%s", r.Prefix, outputDedicatedHostID), pulumi.String(*r.dedicatedHost.Host.HostId)) // Lookup AMI - ami, err := ami.GetAMIByName(ctx, - fmt.Sprintf(amiRegex, r.Version), - "", - map[string]string{ - "architecture": awsArchIDbyArch[r.Architecture]}) + aN := fmt.Sprintf(amiRegex, r.Version) + bdt := blockDeviceType + arch := awsArchIDbyArch[r.Architecture] + ami, err := data.GetAMI( + data.ImageRequest{ + Name: &aN, + Arch: &arch, + Region: r.Region, + BlockDeviceType: &bdt}) if err != nil { return err } @@ -296,14 +298,14 @@ func (r *MacRequest) securityGroups(ctx *pulumi.Context, // Create the mac instance func (r *MacRequest) instance(ctx *pulumi.Context, subnet *ec2.Subnet, - ami *ec2.LookupAmiResult, + ami *data.ImageInfo, keyResources *keypair.KeyPairResources, securityGroups pulumi.StringArray, ) (*ec2.Instance, error) { instanceArgs := ec2.InstanceArgs{ HostId: pulumi.String(*r.dedicatedHost.Host.HostId), SubnetId: subnet.ID(), - Ami: pulumi.String(ami.Id), + Ami: pulumi.String(*ami.Image.ImageId), InstanceType: pulumi.String(macTypesByArch[r.Architecture]), KeyName: keyResources.AWSKeyPair.KeyName, AssociatePublicIpAddress: pulumi.Bool(true), @@ -360,19 +362,15 @@ func (r *MacRequest) getBootstrapScript(ctx *pulumi.Context) ( *random.RandomPassword, *keypair.KeyPairResources, error) { - password, err := security.CreatePassword(ctx, - resourcesUtil.GetResourceName(r.Prefix, awsMacMachineID, "passwd")) - if err != nil { - return nil, nil, nil, err - } - // Create Keypair for the user - // If replace is enabled we need to re create the pk-user - // name := resourcesUtil.GetResourceName( - // r.Prefix, awsMacMachineID, "pk-user5be3717a") name := *r.dedicatedHost.RunID if r.replace { name = qenvsContext.CreateRunID() } + password, err := security.CreatePassword(ctx, + name) + if err != nil { + return nil, nil, nil, err + } ukpr := keypair.KeyPairRequest{ Name: name} ukp, err := ukpr.Create(ctx) diff --git a/pkg/provider/aws/action/mac/mac.go b/pkg/provider/aws/action/mac/mac.go index f04594cda..f07aff8db 100644 --- a/pkg/provider/aws/action/mac/mac.go +++ b/pkg/provider/aws/action/mac/mac.go @@ -2,6 +2,8 @@ package mac import ( _ "embed" + "fmt" + "strings" qenvsContext "github.com/adrianriobo/qenvs/pkg/manager/context" "github.com/adrianriobo/qenvs/pkg/provider/aws" @@ -28,17 +30,26 @@ import ( // // ... func Request(r *MacRequest) error { + // Get list of dedicated host ordered by allocation time his, err := getMatchingHostsInformation(r.Architecture) if err != nil { return err } // If no machines we will create one if len(his) == 0 { - return create(r) + return create(r, nil) } // Pick the most suited to be offered to the requester // and replcae (create fresh env) - hi := pickHost(his) + // If for whatever reason the mac has no been created + // stack does nt exist pick will require create not replace + hi, err := pickHost(r.Prefix, his) + if err != nil { + if hi == nil { + return err + } + return create(r, hi) + } err = r.replaceMachine(hi) if err != nil { return err @@ -129,30 +140,37 @@ func Destroy(prefix, hostID string) error { // It will also create a mac machine based on the arch and version setup // and will set a lock on it -func create(r *MacRequest) (err error) { - var dh *HostInformation - adh, err := getMatchingAvailableHostsInformation(r.Architecture) - if err != nil { - return err - } - // if no available dh create it - // otherwise pick the first (newest allocated) - if len(adh) == 0 { +func create(r *MacRequest, dh *HostInformation) (err error) { + if dh == nil { dh, err = r.createDedicatedHost() if err != nil { return err } - } else { - dh = &adh[0] } // Setup the topology and install the mac machine if !r.Airgap { - return r.createMacMachine(*dh) + return r.createMacMachine(dh) } - return r.createAirgapMacMachine(*dh) + return r.createAirgapMacMachine(dh) } -// TODO check which on the list is not locked -func pickHost(his []HostInformation) HostInformation { - return his[0] +// We will get a list of hosts from the pool ordered by allocation time +// We will apply several rules on them to pick the right one +// - TODO Remove those with allocation time > 24 h as they may destroyed +// - if none left use them again +// - if more available pick in order the first without lock +func pickHost(prefix string, his []*HostInformation) (*HostInformation, error) { + for _, h := range his { + isLocked, err := isMachineLocked(prefix, h) + if err != nil { + logging.Errorf("error checking if machine %s is locked", *h.Host.HostId) + if strings.Contains(err.Error(), "no stack") { + return h, err + } + } + if !isLocked { + return h, nil + } + } + return nil, fmt.Errorf("all hosts are locked at the moment") } diff --git a/pkg/provider/aws/action/mac/util.go b/pkg/provider/aws/action/mac/util.go index fd91cdb8d..bcd3d5944 100644 --- a/pkg/provider/aws/action/mac/util.go +++ b/pkg/provider/aws/action/mac/util.go @@ -13,11 +13,11 @@ import ( ) // Compose information around dedicated host -func getHostInformation(h ec2Types.Host) HostInformation { +func getHostInformation(h ec2Types.Host) *HostInformation { az := *h.AvailabilityZone region := az[:len(az)-1] archValue := awsArchIDbyArch[*getTagValue(h.Tags, tagKeyArch)] - return HostInformation{ + return &HostInformation{ Arch: &archValue, BackedURL: getTagValue(h.Tags, tagKeyBackedURL), ProjectName: getTagValue(h.Tags, qenvsContext.TagKeyProjectName), @@ -45,19 +45,20 @@ func getBackedURL() string { } // Get all dedicated hosts matching the tags + arch -func getMatchingHostsInformation(arch string) ([]HostInformation, error) { +// it will return the list ordered by allocation time +func getMatchingHostsInformation(arch string) ([]*HostInformation, error) { return getMatchingHostsInStateInformation(arch, nil) } // Get all dedicated hosts in available state ordered based on the allocation time // newer allocations go first -func getMatchingAvailableHostsInformation(arch string) ([]HostInformation, error) { - as := ec2Types.AllocationStateAvailable - return getMatchingHostsInStateInformation(arch, &as) -} +// func getMatchingAvailableHostsInformation(arch string) ([]HostInformation, error) { +// as := ec2Types.AllocationStateAvailable +// return getMatchingHostsInStateInformation(arch, &as) +// } // Get all dedicated hosts by tag and state -func getMatchingHostsInStateInformation(arch string, state *ec2Types.AllocationState) ([]HostInformation, error) { +func getMatchingHostsInStateInformation(arch string, state *ec2Types.AllocationState) ([]*HostInformation, error) { matchingTags := qenvsContext.GetTags() matchingTags[tagKeyArch] = arch hosts, err := data.GetDedicatedHosts(data.DedicatedHostResquest{ @@ -66,7 +67,7 @@ func getMatchingHostsInStateInformation(arch string, state *ec2Types.AllocationS if err != nil { return nil, err } - var r []HostInformation + var r []*HostInformation for _, dh := range hosts { if state == nil || (state != nil && dh.State == *state) { r = append(r, getHostInformation(dh)) @@ -74,7 +75,7 @@ func getMatchingHostsInStateInformation(arch string, state *ec2Types.AllocationS } // Order by allocation time, first newest if len(r) > 1 { - slices.SortFunc(r, func(a, b HostInformation) int { + slices.SortFunc(r, func(a, b *HostInformation) int { return b.Host.AllocationTime.Compare(*a.Host.AllocationTime) }) } @@ -96,6 +97,7 @@ func getRegion(r *MacRequest) (*string, error) { return nil, err } if isOffered { + logging.Debugf("%s offers it", os.Getenv("AWS_DEFAULT_REGION")) region := os.Getenv("AWS_DEFAULT_REGION") return ®ion, nil } @@ -105,6 +107,7 @@ func getRegion(r *MacRequest) (*string, error) { os.Getenv("AWS_DEFAULT_REGION")) } // We look for a region offering the type of instance + logging.Debugf("%s is not offered, a new region offering it will be used instead", os.Getenv("AWS_DEFAULT_REGION")) return data.LokupRegionOfferingInstanceType( macTypesByArch[r.Architecture]) } diff --git a/pkg/provider/aws/action/windows/windows.go b/pkg/provider/aws/action/windows/windows.go index 5ecddece8..0b491a679 100644 --- a/pkg/provider/aws/action/windows/windows.go +++ b/pkg/provider/aws/action/windows/windows.go @@ -100,7 +100,10 @@ func Create(r *Request) error { } r.az = *az } - isAMIOffered, _, err := data.IsAMIOffered(&r.AMIName, nil, &r.region) + isAMIOffered, _, err := data.IsAMIOffered( + data.ImageRequest{ + Name: &r.AMIName, + Region: &r.region}) if err != nil { return err } diff --git a/pkg/provider/aws/data/ami.go b/pkg/provider/aws/data/ami.go index a11517404..71a87a5d0 100644 --- a/pkg/provider/aws/data/ami.go +++ b/pkg/provider/aws/data/ami.go @@ -4,6 +4,9 @@ import ( "context" "fmt" "sync" + "time" + + "golang.org/x/exp/slices" "github.com/adrianriobo/qenvs/pkg/util/logging" "github.com/aws/aws-sdk-go-v2/config" @@ -16,11 +19,18 @@ type ImageInfo struct { Image *ec2Types.Image } -// GetAMI on a region based on name and arch -func GetAMI(amiName, amiArch, region, owner *string) (*ImageInfo, error) { +type ImageRequest struct { + Name, Arch, Owner *string + Region *string + BlockDeviceType *string +} + +// GetAMI based on params defined by request +// In case multiple AMIs it will return the newest +func GetAMI(r ImageRequest) (*ImageInfo, error) { var cfgOpts config.LoadOptionsFunc - if len(*region) > 0 { - cfgOpts = config.WithRegion(*region) + if len(*r.Region) > 0 { + cfgOpts = config.WithRegion(*r.Region) } cfg, err := config.LoadDefaultConfig(context.TODO(), cfgOpts) if err != nil { @@ -31,41 +41,60 @@ func GetAMI(amiName, amiArch, region, owner *string) (*ImageInfo, error) { filters := []ec2Types.Filter{ { Name: &filterName, - Values: []string{*amiName}}} - if amiArch != nil { - var filterArch = "architecture" + Values: []string{*r.Name}}} + if r.Arch != nil { + filter := "architecture" + filters = append(filters, ec2Types.Filter{ + Name: &filter, + Values: []string{*r.Arch}}) + } + if r.BlockDeviceType != nil { + filter := "block-device-mapping.volume-type" filters = append(filters, ec2Types.Filter{ - Name: &filterArch, - Values: []string{*amiArch}}) + Name: &filter, + Values: []string{*r.BlockDeviceType}}) } input := &ec2.DescribeImagesInput{ Filters: filters} - if owner != nil { - input.Owners = []string{*owner} + if r.Owner != nil { + input.Owners = []string{*r.Owner} } result, err := client.DescribeImages( context.Background(), input) if err != nil { - logging.Debugf("error checking %s in %s error is %v", *amiName, *region, err) + logging.Debugf("error checking %s in %s error is %v", *r.Name, *r.Region, err) return nil, err } if result == nil || len(result.Images) == 0 { - logging.Debugf("result len 0 checking %s in %s", *amiName, *region) - return nil, fmt.Errorf("no AMI %s in %s", *amiName, *region) + logging.Debugf("result len 0 checking %s in %s", *r.Name, *r.Region) + return nil, fmt.Errorf("no AMI %s in %s", *r.Name, *r.Region) } - logging.Debugf("len %d checking %s in %s", len(result.Images), *amiName, *region) + logging.Debugf("len %d checking %s in %s", len(result.Images), *r.Name, *r.Region) if err != nil { return nil, err } + if len(result.Images) > 1 { + slices.SortFunc(result.Images, func(a, b ec2Types.Image) int { + ac, err := time.Parse("2006-01-02", *a.CreationDate) + if err != nil { + return 0 + } + bc, err := time.Parse("2006-01-02", *b.CreationDate) + if err != nil { + return 0 + } + return bc.Compare(ac) + }) + } return &ImageInfo{ - Region: region, + Region: r.Region, Image: &result.Images[0]}, nil } // IsAMIOffered checks if an ami based on its Name is offered on a specific region -func IsAMIOffered(amiName, amiArch, region *string) (bool, *ImageInfo, error) { - ami, err := GetAMI(amiName, amiArch, region, nil) +func IsAMIOffered(r ImageRequest) (bool, *ImageInfo, error) { + ami, err := GetAMI(r) return ami != nil, ami, err } @@ -86,7 +115,11 @@ func FindAMI(amiName, amiArch *string) (*ImageInfo, error) { go func(r chan *ImageInfo) { defer wg.Done() if isOffered, i, _ := IsAMIOffered( - amiName, amiArch, &lRegion); isOffered { + ImageRequest{ + Name: amiName, + Arch: amiArch, + Region: &lRegion, + }); isOffered { r <- i } }(r) diff --git a/pkg/provider/aws/modules/spot/spot.go b/pkg/provider/aws/modules/spot/spot.go index 03a5bd1ec..0d15ea661 100644 --- a/pkg/provider/aws/modules/spot/spot.go +++ b/pkg/provider/aws/modules/spot/spot.go @@ -238,7 +238,11 @@ func checkBestOption(amiName, amiArch string, source []spotOptionInfo, // Check for AMI is optional, i.e if we will use custom AMIs which can be replicated // we want the best option and the we will take care for replicate the AMI if result && len(amiName) > 0 { - result, _, err = data.IsAMIOffered(&amiName, &amiArch, &price.Region) + result, _, err = data.IsAMIOffered( + data.ImageRequest{ + Name: &amiName, + Arch: &amiArch, + Region: &price.Region}) if err != nil { return false } diff --git a/pkg/provider/util/security/security.go b/pkg/provider/util/security/security.go index 351b4763a..738a4d89a 100644 --- a/pkg/provider/util/security/security.go +++ b/pkg/provider/util/security/security.go @@ -14,5 +14,6 @@ func CreatePassword(ctx *pulumi.Context, name string) (*random.RandomPassword, e Length: pulumi.Int(16), Special: pulumi.Bool(true), OverrideSpecial: pulumi.String(passwordOverrideSpecial), - }) + }, + pulumi.ReplaceOnChanges([]string{"name"})) }