diff --git a/config/ks-core/charts/ks-crds/crds/kubesphere.io_repositories.yaml b/config/ks-core/charts/ks-crds/crds/kubesphere.io_repositories.yaml index ddbff7cfa..b2c43fd25 100644 --- a/config/ks-core/charts/ks-crds/crds/kubesphere.io_repositories.yaml +++ b/config/ks-core/charts/ks-crds/crds/kubesphere.io_repositories.yaml @@ -53,6 +53,10 @@ spec: description: The caBundle (base64 string) is used in helmExecutor to verify the helm server. type: string + depth: + description: The number of synchronized versions of each extension. + 0 means synchronized all versions, default is 3. + type: integer description: type: string image: diff --git a/pkg/controller/core/repository_controller.go b/pkg/controller/core/repository_controller.go index 188820485..d2a1bed3b 100644 --- a/pkg/controller/core/repository_controller.go +++ b/pkg/controller/core/repository_controller.go @@ -26,10 +26,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/retry" "k8s.io/klog/v2" + "k8s.io/utils/ptr" clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1" corev1alpha1 "kubesphere.io/api/core/v1alpha1" "kubesphere.io/utils/helm" @@ -188,6 +190,12 @@ func (r *RepositoryReconciler) syncExtensionsFromURL(ctx context.Context, repo * } for extensionName, versions := range index.Entries { + // check extensionName + if errs := validation.IsDNS1123Subdomain(extensionName); len(errs) > 0 { + logger.Info("invalid extension name", "extension", extensionName, "error", errs) + continue + } + extensionVersions := make([]corev1alpha1.ExtensionVersion, 0, len(versions)) for _, version := range versions { if version.Metadata == nil { @@ -247,25 +255,25 @@ func (r *RepositoryReconciler) syncExtensionsFromURL(ctx context.Context, repo * extensionVersions = append(extensionVersions, extensionVersion) } - latestExtensionVersion := getLatestExtensionVersion(extensionVersions) - if latestExtensionVersion == nil { + filteredVersions := filterExtensionVersions(extensionVersions, repo.Spec.Depth) + if len(filteredVersions) == 0 { continue } - - extension, err := r.createOrUpdateExtension(ctx, repo, extensionName, latestExtensionVersion) + // update extension of latest extensionVersion + extension, err := r.createOrUpdateExtension(ctx, repo, extensionName, ptr.To(filteredVersions[0])) if err != nil { if errors.Is(err, extensionRepoConflict) { continue } return fmt.Errorf("failed to create or update extension: %s", err) } - - for _, extensionVersion := range extensionVersions { + // create extensionVersions of filteredVersions + for _, extensionVersion := range filteredVersions { if err := r.createOrUpdateExtensionVersion(ctx, extension, &extensionVersion); err != nil { return fmt.Errorf("failed to create or update extension version: %s", err) } } - + // remove extensionVersions of existVersions if err := r.removeSuspendedExtensionVersion(ctx, repo, extension, extensionVersions); err != nil { return fmt.Errorf("failed to remove suspended extension version: %s", err) } diff --git a/pkg/controller/core/util.go b/pkg/controller/core/util.go index 647e6d58d..9107c31bd 100644 --- a/pkg/controller/core/util.go +++ b/pkg/controller/core/util.go @@ -13,6 +13,7 @@ import ( "io" "os" "path/filepath" + "slices" "sort" "strings" "text/template" @@ -82,26 +83,58 @@ func checkVersionConstraint(constraint string, version *semver.Version) bool { return targetVersion.Check(version) } -func getLatestExtensionVersion(versions []corev1alpha1.ExtensionVersion) *corev1alpha1.ExtensionVersion { - if len(versions) == 0 { - return nil - } +// filterExtensionVersions filters and sorts a slice of ExtensionVersion objects based on semantic versioning. +// It first validates and removes entries with invalid versions (non-semver format) and logs warnings for them. +// The remaining entries are sorted in descending order by version (latest first). +// Finally, the slice is truncated to the specified depth: +// - If depth is nil, it defaults to a pre-configured depth (DefaultRepositoryDepth). +// - If depth is 0, all valid entries are kept. +// +// The function returns the filtered and truncated list of ExtensionVersion objects. +func filterExtensionVersions(versions []corev1alpha1.ExtensionVersion, depth *int) []corev1alpha1.ExtensionVersion { + // Filter and parse valid versions. + parsedVersions := make([]struct { + semver *semver.Version + original corev1alpha1.ExtensionVersion + }, 0, len(versions)) - var latestVersion *corev1alpha1.ExtensionVersion - var latestSemver *semver.Version - - for i := range versions { - currSemver, err := semver.NewVersion(versions[i].Spec.Version) + for _, v := range versions { + parsed, err := semver.NewVersion(v.Spec.Version) if err != nil { - klog.Warningf("parse version failed, extension version: %s, err: %s", versions[i].Name, err) + klog.Warningf("failed to parse version, extension %s version: %s, err: %s", v.Name, v.Spec.Version, err) continue } - if latestSemver == nil || latestSemver.LessThan(currSemver) { - latestSemver = currSemver - latestVersion = &versions[i] - } + parsedVersions = append(parsedVersions, struct { + semver *semver.Version + original corev1alpha1.ExtensionVersion + }{semver: parsed, original: v}) } - return latestVersion + + // Sort by descending semantic version. + slices.SortFunc(parsedVersions, func(a, b struct { + semver *semver.Version + original corev1alpha1.ExtensionVersion + }) int { + return b.semver.Compare(a.semver) + }) + + // Determine truncation length. + end := len(parsedVersions) + if depth == nil { + end = corev1alpha1.DefaultRepositoryDepth + } else if *depth > 0 && *depth < len(parsedVersions) { + end = *depth + } + if end > len(parsedVersions) { + end = len(parsedVersions) + } + + // Extract the truncated versions. + filteredVersions := make([]corev1alpha1.ExtensionVersion, end) + for i := 0; i < end; i++ { + filteredVersions[i] = parsedVersions[i].original + } + return filteredVersions } func isReleaseNotFoundError(err error) bool { diff --git a/pkg/controller/core/util_test.go b/pkg/controller/core/util_test.go index 9198c331a..6bd205447 100644 --- a/pkg/controller/core/util_test.go +++ b/pkg/controller/core/util_test.go @@ -9,7 +9,9 @@ import ( "testing" "github.com/Masterminds/semver/v3" - + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" corev1alpha1 "kubesphere.io/api/core/v1alpha1" "kubesphere.io/kubesphere/pkg/version" @@ -115,3 +117,103 @@ func TestGetRecommendedExtensionVersion(t *testing.T) { }) } } + +func TestFilterExtensionVersions(t *testing.T) { + var getExtensionVersion = func(version string) corev1alpha1.ExtensionVersion { + return corev1alpha1.ExtensionVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-" + version, + }, + Spec: corev1alpha1.ExtensionVersionSpec{ + Version: version, + }, + } + } + + tests := []struct { + name string + versions []corev1alpha1.ExtensionVersion + depth *int + exceptVersions []corev1alpha1.ExtensionVersion + }{ + { + name: "invalid versions", + versions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("abc"), + getExtensionVersion("v1.1.1"), + getExtensionVersion("v1.1.2"), + }, + exceptVersions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("v1.1.2"), + getExtensionVersion("v1.1.1"), + }, + }, + { + name: "depth is null", // default value + versions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("v1.1.1"), + getExtensionVersion("v1.1.2"), + getExtensionVersion("v1.1.3"), + getExtensionVersion("v1.1.4"), + }, + exceptVersions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("v1.1.4"), + getExtensionVersion("v1.1.3"), + getExtensionVersion("v1.1.2"), + }, + }, + { + name: "depth is 0", // all value + versions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("v1.1.1"), + getExtensionVersion("v1.1.2"), + getExtensionVersion("v1.1.3"), + getExtensionVersion("v1.1.4"), + }, + depth: ptr.To(0), + exceptVersions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("v1.1.4"), + getExtensionVersion("v1.1.3"), + getExtensionVersion("v1.1.2"), + getExtensionVersion("v1.1.1"), + }, + }, + { + name: "depth over length range", // all value + versions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("v1.1.1"), + getExtensionVersion("v1.1.2"), + getExtensionVersion("v1.1.3"), + getExtensionVersion("v1.1.4"), + }, + depth: ptr.To(10), + exceptVersions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("v1.1.4"), + getExtensionVersion("v1.1.3"), + getExtensionVersion("v1.1.2"), + getExtensionVersion("v1.1.1"), + }, + }, + { + name: "depth in length range", // specific value + versions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("v1.1.1"), + getExtensionVersion("v1.1.2"), + getExtensionVersion("v1.1.3"), + getExtensionVersion("v1.1.4"), + }, + depth: ptr.To(2), + exceptVersions: []corev1alpha1.ExtensionVersion{ + getExtensionVersion("v1.1.4"), + getExtensionVersion("v1.1.3"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + versions := filterExtensionVersions(tt.versions, tt.depth) + assert.Equal(t, versions, tt.exceptVersions) + }) + } +} diff --git a/staging/src/kubesphere.io/api/core/v1alpha1/constants.go b/staging/src/kubesphere.io/api/core/v1alpha1/constants.go index 2fe87289d..b81dfc2a5 100644 --- a/staging/src/kubesphere.io/api/core/v1alpha1/constants.go +++ b/staging/src/kubesphere.io/api/core/v1alpha1/constants.go @@ -40,3 +40,7 @@ const ( ExecutorUpgradeHookImageAnnotation = "executor-hook-image.kubesphere.io/upgrade" ExecutorUninstallHookImageAnnotation = "executor-hook-image.kubesphere.io/uninstall" ) + +const ( + DefaultRepositoryDepth = 3 +) diff --git a/staging/src/kubesphere.io/api/core/v1alpha1/repository_types.go b/staging/src/kubesphere.io/api/core/v1alpha1/repository_types.go index 12e26281f..96fc6dfd5 100644 --- a/staging/src/kubesphere.io/api/core/v1alpha1/repository_types.go +++ b/staging/src/kubesphere.io/api/core/v1alpha1/repository_types.go @@ -29,6 +29,9 @@ type RepositorySpec struct { CABundle string `json:"caBundle,omitempty"` // --insecure-skip-tls-verify. default false Insecure bool `json:"insecure,omitempty"` + // The maximum number of synchronized versions for each extension. A value of 0 indicates that all versions will be synchronized. The default is 3. + // +optional + Depth *int `json:"depth,omitempty"` } type RepositoryStatus struct { diff --git a/staging/src/kubesphere.io/api/core/v1alpha1/zz_generated.deepcopy.go b/staging/src/kubesphere.io/api/core/v1alpha1/zz_generated.deepcopy.go index b1ea3e909..73c4fc616 100644 --- a/staging/src/kubesphere.io/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/staging/src/kubesphere.io/api/core/v1alpha1/zz_generated.deepcopy.go @@ -754,6 +754,11 @@ func (in *RepositorySpec) DeepCopyInto(out *RepositorySpec) { *out = new(UpdateStrategy) **out = **in } + if in.Depth != nil { + in, out := &in.Depth, &out.Depth + *out = new(int) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositorySpec.