sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/repository/repository_versions.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 repository
    18  
    19  import (
    20  	"context"
    21  	"sort"
    22  
    23  	"github.com/pkg/errors"
    24  	"k8s.io/apimachinery/pkg/runtime"
    25  	"k8s.io/apimachinery/pkg/runtime/serializer"
    26  	"k8s.io/apimachinery/pkg/util/version"
    27  
    28  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    29  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
    30  )
    31  
    32  const (
    33  	latestVersionTag = "latest"
    34  )
    35  
    36  // latestContractRelease returns the latest patch release for a repository for the current API contract, according to
    37  // semantic version order of the release tag name.
    38  func latestContractRelease(ctx context.Context, repo Repository, contract string) (string, error) {
    39  	latest, err := latestRelease(ctx, repo)
    40  	if err != nil {
    41  		return latest, err
    42  	}
    43  	// Attempt to check if the latest release satisfies the API Contract
    44  	// This is a best-effort attempt to find the latest release for an older API contract if it's not the latest release.
    45  	file, err := repo.GetFile(ctx, latest, metadataFile)
    46  	// If an error occurs, we just return the latest release.
    47  	if err != nil {
    48  		if errors.Is(err, errNotFound) {
    49  			// If it was ErrNotFound, then there is no release yet for the resolved tag.
    50  			// Ref: https://github.com/kubernetes-sigs/cluster-api/issues/7889
    51  			return "", err
    52  		}
    53  		// if we can't get the metadata file from the release, we return latest.
    54  		return latest, nil
    55  	}
    56  	latestMetadata := &clusterctlv1.Metadata{}
    57  	codecFactory := serializer.NewCodecFactory(scheme.Scheme)
    58  	if err := runtime.DecodeInto(codecFactory.UniversalDecoder(), file, latestMetadata); err != nil {
    59  		return latest, nil //nolint:nilerr
    60  	}
    61  
    62  	releaseSeries := latestMetadata.GetReleaseSeriesForContract(contract)
    63  	if releaseSeries == nil {
    64  		return latest, nil
    65  	}
    66  
    67  	sv, err := version.ParseSemantic(latest)
    68  	if err != nil {
    69  		return latest, nil //nolint:nilerr
    70  	}
    71  
    72  	// If the Major or Minor version of the latest release doesn't match the release series for the current contract,
    73  	// return the latest patch release of the desired Major/Minor version.
    74  	if sv.Major() != releaseSeries.Major || sv.Minor() != releaseSeries.Minor {
    75  		return latestPatchRelease(ctx, repo, &releaseSeries.Major, &releaseSeries.Minor)
    76  	}
    77  	return latest, nil
    78  }
    79  
    80  // latestRelease returns the latest release for a repository, according to
    81  // semantic version order of the release tag name.
    82  func latestRelease(ctx context.Context, repo Repository) (string, error) {
    83  	return latestPatchRelease(ctx, repo, nil, nil)
    84  }
    85  
    86  // latestPatchRelease returns the latest patch release for a given Major and Minor version.
    87  func latestPatchRelease(ctx context.Context, repo Repository, major, minor *uint) (string, error) {
    88  	versions, err := repo.GetVersions(ctx)
    89  	if err != nil {
    90  		return "", errors.Wrapf(err, "failed to get repository versions")
    91  	}
    92  
    93  	// Search for the latest release according to semantic version ordering.
    94  	// Releases with tag name that are not in semver format are ignored.
    95  	versionCandidates := []*version.Version{}
    96  
    97  	for _, v := range versions {
    98  		sv, err := version.ParseSemantic(v)
    99  		if err != nil {
   100  			// discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases)
   101  			continue
   102  		}
   103  
   104  		if (major != nil && sv.Major() != *major) || (minor != nil && sv.Minor() != *minor) {
   105  			// skip versions that don't match the desired Major.Minor version.
   106  			continue
   107  		}
   108  
   109  		versionCandidates = append(versionCandidates, sv)
   110  	}
   111  
   112  	if len(versionCandidates) == 0 {
   113  		return "", errors.New("failed to find releases tagged with a valid semantic version number")
   114  	}
   115  
   116  	// Sort parsed versions by semantic version order.
   117  	sort.SliceStable(versionCandidates, func(i, j int) bool {
   118  		// Prioritize release versions over pre-releases. For example v1.0.0 > v2.0.0-alpha
   119  		// If both are pre-releases, sort by semantic version order as usual.
   120  		if versionCandidates[j].PreRelease() == "" && versionCandidates[i].PreRelease() != "" {
   121  			return false
   122  		}
   123  		if versionCandidates[i].PreRelease() == "" && versionCandidates[j].PreRelease() != "" {
   124  			return true
   125  		}
   126  
   127  		return versionCandidates[j].LessThan(versionCandidates[i])
   128  	})
   129  
   130  	// Limit the number of searchable versions by 5.
   131  	versionCandidates = versionCandidates[:min(5, len(versionCandidates))]
   132  
   133  	for _, v := range versionCandidates {
   134  		// Iterate through sorted versions and try to fetch a file from that release.
   135  		// If it's completed successfully, we get the latest release.
   136  		// Note: the fetched file will be cached and next time we will get it from the cache.
   137  		versionString := "v" + v.String()
   138  		_, err := repo.GetFile(ctx, versionString, metadataFile)
   139  		if err != nil {
   140  			if errors.Is(err, errNotFound) {
   141  				// Ignore this version
   142  				continue
   143  			}
   144  
   145  			return "", err
   146  		}
   147  
   148  		return versionString, nil
   149  	}
   150  
   151  	// If we reached this point, it means we didn't find any release.
   152  	return "", errors.New("failed to find releases tagged with a valid semantic version number")
   153  }