feat: create extensionVersions by depth (#6286)

* feat: create extensionVersions by depth

* Apply suggestions from code review

Signed-off-by: hongming <coder.scala@gmail.com>

* Apply suggestions from code review

Signed-off-by: hongming <coder.scala@gmail.com>

---------

Signed-off-by: hongming <coder.scala@gmail.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
Co-authored-by: hongming <coder.scala@gmail.com>
This commit is contained in:
liujian
2024-11-26 10:47:36 +08:00
committed by GitHub
parent 238bd67b8f
commit 9c962d3cbf
7 changed files with 182 additions and 23 deletions

View File

@@ -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:

View File

@@ -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)
}

View File

@@ -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})
}
// 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
}
return latestVersion
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 {

View File

@@ -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)
})
}
}

View File

@@ -40,3 +40,7 @@ const (
ExecutorUpgradeHookImageAnnotation = "executor-hook-image.kubesphere.io/upgrade"
ExecutorUninstallHookImageAnnotation = "executor-hook-image.kubesphere.io/uninstall"
)
const (
DefaultRepositoryDepth = 3
)

View File

@@ -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 {

View File

@@ -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.