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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,3 +40,7 @@ const (
|
||||
ExecutorUpgradeHookImageAnnotation = "executor-hook-image.kubesphere.io/upgrade"
|
||||
ExecutorUninstallHookImageAnnotation = "executor-hook-image.kubesphere.io/uninstall"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultRepositoryDepth = 3
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user