Skip to content

Commit

Permalink
Merge pull request #461 from Fedosin/plugin_improvements
Browse files Browse the repository at this point in the history
🐛 Plugin Init improvements
  • Loading branch information
k8s-ci-robot authored Apr 9, 2024
2 parents d72fb20 + a3d27be commit b8643e6
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 19 deletions.
49 changes: 30 additions & 19 deletions cmd/plugin/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ type initOptions struct {
}

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

var initOpts = &initOptions{}
Expand Down Expand Up @@ -115,6 +117,13 @@ var initCmd = &cobra.Command{
},
}

var backoffOpts = wait.Backoff{
Duration: 500 * time.Millisecond,
Factor: 1.5,
Steps: 10,
Jitter: 0.4,
}

func init() {
initCmd.PersistentFlags().StringVar(&initOpts.kubeconfig, "kubeconfig", "",
"Path to the kubeconfig for the management cluster. If unspecified, default discovery rules apply.")
Expand Down Expand Up @@ -186,16 +195,9 @@ func runInit() error {
return fmt.Errorf("cannot deploy CAPI operator: %w", err)
}

opts := wait.Backoff{
Duration: 500 * time.Millisecond,
Factor: 1.5,
Steps: 10,
Jitter: 0.4,
}

log.Info("Waiting for CAPI Operator to be available...")

if err := wait.ExponentialBackoff(opts, func() (bool, error) {
if err := wait.ExponentialBackoff(backoffOpts, func() (bool, error) {
return CheckDeploymentAvailability(ctx, client, capiOperatorLabels)
}); err != nil {
return fmt.Errorf("cannot check CAPI operator availability: %w", err)
Expand Down Expand Up @@ -383,8 +385,6 @@ func deployCAPIOperator(ctx context.Context, opts *initOptions) error {
return fmt.Errorf("cannot create config client: %w", err)
}

capiOperatorManifestsURL := fmt.Sprintf(capiOperatorManifestsURLTemplate, opts.operatorVersion)

providerConfig := configclient.NewProvider(capiOperatorProviderName, capiOperatorManifestsURL, clusterctlv1.ProviderTypeUnknown)

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

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

log.Info("Detected latest operator version", "Version", opts.operatorVersion)
}
Expand Down Expand Up @@ -510,14 +514,21 @@ func createGenericProvider(ctx context.Context, client ctrlclient.Client, provid
log.Info("Installing provider", "Type", provider.GetType(), "Name", name, "Version", version, "Namespace", namespace)

// Create the provider
if err := client.Create(ctx, provider); err != nil {
if !apierrors.IsAlreadyExists(err) {
return nil, fmt.Errorf("cannot create provider: %w", err)
}
if err := wait.ExponentialBackoff(backoffOpts, func() (bool, error) {
if err := client.Create(ctx, provider); err != nil {
// If the provider already exists, return immediately and do not retry.
if apierrors.IsAlreadyExists(err) {
log.Info("Provider already exists, skipping creation", "Type", provider.GetType(), "Name", name, "Version", version, "Namespace", namespace)

return true, err
}

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

return nil, err
return true, nil
}); err != nil {
return nil, fmt.Errorf("cannot create provider: %w", err)
}

return provider, nil
Expand Down
77 changes: 77 additions & 0 deletions cmd/plugin/cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ import (
"errors"
"fmt"
"os"
"sort"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/version"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/clientcmd"
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

admissionv1 "k8s.io/api/admissionregistration/v1"
Expand Down Expand Up @@ -67,6 +70,8 @@ type genericProviderList interface {
operatorv1.GenericProviderList
}

var errNotFound = errors.New("404 Not Found")

// CreateKubeClient creates a kubernetes client from provided kubeconfig and kubecontext.
func CreateKubeClient(kubeconfigPath, kubeconfigContext string) (ctrlclient.Client, error) {
// Use specified kubeconfig path and context
Expand Down Expand Up @@ -181,3 +186,75 @@ func NewGenericProvider(providerType clusterctlv1.ProviderType) operatorv1.Gener
panic(fmt.Sprintf("unknown provider type %s", providerType))
}
}

// GetLatestRelease returns the latest patch release.
func GetLatestRelease(ctx context.Context, repo repository.Repository) (string, error) {
versions, err := repo.GetVersions(ctx)
if err != nil {
return "", fmt.Errorf("failed to get repository versions: %w", err)
}

// Search for the latest release according to semantic version ordering.
// Releases with tag name that are not in semver format are ignored.
parsedReleaseVersions := []*version.Version{}

for _, v := range versions {
sv, err := version.ParseSemantic(v)
if err != nil {
// discard releases with tags that are not a valid semantic versions (the user can point explicitly to such releases)
continue
}

parsedReleaseVersions = append(parsedReleaseVersions, sv)
}

versionCandidates := parsedReleaseVersions

if len(parsedReleaseVersions) == 0 {
return "", errors.New("failed to find releases tagged with a valid semantic version number")
}

// Sort parsed versions by semantic version order.
sort.SliceStable(versionCandidates, func(i, j int) bool {
// Prioritize release versions over pre-releases. For example v1.0.0 > v2.0.0-alpha
// If both are pre-releases, sort by semantic version order as usual.
if versionCandidates[j].PreRelease() == "" && versionCandidates[i].PreRelease() != "" {
return false
}
if versionCandidates[i].PreRelease() == "" && versionCandidates[j].PreRelease() != "" {
return true
}

return versionCandidates[j].LessThan(versionCandidates[i])
})

// Limit the number of searchable versions by 3.
size := 3
if size > len(versionCandidates) {
size = len(versionCandidates)
}

versionCandidates = versionCandidates[:size]

for _, v := range versionCandidates {
// Iterate through sorted versions and try to fetch a file from that release.
// If it's completed successfully, we get the latest release.
// Note: the fetched file will be cached and next time we will get it from the cache.
versionString := "v" + v.String()

_, err := repo.GetFile(ctx, versionString, repo.ComponentsPath())
if err != nil {
if errors.Is(err, errNotFound) {
// Ignore this version
continue
}

return "", err
}

return versionString, nil
}

// If we reached this point, it means we didn't find any release.
return "", errors.New("failed to find releases tagged with a valid semantic version number")
}

0 comments on commit b8643e6

Please sign in to comment.