Skip to content

Commit b8643e6

Browse files
authored
Merge pull request #461 from Fedosin/plugin_improvements
🐛 Plugin Init improvements
2 parents d72fb20 + a3d27be commit b8643e6

File tree

2 files changed

+107
-19
lines changed

2 files changed

+107
-19
lines changed

cmd/plugin/cmd/init.go

Lines changed: 30 additions & 19 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{}
@@ -115,6 +117,13 @@ var initCmd = &cobra.Command{
115117
},
116118
}
117119

120+
var backoffOpts = wait.Backoff{
121+
Duration: 500 * time.Millisecond,
122+
Factor: 1.5,
123+
Steps: 10,
124+
Jitter: 0.4,
125+
}
126+
118127
func init() {
119128
initCmd.PersistentFlags().StringVar(&initOpts.kubeconfig, "kubeconfig", "",
120129
"Path to the kubeconfig for the management cluster. If unspecified, default discovery rules apply.")
@@ -186,16 +195,9 @@ func runInit() error {
186195
return fmt.Errorf("cannot deploy CAPI operator: %w", err)
187196
}
188197

189-
opts := wait.Backoff{
190-
Duration: 500 * time.Millisecond,
191-
Factor: 1.5,
192-
Steps: 10,
193-
Jitter: 0.4,
194-
}
195-
196198
log.Info("Waiting for CAPI Operator to be available...")
197199

198-
if err := wait.ExponentialBackoff(opts, func() (bool, error) {
200+
if err := wait.ExponentialBackoff(backoffOpts, func() (bool, error) {
199201
return CheckDeploymentAvailability(ctx, client, capiOperatorLabels)
200202
}); err != nil {
201203
return fmt.Errorf("cannot check CAPI operator availability: %w", err)
@@ -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
}
@@ -510,14 +514,21 @@ func createGenericProvider(ctx context.Context, client ctrlclient.Client, provid
510514
log.Info("Installing provider", "Type", provider.GetType(), "Name", name, "Version", version, "Namespace", namespace)
511515

512516
// Create the provider
513-
if err := client.Create(ctx, provider); err != nil {
514-
if !apierrors.IsAlreadyExists(err) {
515-
return nil, fmt.Errorf("cannot create provider: %w", err)
516-
}
517+
if err := wait.ExponentialBackoff(backoffOpts, func() (bool, error) {
518+
if err := client.Create(ctx, provider); err != nil {
519+
// If the provider already exists, return immediately and do not retry.
520+
if apierrors.IsAlreadyExists(err) {
521+
log.Info("Provider already exists, skipping creation", "Type", provider.GetType(), "Name", name, "Version", version, "Namespace", namespace)
522+
523+
return true, err
524+
}
517525

518-
log.Info("Provider already exists, skipping creation", "Type", provider.GetType(), "Name", name, "Version", version, "Namespace", namespace)
526+
return false, err
527+
}
519528

520-
return nil, err
529+
return true, nil
530+
}); err != nil {
531+
return nil, fmt.Errorf("cannot create provider: %w", err)
521532
}
522533

523534
return provider, nil

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)