Skip to content

Commit

Permalink
Use CAPZ community gallery for default VM images
Browse files Browse the repository at this point in the history
  • Loading branch information
mboersma committed Oct 7, 2024
1 parent 563cf73 commit 6c7d0db
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 193 deletions.
2 changes: 1 addition & 1 deletion azure/converters/vmss.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func GetOrchestrationMode(modeType infrav1.OrchestrationModeType) armcompute.Orc
return armcompute.OrchestrationModeUniform
}

// IDImageRefToImage converts an ID to a infrav1.Image with ComputerGallery set or ID, depending on the structure of the ID.
// IDImageRefToImage converts an ID to a infrav1.Image with ComputeGallery set or ID, depending on the structure of the ID.
func IDImageRefToImage(id string) infrav1.Image {
// compute gallery image
if ok, params := getParams(RegExpStrComputeGalleryID, id); ok {
Expand Down
6 changes: 6 additions & 0 deletions azure/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ const (
DefaultImagePublisherID = "cncf-upstream"
// LatestVersion is the image version latest.
LatestVersion = "latest"
// DefaultPublicGalleryName is the default Azure Compute Gallery.
DefaultPublicGalleryName = "capzed-489de9a5-a0a0-4e79-a806-ad5479ec43a5"
// DefaultLinuxGalleryImageName is the default Azure Linux Gallery Image Name.
DefaultLinuxGalleryImageName = "capi-ubun2-2404"
// DefaultWindowsGalleryImageName is the default Azure Windows Gallery Image Name.
DefaultWindowsGalleryImageName = "capi-win-2022-containerd"
)

const (
Expand Down
28 changes: 13 additions & 15 deletions azure/services/virtualmachineimages/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@ import (

// Key contains the fields necessary to locate a VM image list resource.
type Key struct {
location string
publisher string
offer string
sku string
location string
publicGalleryName string
galleryImageName string
}

// Cache stores VM image list resources.
type Cache struct {
client Client
data map[Key]armcompute.VirtualMachineImagesClientListResponse
data map[Key][]*armcompute.CommunityGalleryImageVersion
}

// Cacher allows getting items from and adding them to a cache.
Expand Down Expand Up @@ -95,7 +94,7 @@ func (c *Cache) refresh(ctx context.Context, key Key) error {
ctx, _, done := tele.StartSpanWithLogger(ctx, "virtualmachineimages.Cache.refresh")
defer done()

data, err := c.client.List(ctx, key.location, key.publisher, key.offer, key.sku)
data, err := c.client.List(ctx, key.location, key.publicGalleryName, key.galleryImageName)
if err != nil {
return errors.Wrap(err, "failed to refresh VM images cache")
}
Expand All @@ -106,28 +105,27 @@ func (c *Cache) refresh(ctx context.Context, key Key) error {
}

// Get returns a VM image list resource in a location given a publisher, offer, and sku.
func (c *Cache) Get(ctx context.Context, location, publisher, offer, sku string) (armcompute.VirtualMachineImagesClientListResponse, error) {
func (c *Cache) Get(ctx context.Context, location, publicGalleryName, galleryImageName string) ([]*armcompute.CommunityGalleryImageVersion, error) {
ctx, log, done := tele.StartSpanWithLogger(ctx, "virtualmachineimages.Cache.Get")
defer done()

if c.data == nil {
c.data = make(map[Key]armcompute.VirtualMachineImagesClientListResponse)
c.data = make(map[Key][]*armcompute.CommunityGalleryImageVersion)
}

key := Key{
location: location,
publisher: publisher,
offer: offer,
sku: sku,
location: location,
publicGalleryName: publicGalleryName,
galleryImageName: galleryImageName,
}

if _, ok := c.data[key]; !ok {
log.V(4).Info("VM images cache miss", "location", key.location, "publisher", key.publisher, "offer", key.offer, "sku", key.sku)
log.V(4).Info("VM images cache miss", "location", key.location, "publicGalleryName", key.publicGalleryName, "galleryImageName", key.galleryImageName)
if err := c.refresh(ctx, key); err != nil {
return armcompute.VirtualMachineImagesClientListResponse{}, err
return []*armcompute.CommunityGalleryImageVersion{}, err
}
} else {
log.V(4).Info("VM images cache hit", "location", key.location, "publisher", key.publisher, "offer", key.offer, "sku", key.sku)
log.V(4).Info("VM images cache hit", "location", key.location, "publicGalleryName", key.publicGalleryName, "galleryImageName", key.galleryImageName)
}

return c.data[key], nil
Expand Down
30 changes: 13 additions & 17 deletions azure/services/virtualmachineimages/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,23 @@ import (

func TestCacheGet(t *testing.T) {
cases := map[string]struct {
location string
publisher string
offer string
sku string
have armcompute.VirtualMachineImagesClientListResponse
expectedError error
location string
publicGalleryName string
galleryImageName string
have []*armcompute.CommunityGalleryImageVersion
expectedError error
}{
"should find": {
location: "test", publisher: "foo", offer: "bar", sku: "baz",
have: armcompute.VirtualMachineImagesClientListResponse{
VirtualMachineImageResourceArray: []*armcompute.VirtualMachineImageResource{
{Name: ptr.To("foo")},
},
location: "test", publicGalleryName: "foo", galleryImageName: "bar",
have: []*armcompute.CommunityGalleryImageVersion{
{Name: ptr.To("1.30.5")},
{Name: ptr.To("1.29.9")},
},
expectedError: nil,
},
"should not find": {
location: "test", publisher: "foo", offer: "bar", sku: "baz",
have: armcompute.VirtualMachineImagesClientListResponse{
VirtualMachineImageResourceArray: []*armcompute.VirtualMachineImageResource{},
},
location: "test", publicGalleryName: "foo", galleryImageName: "bar",
have: []*armcompute.CommunityGalleryImageVersion{},
expectedError: errors.New("failed to refresh VM images cache"),
},
}
Expand All @@ -63,11 +59,11 @@ func TestCacheGet(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockClient := mock_virtualmachineimages.NewMockClient(mockCtrl)
mockClient.EXPECT().List(gomock.Any(), tc.location, tc.publisher, tc.offer, tc.sku).Return(tc.have, tc.expectedError)
mockClient.EXPECT().List(gomock.Any(), tc.location, tc.publicGalleryName, tc.galleryImageName).Return(tc.have, tc.expectedError)
c := &Cache{client: mockClient}

g := NewWithT(t)
val, err := c.Get(context.Background(), tc.location, tc.publisher, tc.offer, tc.sku)
val, err := c.Get(context.Background(), tc.location, tc.publicGalleryName, tc.galleryImageName)
if tc.expectedError != nil {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tc.expectedError.Error()))
Expand Down
25 changes: 17 additions & 8 deletions azure/services/virtualmachineimages/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ import (

// Client is an interface for listing VM images.
type Client interface {
List(ctx context.Context, location, publisher, offer, sku string) (armcompute.VirtualMachineImagesClientListResponse, error)
List(ctx context.Context, location, publicGalleryName, galleryImageName string) ([]*armcompute.CommunityGalleryImageVersion, error)
}

// AzureClient contains the Azure go-sdk Client.
type AzureClient struct {
images *armcompute.VirtualMachineImagesClient
images *armcompute.CommunityGalleryImageVersionsClient
}

var _ Client = (*AzureClient)(nil)
Expand All @@ -42,20 +42,29 @@ var _ Client = (*AzureClient)(nil)
func NewClient(auth azure.Authorizer) (*AzureClient, error) {
opts, err := azure.ARMClientOptions(auth.CloudEnvironment())
if err != nil {
return nil, errors.Wrap(err, "failed to create virtualmachineimages client options")
return nil, errors.Wrap(err, "failed to create communitygalleryimageversions client options")
}
computeClientFactory, err := armcompute.NewClientFactory(auth.SubscriptionID(), auth.Token(), opts)
if err != nil {
return nil, errors.Wrap(err, "failed to create armcompute client factory")
}
return &AzureClient{computeClientFactory.NewVirtualMachineImagesClient()}, nil
return &AzureClient{computeClientFactory.NewCommunityGalleryImageVersionsClient()}, nil
}

// List returns a VM image list response.
func (ac *AzureClient) List(ctx context.Context, location, publisher, offer, sku string) (armcompute.VirtualMachineImagesClientListResponse, error) {
// List returns a community gallery image versions list response.
func (ac *AzureClient) List(ctx context.Context, location, publicGalleryName, galleryImageName string) ([]*armcompute.CommunityGalleryImageVersion, error) {
ctx, _, done := tele.StartSpanWithLogger(ctx, "virtualmachineimages.AzureClient.List")
defer done()

opts := &armcompute.VirtualMachineImagesClientListOptions{}
return ac.images.List(ctx, location, publisher, offer, sku, opts)
responses := make([]*armcompute.CommunityGalleryImageVersion, 0)
pager := ac.images.NewListPager(location, publicGalleryName, galleryImageName, nil)
for pager.More() {
resp, err := pager.NextPage(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to list image versions")
}
responses = append(responses, resp.CommunityGalleryImageVersionList.Value...)
}

return responses, nil
}
144 changes: 8 additions & 136 deletions azure/services/virtualmachineimages/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,12 @@ package virtualmachineimages

import (
"context"
"fmt"
"sort"
"strings"

"github.com/blang/semver"
"github.com/pkg/errors"

infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
"sigs.k8s.io/cluster-api-provider-azure/azure"
"sigs.k8s.io/cluster-api-provider-azure/util/tele"
)

// Service provides operations on Azure VM Images.
Expand Down Expand Up @@ -55,22 +51,11 @@ func (s *Service) GetDefaultUbuntuImage(ctx context.Context, location, k8sVersio
return nil, errors.Wrapf(err, "unable to parse Kubernetes version \"%s\"", k8sVersion)
}

osVersion := getUbuntuOSVersion(v.Major, v.Minor, v.Patch)
publisher, offer := azure.DefaultImagePublisherID, azure.DefaultImageOfferID
skuID, version, err := s.getSKUAndVersion(
ctx, location, publisher, offer, k8sVersion, fmt.Sprintf("ubuntu-%s", osVersion))
if err != nil {
return nil, errors.Wrap(err, "failed to get default image")
}

defaultImage := &infrav1.Image{
Marketplace: &infrav1.AzureMarketplaceImage{
ImagePlan: infrav1.ImagePlan{
Publisher: publisher,
Offer: offer,
SKU: skuID,
},
Version: version,
ComputeGallery: &infrav1.AzureComputeGalleryImage{
Gallery: azure.DefaultPublicGalleryName,
Name: azure.DefaultLinuxGalleryImageName,
Version: v.String(),
},
}

Expand All @@ -79,131 +64,18 @@ func (s *Service) GetDefaultUbuntuImage(ctx context.Context, location, k8sVersio

// GetDefaultWindowsImage returns the default image spec for Windows.
func (s *Service) GetDefaultWindowsImage(ctx context.Context, location, k8sVersion, runtime, osAndVersion string) (*infrav1.Image, error) {

Check failure on line 66 in azure/services/virtualmachineimages/images.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)

Check failure on line 66 in azure/services/virtualmachineimages/images.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
v122 := semver.MustParse("1.22.0")
v, err := semver.ParseTolerant(k8sVersion)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse Kubernetes version \"%s\"", k8sVersion)
}

// If containerd is specified we don't currently support less than 1.22
if v.LE(v122) && runtime == "containerd" {
return nil, errors.New("containerd image only supported in 1.22+")
}

if osAndVersion == "" {
osAndVersion = azure.DefaultWindowsOsAndVersion
}

// Starting with 1.22 we default to containerd for Windows unless the runtime flag is set.
if v.GE(v122) && runtime != "dockershim" && !strings.HasSuffix(osAndVersion, "-containerd") {
osAndVersion += "-containerd"
}

publisher, offer := azure.DefaultImagePublisherID, azure.DefaultWindowsImageOfferID
skuID, version, err := s.getSKUAndVersion(
ctx, location, publisher, offer, k8sVersion, osAndVersion)
if err != nil {
return nil, errors.Wrap(err, "failed to get default image")
}

defaultImage := &infrav1.Image{
Marketplace: &infrav1.AzureMarketplaceImage{
ImagePlan: infrav1.ImagePlan{
Publisher: publisher,
Offer: offer,
SKU: skuID,
},
Version: version,
ComputeGallery: &infrav1.AzureComputeGalleryImage{
Gallery: azure.DefaultPublicGalleryName,
Name: azure.DefaultWindowsGalleryImageName,
Version: v.String(),
},
}

return defaultImage, nil
}

// getSKUAndVersion gets the SKU ID and version of the image to use for the provided version of Kubernetes.
// note: osAndVersion is expected to be in the format of {os}-{version} (ex: ubuntu-2004 or windows-2022)
func (s *Service) getSKUAndVersion(ctx context.Context, location, publisher, offer, k8sVersion, osAndVersion string) (skuID string, imageVersion string, err error) {
ctx, log, done := tele.StartSpanWithLogger(ctx, "virtualmachineimages.Service.getSKUAndVersion")
defer done()

log.V(4).Info("Getting VM image SKU and version", "location", location, "publisher", publisher, "offer", offer, "k8sVersion", k8sVersion, "osAndVersion", osAndVersion)

v, err := semver.ParseTolerant(k8sVersion)
if err != nil {
return "", "", errors.Wrapf(err, "unable to parse Kubernetes version \"%s\" in spec, expected valid SemVer string", k8sVersion)
}

// Old SKUs before 1.21.12, 1.22.9, or 1.23.6 are named like "k8s-1dot21dot2-ubuntu-2004".
if k8sVersionInSKUName(v.Major, v.Minor, v.Patch) {
return fmt.Sprintf("k8s-%ddot%ddot%d-%s", v.Major, v.Minor, v.Patch, osAndVersion), azure.LatestVersion, nil
}

// New SKUs don't contain the Kubernetes version and are named like "ubuntu-2004-gen1".
sku := fmt.Sprintf("%s-gen1", osAndVersion)

imageCache, err := GetCache(s.Authorizer)
if err != nil {
return "", "", errors.Wrap(err, "failed to get image cache")
}
imageCache.client = s.Client

listImagesResponse, err := imageCache.Get(ctx, location, publisher, offer, sku)
if err != nil {
return "", "", errors.Wrapf(err, "unable to list VM images for publisher \"%s\" offer \"%s\" sku \"%s\"", publisher, offer, sku)
}

vmImages := listImagesResponse.VirtualMachineImageResourceArray
if len(vmImages) == 0 {
return "", "", errors.Errorf("no VM images found for publisher \"%s\" offer \"%s\" sku \"%s\"", publisher, offer, sku)
}

// Sort the VM image names descending, so more recent dates sort first.
// (The date is encoded into the end of the name, for example "124.0.20220512").
names := []string{}
for _, vmImage := range vmImages {
names = append(names, *vmImage.Name)
}
sort.Sort(sort.Reverse(sort.StringSlice(names)))

// Pick the first (most recent) one whose k8s version matches.
var version string
id := fmt.Sprintf("%d%d.%d", v.Major, v.Minor, v.Patch)
for _, name := range names {
if strings.HasPrefix(name, id) {
version = name
break
}
}
if version == "" {
return "", "", errors.Errorf("no VM image found for publisher \"%s\" offer \"%s\" sku \"%s\" with Kubernetes version \"%s\"", publisher, offer, sku, k8sVersion)
}

log.V(4).Info("Found VM image SKU and version", "location", location, "publisher", publisher, "offer", offer, "sku", sku, "version", version)

return sku, version, nil
}

// getUbuntuOSVersion returns the default Ubuntu OS version for the given Kubernetes version.
func getUbuntuOSVersion(major, minor, patch uint64) string {
// Default to Ubuntu 22.04 LTS for Kubernetes v1.25.3 and later.
osVersion := "2204"
if major == 1 && minor == 21 && patch < 2 ||
major == 1 && minor == 20 && patch < 8 ||
major == 1 && minor == 19 && patch < 12 ||
major == 1 && minor == 18 && patch < 20 ||
major == 1 && minor < 18 {
osVersion = "1804"
} else if major == 1 && minor == 25 && patch < 3 ||
major == 1 && minor < 25 {
osVersion = "2004"
}
return osVersion
}

// k8sVersionInSKUName returns true if the k8s version is in the SKU name (the older style of naming).
func k8sVersionInSKUName(major, minor, patch uint64) bool {
return (major == 1 && minor < 21) ||
(major == 1 && minor == 21 && patch <= 12) ||
(major == 1 && minor == 22 && patch <= 9) ||
(major == 1 && minor == 23 && patch <= 6)
}
Loading

0 comments on commit 6c7d0db

Please sign in to comment.