sigs.k8s.io/cluster-api-provider-aws@v1.5.5/cmd/clusterawsadm/ami/helper.go (about)

     1  /*
     2  Copyright 2021 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 ami
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/aws/aws-sdk-go/aws"
    27  	"github.com/aws/aws-sdk-go/service/ec2"
    28  	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
    29  	"github.com/blang/semver"
    30  	"github.com/pkg/errors"
    31  
    32  	ec2service "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/ec2"
    33  )
    34  
    35  const (
    36  	latestStableReleaseURL = "https://dl.k8s.io/release/stable%s.txt"
    37  	tagPrefix              = "v"
    38  )
    39  
    40  func getSupportedOsList() []string {
    41  	return []string{"centos-7", "ubuntu-18.04", "ubuntu-20.04", "amazon-2", "flatcar-stable"}
    42  }
    43  
    44  func getimageRegionList() []string {
    45  	return []string{
    46  		"ap-northeast-1",
    47  		"ap-northeast-2",
    48  		"ap-south-1",
    49  		"ap-southeast-1",
    50  		"ap-southeast-2",
    51  		"ca-central-1",
    52  		"eu-central-1",
    53  		"eu-west-1",
    54  		"eu-west-2",
    55  		"eu-west-3",
    56  		"sa-east-1",
    57  		"us-east-1",
    58  		"us-east-2",
    59  		"us-west-1",
    60  		"us-west-2",
    61  	}
    62  }
    63  
    64  // LatestPatchRelease returns the latest patch release matching.
    65  func LatestPatchRelease(searchVersion string) (string, error) {
    66  	searchSemVer, err := semver.Make(strings.TrimPrefix(searchVersion, tagPrefix))
    67  	if err != nil {
    68  		return "", err
    69  	}
    70  	resp, err := http.Get(fmt.Sprintf(latestStableReleaseURL, "-"+strconv.Itoa(int(searchSemVer.Major))+"."+strconv.Itoa(int(searchSemVer.Minor))))
    71  	if err != nil {
    72  		return "", err
    73  	}
    74  	defer resp.Body.Close()
    75  	b, err := io.ReadAll(resp.Body)
    76  	if err != nil {
    77  		return "", err
    78  	}
    79  
    80  	return strings.TrimSpace(string(b)), nil
    81  }
    82  
    83  // PreviousMinorRelease returns the latest patch release for the previous version
    84  // of Kubernetes, e.g. v1.19.1 returns v1.18.8 as of Sep 2020.
    85  func PreviousMinorRelease(searchVersion string) (string, error) {
    86  	semVer, err := semver.Make(strings.TrimPrefix(searchVersion, tagPrefix))
    87  	if err != nil {
    88  		return "", err
    89  	}
    90  	semVer.Minor--
    91  
    92  	return LatestPatchRelease(semVer.String())
    93  }
    94  
    95  // getSupportedKubernetesVersions returns all possible k8s versions till last nth kubernetes release.
    96  func getSupportedKubernetesVersions(lastNReleases int) ([]string, error) {
    97  	currentVersion, err := latestStableRelease()
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	versionPatches, err := allPatchesForVersion(currentVersion)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	versionPatches = append(versionPatches, currentVersion)
   108  
   109  	for lastNReleases != 0 {
   110  		currentVersion, err = PreviousMinorRelease(currentVersion)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  
   115  		currentVersionPatches, err := allPatchesForVersion(currentVersion)
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  
   120  		versionPatches = append(versionPatches, currentVersion)
   121  		versionPatches = append(versionPatches, currentVersionPatches...)
   122  
   123  		lastNReleases--
   124  	}
   125  	return versionPatches, nil
   126  }
   127  
   128  // allPatchesForVersion return all patches for a given version starting with given minor release.
   129  func allPatchesForVersion(latestVersion string) ([]string, error) {
   130  	semVer, err := semver.Make(strings.TrimPrefix(latestVersion, tagPrefix))
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  	patchVersions := make([]string, 0)
   135  	versionStr := fmt.Sprintf("%s%d.%d", tagPrefix, semVer.Major, semVer.Minor)
   136  	for semVer.Patch != 0 {
   137  		semVer.Patch--
   138  		patchVersions = append(patchVersions, fmt.Sprintf("%s.%d", versionStr, semVer.Patch))
   139  	}
   140  	return patchVersions, nil
   141  }
   142  
   143  // latestStableRelease fetches the latest stable Kubernetes version
   144  // If it is a x.x.0 release, it gets the previous minor version.
   145  func latestStableRelease() (string, error) {
   146  	resp, err := http.Get(fmt.Sprintf(latestStableReleaseURL, ""))
   147  	if err != nil {
   148  		return "", err
   149  	}
   150  	defer resp.Body.Close()
   151  
   152  	b, err := io.ReadAll(resp.Body)
   153  	if err != nil {
   154  		return "", err
   155  	}
   156  	latestVersion := strings.TrimSpace(string(b))
   157  	tagPrefix := "v"
   158  	latestVersionSemVer, err := semver.Make(strings.TrimPrefix(latestVersion, tagPrefix))
   159  	if err != nil {
   160  		return "", err
   161  	}
   162  
   163  	// If it is the first release, use the previous version instead
   164  	if latestVersionSemVer.Patch == 0 {
   165  		latestVersionSemVer.Minor--
   166  		// Address to get stable release for a particular version is: https://dl.k8s.io/release/stable-1.19.txt"
   167  		olderVersion := fmt.Sprintf("-%v.%v", latestVersionSemVer.Major, latestVersionSemVer.Minor)
   168  		resp, err = http.Get(fmt.Sprintf(latestStableReleaseURL, olderVersion))
   169  		if err != nil {
   170  			return "", err
   171  		}
   172  		defer resp.Body.Close()
   173  		b, err = io.ReadAll(resp.Body)
   174  		if err != nil {
   175  			return "", err
   176  		}
   177  		latestVersion = strings.TrimSpace(string(b))
   178  	}
   179  	return latestVersion, nil
   180  }
   181  
   182  func getAllImages(ec2Client ec2iface.EC2API, ownerID string) (map[string][]*ec2.Image, error) {
   183  	if ownerID == "" {
   184  		ownerID = ec2service.DefaultMachineAMIOwnerID
   185  	}
   186  
   187  	describeImageInput := &ec2.DescribeImagesInput{
   188  		Filters: []*ec2.Filter{
   189  			{
   190  				Name:   aws.String("owner-id"),
   191  				Values: []*string{aws.String(ownerID)},
   192  			},
   193  			{
   194  				Name:   aws.String("architecture"),
   195  				Values: []*string{aws.String("x86_64")},
   196  			},
   197  			{
   198  				Name:   aws.String("state"),
   199  				Values: []*string{aws.String("available")},
   200  			},
   201  			{
   202  				Name:   aws.String("virtualization-type"),
   203  				Values: []*string{aws.String("hvm")},
   204  			},
   205  		},
   206  	}
   207  
   208  	out, err := ec2Client.DescribeImages(describeImageInput)
   209  	if err != nil {
   210  		return nil, errors.Wrap(err, "failed to fetch AMIs")
   211  	}
   212  	if len(out.Images) == 0 {
   213  		return nil, nil
   214  	}
   215  
   216  	imagesMap := make(map[string][]*ec2.Image)
   217  	for _, image := range out.Images {
   218  		arr := strings.Split(aws.StringValue(image.Name), "-")
   219  		arr = arr[:len(arr)-2]
   220  		name := strings.Join(arr, "-")
   221  		images, ok := imagesMap[name]
   222  		if !ok {
   223  			images = make([]*ec2.Image, 0)
   224  		}
   225  		imagesMap[name] = append(images, image)
   226  	}
   227  
   228  	return imagesMap, nil
   229  }
   230  
   231  func findAMI(imagesMap map[string][]*ec2.Image, baseOS, kubernetesVersion string) (*ec2.Image, error) {
   232  	amiNameFormat := "capa-ami-{{.BaseOS}}-{{.K8sVersion}}"
   233  	amiName, err := ec2service.GenerateAmiName(amiNameFormat, baseOS, kubernetesVersion)
   234  	if err != nil {
   235  		return nil, errors.Wrapf(err, "failed to process ami format: %q", amiNameFormat)
   236  	}
   237  
   238  	if val, ok := imagesMap[amiName]; ok && val != nil {
   239  		latestImage, err := ec2service.GetLatestImage(val)
   240  		if err != nil {
   241  			return nil, err
   242  		}
   243  		return latestImage, nil
   244  	}
   245  
   246  	return nil, nil
   247  }