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 }