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 }