sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/services/virtualmachineimages/images.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package virtualmachineimages
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/blang/semver"
    26  	"github.com/pkg/errors"
    27  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    28  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    29  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    30  )
    31  
    32  // Service provides operations on Azure VM Images.
    33  type Service struct {
    34  	Client
    35  	azure.Authorizer
    36  }
    37  
    38  // New creates a VM Images service.
    39  func New(auth azure.Authorizer) (*Service, error) {
    40  	client, err := NewClient(auth)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	return &Service{
    45  		Client:     client,
    46  		Authorizer: auth,
    47  	}, nil
    48  }
    49  
    50  // GetDefaultUbuntuImage returns the default image spec for Ubuntu.
    51  func (s *Service) GetDefaultUbuntuImage(ctx context.Context, location, k8sVersion string) (*infrav1.Image, error) {
    52  	v, err := semver.ParseTolerant(k8sVersion)
    53  	if err != nil {
    54  		return nil, errors.Wrapf(err, "unable to parse Kubernetes version \"%s\"", k8sVersion)
    55  	}
    56  
    57  	osVersion := getUbuntuOSVersion(v.Major, v.Minor, v.Patch)
    58  	publisher, offer := azure.DefaultImagePublisherID, azure.DefaultImageOfferID
    59  	skuID, version, err := s.getSKUAndVersion(
    60  		ctx, location, publisher, offer, k8sVersion, fmt.Sprintf("ubuntu-%s", osVersion))
    61  	if err != nil {
    62  		return nil, errors.Wrap(err, "failed to get default image")
    63  	}
    64  
    65  	defaultImage := &infrav1.Image{
    66  		Marketplace: &infrav1.AzureMarketplaceImage{
    67  			ImagePlan: infrav1.ImagePlan{
    68  				Publisher: publisher,
    69  				Offer:     offer,
    70  				SKU:       skuID,
    71  			},
    72  			Version: version,
    73  		},
    74  	}
    75  
    76  	return defaultImage, nil
    77  }
    78  
    79  // GetDefaultWindowsImage returns the default image spec for Windows.
    80  func (s *Service) GetDefaultWindowsImage(ctx context.Context, location, k8sVersion, runtime, osAndVersion string) (*infrav1.Image, error) {
    81  	v122 := semver.MustParse("1.22.0")
    82  	v, err := semver.ParseTolerant(k8sVersion)
    83  	if err != nil {
    84  		return nil, errors.Wrapf(err, "unable to parse Kubernetes version \"%s\"", k8sVersion)
    85  	}
    86  
    87  	// If containerd is specified we don't currently support less than 1.22
    88  	if v.LE(v122) && runtime == "containerd" {
    89  		return nil, errors.New("containerd image only supported in 1.22+")
    90  	}
    91  
    92  	if osAndVersion == "" {
    93  		osAndVersion = azure.DefaultWindowsOsAndVersion
    94  	}
    95  
    96  	// Starting with 1.22 we default to containerd for Windows unless the runtime flag is set.
    97  	if v.GE(v122) && runtime != "dockershim" && !strings.HasSuffix(osAndVersion, "-containerd") {
    98  		osAndVersion += "-containerd"
    99  	}
   100  
   101  	publisher, offer := azure.DefaultImagePublisherID, azure.DefaultWindowsImageOfferID
   102  	skuID, version, err := s.getSKUAndVersion(
   103  		ctx, location, publisher, offer, k8sVersion, osAndVersion)
   104  	if err != nil {
   105  		return nil, errors.Wrap(err, "failed to get default image")
   106  	}
   107  
   108  	defaultImage := &infrav1.Image{
   109  		Marketplace: &infrav1.AzureMarketplaceImage{
   110  			ImagePlan: infrav1.ImagePlan{
   111  				Publisher: publisher,
   112  				Offer:     offer,
   113  				SKU:       skuID,
   114  			},
   115  			Version: version,
   116  		},
   117  	}
   118  
   119  	return defaultImage, nil
   120  }
   121  
   122  // getSKUAndVersion gets the SKU ID and version of the image to use for the provided version of Kubernetes.
   123  // note: osAndVersion is expected to be in the format of {os}-{version} (ex: ubuntu-2004 or windows-2022)
   124  func (s *Service) getSKUAndVersion(ctx context.Context, location, publisher, offer, k8sVersion, osAndVersion string) (skuID string, imageVersion string, err error) {
   125  	ctx, log, done := tele.StartSpanWithLogger(ctx, "virtualmachineimages.Service.getSKUAndVersion")
   126  	defer done()
   127  
   128  	log.V(4).Info("Getting VM image SKU and version", "location", location, "publisher", publisher, "offer", offer, "k8sVersion", k8sVersion, "osAndVersion", osAndVersion)
   129  
   130  	v, err := semver.ParseTolerant(k8sVersion)
   131  	if err != nil {
   132  		return "", "", errors.Wrapf(err, "unable to parse Kubernetes version \"%s\" in spec, expected valid SemVer string", k8sVersion)
   133  	}
   134  
   135  	// Old SKUs before 1.21.12, 1.22.9, or 1.23.6 are named like "k8s-1dot21dot2-ubuntu-2004".
   136  	if k8sVersionInSKUName(v.Major, v.Minor, v.Patch) {
   137  		return fmt.Sprintf("k8s-%ddot%ddot%d-%s", v.Major, v.Minor, v.Patch, osAndVersion), azure.LatestVersion, nil
   138  	}
   139  
   140  	// New SKUs don't contain the Kubernetes version and are named like "ubuntu-2004-gen1".
   141  	sku := fmt.Sprintf("%s-gen1", osAndVersion)
   142  
   143  	imageCache, err := GetCache(s.Authorizer)
   144  	if err != nil {
   145  		return "", "", errors.Wrap(err, "failed to get image cache")
   146  	}
   147  	imageCache.client = s.Client
   148  
   149  	listImagesResponse, err := imageCache.Get(ctx, location, publisher, offer, sku)
   150  	if err != nil {
   151  		return "", "", errors.Wrapf(err, "unable to list VM images for publisher \"%s\" offer \"%s\" sku \"%s\"", publisher, offer, sku)
   152  	}
   153  
   154  	vmImages := listImagesResponse.VirtualMachineImageResourceArray
   155  	if len(vmImages) == 0 {
   156  		return "", "", errors.Errorf("no VM images found for publisher \"%s\" offer \"%s\" sku \"%s\"", publisher, offer, sku)
   157  	}
   158  
   159  	// Sort the VM image names descending, so more recent dates sort first.
   160  	// (The date is encoded into the end of the name, for example "124.0.20220512").
   161  	names := []string{}
   162  	for _, vmImage := range vmImages {
   163  		names = append(names, *vmImage.Name)
   164  	}
   165  	sort.Sort(sort.Reverse(sort.StringSlice(names)))
   166  
   167  	// Pick the first (most recent) one whose k8s version matches.
   168  	var version string
   169  	id := fmt.Sprintf("%d%d.%d", v.Major, v.Minor, v.Patch)
   170  	for _, name := range names {
   171  		if strings.HasPrefix(name, id) {
   172  			version = name
   173  			break
   174  		}
   175  	}
   176  	if version == "" {
   177  		return "", "", errors.Errorf("no VM image found for publisher \"%s\" offer \"%s\" sku \"%s\" with Kubernetes version \"%s\"", publisher, offer, sku, k8sVersion)
   178  	}
   179  
   180  	log.V(4).Info("Found VM image SKU and version", "location", location, "publisher", publisher, "offer", offer, "sku", sku, "version", version)
   181  
   182  	return sku, version, nil
   183  }
   184  
   185  // getUbuntuOSVersion returns the default Ubuntu OS version for the given Kubernetes version.
   186  func getUbuntuOSVersion(major, minor, patch uint64) string {
   187  	// Default to Ubuntu 22.04 LTS for Kubernetes v1.25.3 and later.
   188  	osVersion := "2204"
   189  	if major == 1 && minor == 21 && patch < 2 ||
   190  		major == 1 && minor == 20 && patch < 8 ||
   191  		major == 1 && minor == 19 && patch < 12 ||
   192  		major == 1 && minor == 18 && patch < 20 ||
   193  		major == 1 && minor < 18 {
   194  		osVersion = "1804"
   195  	} else if major == 1 && minor == 25 && patch < 3 ||
   196  		major == 1 && minor < 25 {
   197  		osVersion = "2004"
   198  	}
   199  	return osVersion
   200  }
   201  
   202  // k8sVersionInSKUName returns true if the k8s version is in the SKU name (the older style of naming).
   203  func k8sVersionInSKUName(major, minor, patch uint64) bool {
   204  	return (major == 1 && minor < 21) ||
   205  		(major == 1 && minor == 21 && patch <= 12) ||
   206  		(major == 1 && minor == 22 && patch <= 9) ||
   207  		(major == 1 && minor == 23 && patch <= 6)
   208  }