sigs.k8s.io/cluster-api@v1.6.3/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  
    22  	"github.com/pkg/errors"
    23  	"k8s.io/apimachinery/pkg/runtime"
    24  	"k8s.io/apimachinery/pkg/runtime/serializer"
    25  	"k8s.io/apimachinery/pkg/util/version"
    26  
    27  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    28  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
    29  )
    30  
    31  const (
    32  	latestVersionTag = "latest"
    33  )
    34  
    35  // latestContractRelease returns the latest patch release for a repository for the current API contract, according to
    36  // semantic version order of the release tag name.
    37  func latestContractRelease(ctx context.Context, repo Repository, contract string) (string, error) {
    38  	latest, err := latestRelease(ctx, repo)
    39  	if err != nil {
    40  		return latest, err
    41  	}
    42  	// Attempt to check if the latest release satisfies the API Contract
    43  	// This is a best-effort attempt to find the latest release for an older API contract if it's not the latest release.
    44  	file, err := repo.GetFile(ctx, latest, metadataFile)
    45  	// If an error occurs, we just return the latest release.
    46  	if err != nil {
    47  		if errors.Is(err, errNotFound) {
    48  			// If it was ErrNotFound, then there is no release yet for the resolved tag.
    49  			// Ref: https://github.com/kubernetes-sigs/cluster-api/issues/7889
    50  			return "", err
    51  		}
    52  		// if we can't get the metadata file from the release, we return latest.
    53  		return latest, nil
    54  	}
    55  	latestMetadata := &clusterctlv1.Metadata{}
    56  	codecFactory := serializer.NewCodecFactory(scheme.Scheme)
    57  	if err := runtime.DecodeInto(codecFactory.UniversalDecoder(), file, latestMetadata); err != nil {
    58  		return latest, nil //nolint:nilerr
    59  	}
    60  
    61  	releaseSeries := latestMetadata.GetReleaseSeriesForContract(contract)
    62  	if releaseSeries == nil {
    63  		return latest, nil
    64  	}
    65  
    66  	sv, err := version.ParseSemantic(latest)
    67  	if err != nil {
    68  		return latest, nil //nolint:nilerr
    69  	}
    70  
    71  	// If the Major or Minor version of the latest release doesn't match the release series for the current contract,
    72  	// return the latest patch release of the desired Major/Minor version.
    73  	if sv.Major() != releaseSeries.Major || sv.Minor() != releaseSeries.Minor {
    74  		return latestPatchRelease(ctx, repo, &releaseSeries.Major, &releaseSeries.Minor)
    75  	}
    76  	return latest, nil
    77  }
    78  
    79  // latestRelease returns the latest release for a repository, according to
    80  // semantic version order of the release tag name.
    81  func latestRelease(ctx context.Context, repo Repository) (string, error) {
    82  	return latestPatchRelease(ctx, repo, nil, nil)
    83  }
    84  
    85  // latestPatchRelease returns the latest patch release for a given Major and Minor version.
    86  func latestPatchRelease(ctx context.Context, repo Repository, major, minor *uint) (string, error) {
    87  	versions, err := repo.GetVersions(ctx)
    88  	if err != nil {
    89  		return "", errors.Wrapf(err, "failed to get repository versions")
    90  	}
    91  
    92  	// Search for the latest release according to semantic version ordering.
    93  	// Releases with tag name that are not in semver format are ignored.
    94  	var latestTag string
    95  	var latestPrereleaseTag string
    96  
    97  	var latestReleaseVersion *version.Version
    98  	var latestPrereleaseVersion *version.Version
    99  
   100  	for _, v := range versions {
   101  		sv, err := version.ParseSemantic(v)
   102  		if err != nil {
   103  			// discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases)
   104  			continue
   105  		}
   106  
   107  		if (major != nil && sv.Major() != *major) || (minor != nil && sv.Minor() != *minor) {
   108  			// skip versions that don't match the desired Major.Minor version.
   109  			continue
   110  		}
   111  
   112  		// track prereleases separately
   113  		if sv.PreRelease() != "" {
   114  			if latestPrereleaseVersion == nil || latestPrereleaseVersion.LessThan(sv) {
   115  				latestPrereleaseTag = v
   116  				latestPrereleaseVersion = sv
   117  			}
   118  			continue
   119  		}
   120  
   121  		if latestReleaseVersion == nil || latestReleaseVersion.LessThan(sv) {
   122  			latestTag = v
   123  			latestReleaseVersion = sv
   124  		}
   125  	}
   126  
   127  	// Fall back to returning latest prereleases if no release has been cut or bail if it's also empty
   128  	if latestTag == "" {
   129  		if latestPrereleaseTag == "" {
   130  			return "", errors.New("failed to find releases tagged with a valid semantic version number")
   131  		}
   132  
   133  		return latestPrereleaseTag, nil
   134  	}
   135  	return latestTag, nil
   136  }