Skip to content

Commit 9a5b0c6

Browse files
committed
Get the latest operator version from the list of all available versions
Now we use "latest" tag, but due to recent clusterctl changes it forces to download metadata.yaml file which is not available for CAPI operator. Because of that installation fails with exceeded context deadline. To prevent this we specify a version when we create a repo object, and then pick the latest version from the list of all available versions.
1 parent 74dba4e commit 9a5b0c6

File tree

2 files changed

+86
-5
lines changed

2 files changed

+86
-5
lines changed

cmd/plugin/cmd/init.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ type initOptions struct {
5858
}
5959

6060
const (
61-
capiOperatorProviderName = "capi-operator"
62-
capiOperatorManifestsURLTemplate = "https://github.com/kubernetes-sigs/cluster-api-operator/releases/%s/operator-components.yaml"
61+
capiOperatorProviderName = "capi-operator"
62+
// We have to specify a version here, because if we set "latest", clusterctl libs will try to fetch metadata.yaml file for the latest
63+
// release and fail since CAPI operator doesn't provide this file.
64+
capiOperatorManifestsURL = "https://github.com/kubernetes-sigs/cluster-api-operator/releases/v0.1.0/operator-components.yaml"
6365
)
6466

6567
var initOpts = &initOptions{}
@@ -383,8 +385,6 @@ func deployCAPIOperator(ctx context.Context, opts *initOptions) error {
383385
return fmt.Errorf("cannot create config client: %w", err)
384386
}
385387

386-
capiOperatorManifestsURL := fmt.Sprintf(capiOperatorManifestsURLTemplate, opts.operatorVersion)
387-
388388
providerConfig := configclient.NewProvider(capiOperatorProviderName, capiOperatorManifestsURL, clusterctlv1.ProviderTypeUnknown)
389389

390390
// Reduce waiting time for the repository creation from 30 seconds to 5.
@@ -397,7 +397,11 @@ func deployCAPIOperator(ctx context.Context, opts *initOptions) error {
397397
}
398398

399399
if opts.operatorVersion == latestVersion {
400-
opts.operatorVersion = repo.DefaultVersion()
400+
// Detecting the latest release by sorting all available tags and picking that last one with release.
401+
opts.operatorVersion, err = GetLatestRelease(ctx, repo)
402+
if err != nil {
403+
return fmt.Errorf("cannot get latest release: %w", err)
404+
}
401405

402406
log.Info("Detected latest operator version", "Version", opts.operatorVersion)
403407
}

cmd/plugin/cmd/utils.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@ import (
2121
"errors"
2222
"fmt"
2323
"os"
24+
"sort"
2425

2526
appsv1 "k8s.io/api/apps/v1"
2627
corev1 "k8s.io/api/core/v1"
2728
apierrors "k8s.io/apimachinery/pkg/api/errors"
2829
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2930
"k8s.io/apimachinery/pkg/runtime"
3031
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
32+
"k8s.io/apimachinery/pkg/util/version"
3133
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
3234
"k8s.io/client-go/tools/clientcmd"
3335
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
36+
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
3437
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
3538

3639
admissionv1 "k8s.io/api/admissionregistration/v1"
@@ -67,6 +70,8 @@ type genericProviderList interface {
6770
operatorv1.GenericProviderList
6871
}
6972

73+
var errNotFound = errors.New("404 Not Found")
74+
7075
// CreateKubeClient creates a kubernetes client from provided kubeconfig and kubecontext.
7176
func CreateKubeClient(kubeconfigPath, kubeconfigContext string) (ctrlclient.Client, error) {
7277
// Use specified kubeconfig path and context
@@ -181,3 +186,75 @@ func NewGenericProvider(providerType clusterctlv1.ProviderType) operatorv1.Gener
181186
panic(fmt.Sprintf("unknown provider type %s", providerType))
182187
}
183188
}
189+
190+
// GetLatestRelease returns the latest patch release.
191+
func GetLatestRelease(ctx context.Context, repo repository.Repository) (string, error) {
192+
versions, err := repo.GetVersions(ctx)
193+
if err != nil {
194+
return "", fmt.Errorf("failed to get repository versions: %w", err)
195+
}
196+
197+
// Search for the latest release according to semantic version ordering.
198+
// Releases with tag name that are not in semver format are ignored.
199+
parsedReleaseVersions := []*version.Version{}
200+
201+
for _, v := range versions {
202+
sv, err := version.ParseSemantic(v)
203+
if err != nil {
204+
// discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases)
205+
continue
206+
}
207+
208+
parsedReleaseVersions = append(parsedReleaseVersions, sv)
209+
}
210+
211+
versionCandidates := parsedReleaseVersions
212+
213+
if len(parsedReleaseVersions) == 0 {
214+
return "", errors.New("failed to find releases tagged with a valid semantic version number")
215+
}
216+
217+
// Sort parsed versions by semantic version order.
218+
sort.SliceStable(versionCandidates, func(i, j int) bool {
219+
// Prioritize release versions over pre-releases. For example v1.0.0 > v2.0.0-alpha
220+
// If both are pre-releases, sort by semantic version order as usual.
221+
if versionCandidates[j].PreRelease() == "" && versionCandidates[i].PreRelease() != "" {
222+
return false
223+
}
224+
if versionCandidates[i].PreRelease() == "" && versionCandidates[j].PreRelease() != "" {
225+
return true
226+
}
227+
228+
return versionCandidates[j].LessThan(versionCandidates[i])
229+
})
230+
231+
// Limit the number of searchable versions by 3.
232+
size := 3
233+
if size > len(versionCandidates) {
234+
size = len(versionCandidates)
235+
}
236+
237+
versionCandidates = versionCandidates[:size]
238+
239+
for _, v := range versionCandidates {
240+
// Iterate through sorted versions and try to fetch a file from that release.
241+
// If it's completed successfully, we get the latest release.
242+
// Note: the fetched file will be cached and next time we will get it from the cache.
243+
versionString := "v" + v.String()
244+
245+
_, err := repo.GetFile(ctx, versionString, repo.ComponentsPath())
246+
if err != nil {
247+
if errors.Is(err, errNotFound) {
248+
// Ignore this version
249+
continue
250+
}
251+
252+
return "", err
253+
}
254+
255+
return versionString, nil
256+
}
257+
258+
// If we reached this point, it means we didn't find any release.
259+
return "", errors.New("failed to find releases tagged with a valid semantic version number")
260+
}

0 commit comments

Comments
 (0)