Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ GOTESTSUM_VER := v1.6.4
GOTESTSUM_BIN := gotestsum
GOTESTSUM := $(TOOLS_BIN_DIR)/$(GOTESTSUM_BIN)-$(GOTESTSUM_VER)

GINKGO_VER := v2.9.7
GINKGO_VER := v2.11.0
GINKGO_BIN := ginkgo
GINKGO := $(TOOLS_BIN_DIR)/$(GINKGO_BIN)-$(GINKGO_VER)

Expand Down
3 changes: 2 additions & 1 deletion api/v1alpha1/provider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ const (
// ProviderSpec is the desired state of the Provider.
type ProviderSpec struct {
// Version indicates the provider version.
Version string `json:"version"`
// +optional
Version string `json:"version,omitempty"`

// Manager defines the properties that can be enabled on the controller manager for the provider.
// +optional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1458,8 +1458,6 @@ spec:
version:
description: Version indicates the provider version.
type: string
required:
- version
type: object
status:
description: BootstrapProviderStatus defines the observed state of BootstrapProvider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1459,8 +1459,6 @@ spec:
version:
description: Version indicates the provider version.
type: string
required:
- version
type: object
status:
description: ControlPlaneProviderStatus defines the observed state of
Expand Down
2 changes: 0 additions & 2 deletions config/crd/bases/operator.cluster.x-k8s.io_coreproviders.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1458,8 +1458,6 @@ spec:
version:
description: Version indicates the provider version.
type: string
required:
- version
type: object
status:
description: CoreProviderStatus defines the observed state of CoreProvider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1459,8 +1459,6 @@ spec:
version:
description: Version indicates the provider version.
type: string
required:
- version
type: object
status:
description: InfrastructureProviderStatus defines the observed state of
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/genericprovider_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ func (r *GenericProviderReconciler) Reconcile(ctx context.Context, req reconcile

// Set the spec hash annotation if reconciliation was successful or reset it otherwise.
if res.IsZero() && err == nil {
// Recalculate spec hash in case it was changed during reconciliation process.
specHash, err = calculateHash(typedProvider.GetSpec())
if err != nil {
return ctrl.Result{}, err
}

annotations[appliedSpecHashAnnotation] = specHash
} else {
annotations[appliedSpecHashAnnotation] = ""
Expand Down
21 changes: 18 additions & 3 deletions internal/controller/manifests_downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"

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/labels"
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha1"
Expand Down Expand Up @@ -75,15 +76,25 @@ func (p *phaseReconciler) downloadManifests(ctx context.Context) (reconcile.Resu
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason)
}

spec := p.provider.GetSpec()

if spec.Version == "" {
// User didn't set the version, try to get repository default.
spec.Version = repo.DefaultVersion()

// Add version to the provider spec.
p.provider.SetSpec(spec)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why provider spec is set twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason spec is set twice is because it accommodates two distinct workflows:

  • When downloading manifests from GitHub/GitLab.
  • When working with config maps containing manifests.

For the first workflow, the version is set during the manifest download process when a fetch URL is used. In the case of config maps, the version setting occurs in the load phase. Regardless of the workflow, the spec update is executed only once.

}

// Fetch the provider metadata and components yaml files from the provided repository GitHub/GitLab.
metadataFile, err := repo.GetFile(p.options.Version, metadataFile)
metadataFile, err := repo.GetFile(spec.Version, metadataFile)
if err != nil {
err = fmt.Errorf("failed to read %q from the repository for provider %q: %w", metadataFile, p.provider.GetName(), err)

return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason)
}

componentsFile, err := repo.GetFile(p.options.Version, repo.ComponentsPath())
componentsFile, err := repo.GetFile(spec.Version, repo.ComponentsPath())
if err != nil {
err = fmt.Errorf("failed to read %q from the repository for provider %q: %w", componentsFile, p.provider.GetName(), err)

Expand Down Expand Up @@ -156,5 +167,9 @@ func (p *phaseReconciler) createManifestsConfigMap(ctx context.Context, metadata
},
})

return p.ctrlClient.Create(ctx, configMap)
if err := p.ctrlClient.Create(ctx, configMap); err != nil && !apierrors.IsAlreadyExists(err) {
return err
}

return nil
}
49 changes: 40 additions & 9 deletions internal/controller/phases.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,6 @@ func (p *phaseReconciler) initializePhaseReconciler(ctx context.Context) (reconc
return reconcile.Result{}, wrapPhaseError(err, operatorv1.UnknownProviderReason)
}

spec := p.provider.GetSpec()

// Store some provider specific inputs for passing it to clusterctl library
p.options = repository.ComponentsOptions{
TargetNamespace: p.provider.GetNamespace(),
SkipTemplateProcess: false,
Version: spec.Version,
}

return reconcile.Result{}, nil
}

Expand Down Expand Up @@ -167,6 +158,22 @@ func (p *phaseReconciler) load(ctx context.Context) (reconcile.Result, error) {
return reconcile.Result{}, wrapPhaseError(err, "failed to load the repository")
}

if spec.Version == "" {
// User didn't set the version, so we need to find the latest one from the matching config maps.
repoVersions, err := p.repo.GetVersions()
if err != nil {
return reconcile.Result{}, wrapPhaseError(err, fmt.Sprintf("failed to get a list of available versions for provider %q", p.provider.GetName()))
}

spec.Version, err = getLatestVersion(repoVersions)
if err != nil {
return reconcile.Result{}, wrapPhaseError(err, fmt.Sprintf("failed to get the latest version for provider %q", p.provider.GetName()))
}

// Add latest version to the provider spec.
p.provider.SetSpec(spec)
}

// Store some provider specific inputs for passing it to clusterctl library
p.options = repository.ComponentsOptions{
TargetNamespace: p.provider.GetNamespace(),
Expand Down Expand Up @@ -476,3 +483,27 @@ func repositoryFactory(providerConfig configclient.Provider, configVariablesClie

return nil, fmt.Errorf("invalid provider url. Only GitHub and GitLab are supported for %q schema", rURL.Scheme)
}

func getLatestVersion(repoVersions []string) (string, error) {
if len(repoVersions) == 0 {
err := fmt.Errorf("no versions available")

return "", wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason)
}

// Initialize latest version with the first element value.
latestVersion := versionutil.MustParseSemantic(repoVersions[0])

for _, versionString := range repoVersions {
parsedVersion, err := versionutil.ParseSemantic(versionString)
if err != nil {
return "", wrapPhaseError(err, fmt.Sprintf("cannot parse version string: %s", versionString))
}

if latestVersion.LessThan(parsedVersion) {
latestVersion = parsedVersion
}
}

return "v" + latestVersion.String(), nil
}
51 changes: 51 additions & 0 deletions internal/controller/phases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,54 @@ func TestRepositoryFactory(t *testing.T) {
})
}
}

func TestGetLatestVersion(t *testing.T) {
testCases := []struct {
name string
versions []string
expected string
expectError bool
}{
{
name: "Test empty input",
versions: []string{},
expected: "",
expectError: true,
},
{
name: "Test single version",
versions: []string{"v1.0.0"},
expected: "v1.0.0",
expectError: false,
},
{
name: "Test multiple versions",
versions: []string{"v1.0.0", "v2.0.0", "v1.5.0"},
expected: "v2.0.0",
expectError: false,
},
{
name: "Test incorrect versions",
versions: []string{"v1.0.0", "NOT_A_VERSION", "v1.5.0"},
expected: "",
expectError: true,
},
}

g := NewWithT(t)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := getLatestVersion(tc.versions)

if tc.expectError {
g.Expect(err).To(HaveOccurred())

return
}

g.Expect(got).To(Equal(tc.expected))
g.Expect(err).ToNot(HaveOccurred())
})
}
}
36 changes: 12 additions & 24 deletions internal/controller/preflight_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ var (
capiVersionIncompatibilityMessage = "CAPI operator is only compatible with %s providers, detected %s for provider %s."
invalidGithubTokenMessage = "Invalid github token, please check your github token value and its permissions" //nolint:gosec
waitingForCoreProviderReadyMessage = "Waiting for the core provider to be installed."
emptyVersionMessage = "Version cannot be empty"
)

// preflightChecks performs preflight checks before installing provider.
Expand All @@ -57,30 +56,19 @@ func preflightChecks(ctx context.Context, c client.Client, provider genericprovi

spec := provider.GetSpec()

// Check that provider version is not empty.
if spec.Version == "" {
log.Info("Version can't be empty")
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.EmptyVersionReason,
clusterv1.ConditionSeverityError,
emptyVersionMessage,
))

return ctrl.Result{}, fmt.Errorf("version can't be empty for provider %s", provider.GetName())
}

// Check that provider version contains a valid value.
if _, err := version.ParseSemantic(spec.Version); err != nil {
log.Info("Version contains invalid value")
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.IncorrectVersionFormatReason,
clusterv1.ConditionSeverityError,
err.Error(),
))
// Check that provider version contains a valid value if it's not empty.
if spec.Version != "" {
if _, err := version.ParseSemantic(spec.Version); err != nil {
log.Info("Version contains invalid value")
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.IncorrectVersionFormatReason,
clusterv1.ConditionSeverityError,
err.Error(),
))

return ctrl.Result{}, fmt.Errorf("version contains invalid value for provider %s", provider.GetName())
return ctrl.Result{}, fmt.Errorf("version contains invalid value for provider %q", provider.GetName())
}
}

// Check that if a predefined provider is being installed, and if it's not - ensure that FetchConfig is specified.
Expand Down
30 changes: 15 additions & 15 deletions internal/controller/preflight_checks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,34 +463,34 @@ func TestPreflightChecks(t *testing.T) {
},
},
{
name: "missing version, preflight check failed",
expectedError: true,
name: "missing version, preflight check passed",
providers: []genericprovider.GenericProvider{
&genericprovider.InfrastructureProviderWrapper{
InfrastructureProvider: &operatorv1.InfrastructureProvider{
&genericprovider.CoreProviderWrapper{
CoreProvider: &operatorv1.CoreProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "aws",
Name: "cluster-api",
Namespace: namespaceName1,
},
TypeMeta: metav1.TypeMeta{
Kind: "InfrastructureProvider",
Kind: "CoreProvider",
APIVersion: "operator.cluster.x-k8s.io/v1alpha1",
},
Spec: operatorv1.InfrastructureProviderSpec{
ProviderSpec: operatorv1.ProviderSpec{},
Spec: operatorv1.CoreProviderSpec{
ProviderSpec: operatorv1.ProviderSpec{
FetchConfig: &operatorv1.FetchConfiguration{
URL: "https://example.com",
},
},
},
},
},
},
expectedCondition: clusterv1.Condition{
Type: operatorv1.PreflightCheckCondition,
Reason: operatorv1.IncorrectVersionFormatReason,
Severity: clusterv1.ConditionSeverityError,
Message: "Version cannot be empty",
Status: corev1.ConditionFalse,
Type: operatorv1.PreflightCheckCondition,
Status: corev1.ConditionTrue,
},
providerList: &genericprovider.InfrastructureProviderListWrapper{
InfrastructureProviderList: &operatorv1.InfrastructureProviderList{},
providerList: &genericprovider.CoreProviderListWrapper{
CoreProviderList: &operatorv1.CoreProviderList{},
},
},
{
Expand Down
Loading