diff --git a/cmd/mapt/cmd/azure/hosts/linux.go b/cmd/mapt/cmd/azure/hosts/linux.go index 6fe21f249..4f1f2728b 100644 --- a/cmd/mapt/cmd/azure/hosts/linux.go +++ b/cmd/mapt/cmd/azure/hosts/linux.go @@ -7,6 +7,7 @@ import ( params "github.com/redhat-developer/mapt/cmd/mapt/cmd/constants" maptContext "github.com/redhat-developer/mapt/pkg/manager/context" azureLinux "github.com/redhat-developer/mapt/pkg/provider/azure/action/linux" + "github.com/redhat-developer/mapt/pkg/provider/azure/data" "github.com/redhat-developer/mapt/pkg/provider/util/instancetypes" "github.com/redhat-developer/mapt/pkg/util" @@ -25,14 +26,14 @@ const ( ) func GetUbuntuCmd() *cobra.Command { - return getLinuxCmd(cmdUbuntu, cmdUbuntuDesc, azureLinux.Ubuntu, defaultUbuntuVersion) + return getLinuxCmd(cmdUbuntu, cmdUbuntuDesc, data.Ubuntu, defaultUbuntuVersion) } func GetFedoraCmd() *cobra.Command { - return getLinuxCmd(cmdFedora, cmdFedoraDesc, azureLinux.Fedora, defaultFedoraVersion) + return getLinuxCmd(cmdFedora, cmdFedoraDesc, data.Fedora, defaultFedoraVersion) } -func getLinuxCmd(cmd, cmdDesc string, ostype azureLinux.OSType, defaultOSVersion string) *cobra.Command { +func getLinuxCmd(cmd, cmdDesc string, ostype data.OSType, defaultOSVersion string) *cobra.Command { c := &cobra.Command{ Use: cmd, Short: cmdDesc, @@ -47,7 +48,7 @@ func getLinuxCmd(cmd, cmdDesc string, ostype azureLinux.OSType, defaultOSVersion return c } -func getCreateLinux(ostype azureLinux.OSType, defaultOSVersion string) *cobra.Command { +func getCreateLinux(ostype data.OSType, defaultOSVersion string) *cobra.Command { c := &cobra.Command{ Use: params.CreateCmdName, Short: params.CreateCmdName, diff --git a/pkg/provider/azure/action/aks/aks.go b/pkg/provider/azure/action/aks/aks.go index cf466278f..8a18d3b09 100644 --- a/pkg/provider/azure/action/aks/aks.go +++ b/pkg/provider/azure/action/aks/aks.go @@ -209,9 +209,9 @@ func (r *AKSRequest) valuesCheckingSpot() (*string, *float64, error) { if r.Spot { bsc, err := spotAzure.GetBestSpotChoice(spotAzure.BestSpotChoiceRequest{ - VMTypes: []string{r.VMSize}, - OSType: "linux", - EvictioRateTolerance: r.SpotTolerance, + VMTypes: []string{r.VMSize}, + OSType: "linux", + EvictionRateTolerance: r.SpotTolerance, }) logging.Debugf("Best spot price option found: %v", bsc) if err != nil { diff --git a/pkg/provider/azure/action/linux/imageref.go b/pkg/provider/azure/action/linux/imageref.go deleted file mode 100644 index ee7bd1169..000000000 --- a/pkg/provider/azure/action/linux/imageref.go +++ /dev/null @@ -1,82 +0,0 @@ -package linux - -import ( - "fmt" - "strings" -) - -type OSType int - -const ( - Ubuntu OSType = iota + 1 - RHEL - Fedora -) - -const fedoraImageGalleryBase = "/CommunityGalleries/Fedora-5e266ba4-2250-406d-adad-5d73860d958f/Images/" - -type imageReference struct { - publisher string - offer string - sku string - // community gallery image ID - id string -} - -var ( - defaultImageRefs = map[OSType]map[string]imageReference{ - RHEL: { - "x86_64": { - publisher: "RedHat", - offer: "RHEL", - sku: "%s_%s", - }, - "arm64": { - publisher: "RedHat", - offer: "rhel-arm64", - sku: "%s_%s-arm64", - }, - }, - Ubuntu: { - "x86_64": { - publisher: "Canonical", - offer: "ubuntu-%s_%s-lts-daily", - sku: "server", - }, - }, - Fedora: { - "x86_64": { - id: fedoraImageGalleryBase + "Fedora-Cloud-%s-x64/Versions/latest", - }, - "arm64": { - id: fedoraImageGalleryBase + "Fedora-Cloud-%s-Arm64/Versions/latest", - }, - }, - } -) - -// version should came in format X.Y (major.minor) -func getImageRef(osTarget OSType, arch string, version string) (*imageReference, error) { - ir := defaultImageRefs[osTarget][arch] - versions := strings.Split(version, ".") - switch osTarget { - case Ubuntu: - return &imageReference{ - publisher: ir.publisher, - offer: fmt.Sprintf(ir.offer, versions[0], versions[1]), - sku: ir.sku, - }, nil - case RHEL: - return &imageReference{ - publisher: ir.publisher, - offer: ir.offer, - sku: fmt.Sprintf(ir.sku, versions[0], versions[1]), - }, nil - case Fedora: - return &imageReference{ - id: fmt.Sprintf(ir.id, versions[0]), - }, nil - } - return nil, fmt.Errorf("os type not supported") - -} diff --git a/pkg/provider/azure/action/linux/linux.go b/pkg/provider/azure/action/linux/linux.go index acb33ae15..768257213 100644 --- a/pkg/provider/azure/action/linux/linux.go +++ b/pkg/provider/azure/action/linux/linux.go @@ -11,6 +11,7 @@ import ( "github.com/redhat-developer/mapt/pkg/manager" maptContext "github.com/redhat-developer/mapt/pkg/manager/context" "github.com/redhat-developer/mapt/pkg/provider/azure" + "github.com/redhat-developer/mapt/pkg/provider/azure/data" "github.com/redhat-developer/mapt/pkg/provider/azure/module/network" virtualmachine "github.com/redhat-developer/mapt/pkg/provider/azure/module/virtual-machine" "github.com/redhat-developer/mapt/pkg/provider/util/command" @@ -39,7 +40,7 @@ type LinuxRequest struct { VMSizes []string Arch string InstanceRequest instancetypes.InstanceRequest - OSType OSType + OSType data.OSType Version string Username string Spot bool @@ -121,7 +122,7 @@ func (r *LinuxRequest) deployer(ctx *pulumi.Context) error { } ctx.Export(fmt.Sprintf("%s-%s", r.Prefix, outputUserPrivateKey), privateKey.PrivateKeyPem) // Image refence info - ir, err := getImageRef(r.OSType, r.Arch, r.Version) + ir, err := data.GetImageRef(r.OSType, r.Arch, r.Version) if err != nil { return err } @@ -132,10 +133,10 @@ func (r *LinuxRequest) deployer(ctx *pulumi.Context) error { ResourceGroup: rg, NetworkInteface: n.NetworkInterface, VMSize: vmType, - Publisher: ir.publisher, - Offer: ir.offer, - Sku: ir.sku, - ImageID: ir.id, + Publisher: ir.Publisher, + Offer: ir.Offer, + Sku: ir.Sku, + ImageID: ir.ID, AdminUsername: r.Username, PrivateKey: privateKey, SpotPrice: spotPrice, @@ -174,11 +175,16 @@ func (r *LinuxRequest) deployer(ctx *pulumi.Context) error { func (r *LinuxRequest) valuesCheckingSpot() (*string, string, *float64, error) { if r.Spot { + ir, err := data.GetImageRef(r.OSType, r.Arch, r.Version) + if err != nil { + return nil, "", nil, err + } bsc, err := spotAzure.GetBestSpotChoice(spotAzure.BestSpotChoiceRequest{ - VMTypes: util.If(len(r.VMSizes) > 0, r.VMSizes, []string{defaultVMSize}), - OSType: "linux", - EvictioRateTolerance: r.SpotTolerance, + VMTypes: util.If(len(r.VMSizes) > 0, r.VMSizes, []string{defaultVMSize}), + OSType: "linux", + EvictionRateTolerance: r.SpotTolerance, + ImageRef: *ir, }) logging.Debugf("Best spot price option found: %v", bsc) if err != nil { diff --git a/pkg/provider/azure/action/rhel/rhel.go b/pkg/provider/azure/action/rhel/rhel.go index b0965236d..dc2a1718e 100644 --- a/pkg/provider/azure/action/rhel/rhel.go +++ b/pkg/provider/azure/action/rhel/rhel.go @@ -4,6 +4,7 @@ import ( "fmt" azureLinux "github.com/redhat-developer/mapt/pkg/provider/azure/action/linux" + "github.com/redhat-developer/mapt/pkg/provider/azure/data" "github.com/redhat-developer/mapt/pkg/provider/util/command" "github.com/redhat-developer/mapt/pkg/provider/util/instancetypes" spotAzure "github.com/redhat-developer/mapt/pkg/spot/azure" @@ -53,7 +54,7 @@ func Create(r *Request) (err error) { InstanceRequest: r.InstanceRequest, Version: r.Version, Arch: r.Arch, - OSType: azureLinux.RHEL, + OSType: data.RHEL, Username: r.Username, Spot: r.Spot, SpotTolerance: r.SpotTolerance, diff --git a/pkg/provider/azure/action/windows/windows.go b/pkg/provider/azure/action/windows/windows.go index 9947e89e0..3d082746d 100644 --- a/pkg/provider/azure/action/windows/windows.go +++ b/pkg/provider/azure/action/windows/windows.go @@ -172,9 +172,9 @@ func (r *WindowsRequest) valuesCheckingSpot() (*string, string, *float64, error) if r.Spot { bsc, err := spotAzure.GetBestSpotChoice(spotAzure.BestSpotChoiceRequest{ - VMTypes: util.If(len(r.VMSizes) > 0, r.VMSizes, []string{defaultVMSize}), - OSType: "windows", - EvictioRateTolerance: r.SpotTolerance, + VMTypes: util.If(len(r.VMSizes) > 0, r.VMSizes, []string{defaultVMSize}), + OSType: "windows", + EvictionRateTolerance: r.SpotTolerance, }) logging.Debugf("Best spot price option found: %v", bsc) if err != nil { diff --git a/pkg/provider/azure/data/imageref.go b/pkg/provider/azure/data/imageref.go new file mode 100644 index 000000000..f32c7076a --- /dev/null +++ b/pkg/provider/azure/data/imageref.go @@ -0,0 +1,81 @@ +package data + +import ( + "fmt" + "strings" +) + +type OSType int + +const ( + Ubuntu OSType = iota + 1 + RHEL + Fedora +) + +const fedoraImageGalleryBase = "/CommunityGalleries/Fedora-5e266ba4-2250-406d-adad-5d73860d958f/Images/" + +type ImageReference struct { + Publisher string + Offer string + Sku string + // community gallery image ID + ID string +} + +var ( + defaultImageRefs = map[OSType]map[string]ImageReference{ + RHEL: { + "x86_64": { + Publisher: "RedHat", + Offer: "RHEL", + Sku: "%s_%s", + }, + "arm64": { + Publisher: "RedHat", + Offer: "rhel-arm64", + Sku: "%s_%s-arm64", + }, + }, + Ubuntu: { + "x86_64": { + Publisher: "Canonical", + Offer: "ubuntu-%s_%s-lts-daily", + Sku: "server", + }, + }, + Fedora: { + "x86_64": { + ID: fedoraImageGalleryBase + "Fedora-Cloud-%s-x64/Versions/latest", + }, + "arm64": { + ID: fedoraImageGalleryBase + "Fedora-Cloud-%s-Arm64/Versions/latest", + }, + }, + } +) + +// version should came in format X.Y (major.minor) +func GetImageRef(osTarget OSType, arch string, version string) (*ImageReference, error) { + ir := defaultImageRefs[osTarget][arch] + versions := strings.Split(version, ".") + switch osTarget { + case Ubuntu: + return &ImageReference{ + Publisher: ir.Publisher, + Offer: fmt.Sprintf(ir.Offer, versions[0], versions[1]), + Sku: ir.Sku, + }, nil + case RHEL: + return &ImageReference{ + Publisher: ir.Publisher, + Offer: ir.Offer, + Sku: fmt.Sprintf(ir.Sku, versions[0], versions[1]), + }, nil + case Fedora: + return &ImageReference{ + ID: fmt.Sprintf(ir.ID, versions[0]), + }, nil + } + return nil, fmt.Errorf("os type not supported") +} diff --git a/pkg/provider/azure/data/images.go b/pkg/provider/azure/data/images.go new file mode 100644 index 000000000..884e1a37e --- /dev/null +++ b/pkg/provider/azure/data/images.go @@ -0,0 +1,59 @@ +package data + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6" + maptAzIdentity "github.com/redhat-developer/mapt/pkg/provider/azure/module/identity" + "github.com/redhat-developer/mapt/pkg/util/logging" +) + +type ImageRequest struct { + Region string + ImageReference +} + +func GetImage(req ImageRequest) (*armcompute.CommunityGalleryImagesClientGetResponse, error) { + maptAzIdentity.SetAZIdentityEnvs() + + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, err + } + ctx := context.Background() + subscriptionId := os.Getenv("AZURE_SUBSCRIPTION_ID") + + clientFactory, err := armcompute.NewClientFactory(subscriptionId, cred, nil) + if err != nil { + return nil, err + } + // for community gallary images + if len(req.ID) > 0 { + // extract gallary ID and image name from ID url which looks like: + // /CommunityGalleries/Fedora-5e266ba4-2250-406d-adad-5d73860d958f/Images/Fedora-Cloud-41-x64 + parts := strings.Split(req.ID, "/") + if len(parts) != 4 { + return nil, fmt.Errorf("invalida community gallary image ID: %s", req.ID) + } + res, err := clientFactory.NewCommunityGalleryImagesClient().Get(ctx, req.Region, parts[1], parts[3], nil) + if err != nil { + return nil, err + } + return &res, nil + } + // for azure offered VM images: https://learn.microsoft.com/en-us/rest/api/compute/virtual-machine-images/get + // there's a different API to check but currently we only check availability of community images + return nil, nil +} + +func IsImageOffered(req ImageRequest) bool { + if _, err := GetImage(req); err != nil { + logging.Debugf("error while checking if image available at location: %v", err) + return false + } + return true +} diff --git a/pkg/spot/azure/spot.go b/pkg/spot/azure/spot.go index 69c8905fb..cdbacab32 100644 --- a/pkg/spot/azure/spot.go +++ b/pkg/spot/azure/spot.go @@ -13,6 +13,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph" + "github.com/redhat-developer/mapt/pkg/provider/azure/data" maptAzIdentity "github.com/redhat-developer/mapt/pkg/provider/azure/module/identity" "github.com/redhat-developer/mapt/pkg/util" "github.com/redhat-developer/mapt/pkg/util/logging" @@ -42,9 +43,10 @@ const ( type EvictionRate int type BestSpotChoiceRequest struct { - VMTypes []string - OSType string - EvictioRateTolerance EvictionRate + VMTypes []string + OSType string + EvictionRateTolerance EvictionRate + ImageRef data.ImageReference } type BestSpotChoiceResponse struct { @@ -103,7 +105,7 @@ func GetBestSpotChoice(r BestSpotChoiceRequest) (*BestSpotChoiceResponse, error) return nil, fmt.Errorf("error eviction rates are returning empty") } // Compare prices and evictions - return getBestSpotChoice(phr, evrr, Lowest, r.EvictioRateTolerance) + return getBestSpotChoice(phr, evrr, Lowest, r.EvictionRateTolerance, r.ImageRef.ID) } func getGraphClient() (*armresourcegraph.Client, error) { @@ -204,7 +206,7 @@ func getEvictionRateInfoByVMTypes(ctx context.Context, client *armresourcegraph. return results, nil } -func getBestSpotChoice(s []priceHistory, e []evictionRate, currentERT EvictionRate, maxERT EvictionRate) (*BestSpotChoiceResponse, error) { +func getBestSpotChoice(s []priceHistory, e []evictionRate, currentERT EvictionRate, maxERT EvictionRate, imageID string) (*BestSpotChoiceResponse, error) { var evm map[string]string = make(map[string]string) for _, ev := range e { evm[fmt.Sprintf("%s%s", ev.Location, ev.VMType)] = ev.EvictionRate @@ -216,12 +218,21 @@ func getBestSpotChoice(s []priceHistory, e []evictionRate, currentERT EvictionRa // and pick one randomly to improve distribution of instances // across locations if ok && er == getEvictionRateValue(currentERT) { - spotChoices = append(spotChoices, - &BestSpotChoiceResponse{ - VMType: sv.VMType, - Location: sv.Location, - Price: sv.Price, - }) + ir := data.ImageRequest{ + Region: sv.Location, + ImageReference: data.ImageReference{ + ID: imageID, + }, + } + if data.IsImageOffered(ir) { + spotChoices = append(spotChoices, + &BestSpotChoiceResponse{ + VMType: sv.VMType, + Location: sv.Location, + Price: sv.Price, + }) + + } } } if len(spotChoices) > 0 { @@ -237,7 +248,7 @@ func getBestSpotChoice(s []priceHistory, e []evictionRate, currentERT EvictionRa if !ok { return nil, fmt.Errorf("could not find any spot") } - return getBestSpotChoice(s, e, *higherERT, maxERT) + return getBestSpotChoice(s, e, *higherERT, maxERT, imageID) } // Get previous higher evicition rate for a giving eviction rate diff --git a/pkg/spot/spot.go b/pkg/spot/spot.go index c3284a2d7..d99a3ea6b 100644 --- a/pkg/spot/spot.go +++ b/pkg/spot/spot.go @@ -126,9 +126,9 @@ func (sr *SpotRequest) GetAzureLowestPrice() (SpotPrice, error) { return SpotPrice{}, nil } spr := azure.BestSpotChoiceRequest{ - VMTypes: vms, - OSType: sr.getAzureOsType(), - EvictioRateTolerance: azure.EvictionRate(sr.EvictionRateTolerance), + VMTypes: vms, + OSType: sr.getAzureOsType(), + EvictionRateTolerance: azure.EvictionRate(sr.EvictionRateTolerance), } prices, err := azure.GetBestSpotChoice(spr)