feat: kubesphere 4.0 (#6115)
* feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> * feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> --------- Signed-off-by: ci-bot <ci-bot@kubesphere.io> Co-authored-by: ks-ci-bot <ks-ci-bot@example.com> Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
committed by
GitHub
parent
b5015ec7b9
commit
447a51f08b
104
pkg/controller/core/category_controller.go
Normal file
104
pkg/controller/core/category_controller.go
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
kscontroller "kubesphere.io/kubesphere/pkg/controller"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
|
||||
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
const (
|
||||
categoryController = "extension-category"
|
||||
countOfRelatedExtensions = "kubesphere.io/count"
|
||||
)
|
||||
|
||||
var _ kscontroller.Controller = &CategoryReconciler{}
|
||||
var _ reconcile.Reconciler = &CategoryReconciler{}
|
||||
|
||||
func (r *CategoryReconciler) Name() string {
|
||||
return categoryController
|
||||
}
|
||||
|
||||
func (r *CategoryReconciler) Enabled(clusterRole string) bool {
|
||||
return strings.EqualFold(clusterRole, string(clusterv1alpha1.ClusterRoleHost))
|
||||
}
|
||||
|
||||
type CategoryReconciler struct {
|
||||
client.Client
|
||||
recorder record.EventRecorder
|
||||
logger logr.Logger
|
||||
}
|
||||
|
||||
func (r *CategoryReconciler) SetupWithManager(mgr *kscontroller.Manager) error {
|
||||
r.Client = mgr.GetClient()
|
||||
r.logger = ctrl.Log.WithName("controllers").WithName(categoryController)
|
||||
r.recorder = mgr.GetEventRecorderFor(categoryController)
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named(categoryController).
|
||||
For(&corev1alpha1.Category{}).
|
||||
Watches(
|
||||
&corev1alpha1.Extension{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request {
|
||||
var requests []reconcile.Request
|
||||
extension := object.(*corev1alpha1.Extension)
|
||||
if category := extension.Labels[corev1alpha1.CategoryLabel]; category != "" {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: category,
|
||||
},
|
||||
})
|
||||
}
|
||||
return requests
|
||||
}),
|
||||
builder.WithPredicates(predicate.LabelChangedPredicate{}),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *CategoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := r.logger.WithValues("category", req.String())
|
||||
logger.V(4).Info("sync category")
|
||||
ctx = klog.NewContext(ctx, logger)
|
||||
|
||||
category := &corev1alpha1.Category{}
|
||||
if err := r.Client.Get(ctx, req.NamespacedName, category); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
extensions := &corev1alpha1.ExtensionList{}
|
||||
if err := r.List(ctx, extensions, client.MatchingLabels{corev1alpha1.CategoryLabel: category.Name}); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
total := strconv.Itoa(len(extensions.Items))
|
||||
if category.Annotations[countOfRelatedExtensions] != total {
|
||||
if category.Annotations == nil {
|
||||
category.Annotations = make(map[string]string)
|
||||
}
|
||||
category.Annotations[countOfRelatedExtensions] = total
|
||||
if err := r.Update(ctx, category); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
182
pkg/controller/core/extension_controller.go
Normal file
182
pkg/controller/core/extension_controller.go
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/go-logr/logr"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
|
||||
|
||||
kscontroller "kubesphere.io/kubesphere/pkg/controller"
|
||||
)
|
||||
|
||||
const (
|
||||
extensionProtection = "kubesphere.io/extension-protection"
|
||||
extensionController = "extension"
|
||||
)
|
||||
|
||||
var _ kscontroller.Controller = &ExtensionReconciler{}
|
||||
var _ reconcile.Reconciler = &ExtensionReconciler{}
|
||||
|
||||
func (r *ExtensionReconciler) Name() string {
|
||||
return extensionController
|
||||
}
|
||||
|
||||
func (r *ExtensionReconciler) Enabled(clusterRole string) bool {
|
||||
return strings.EqualFold(clusterRole, string(clusterv1alpha1.ClusterRoleHost))
|
||||
}
|
||||
|
||||
type ExtensionReconciler struct {
|
||||
client.Client
|
||||
k8sVersion *semver.Version
|
||||
logger logr.Logger
|
||||
}
|
||||
|
||||
func (r *ExtensionReconciler) SetupWithManager(mgr *kscontroller.Manager) error {
|
||||
r.Client = mgr.GetClient()
|
||||
r.k8sVersion = mgr.K8sVersion
|
||||
r.logger = ctrl.Log.WithName("controllers").WithName(extensionController)
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named(extensionController).
|
||||
For(&corev1alpha1.Extension{}).
|
||||
Watches(
|
||||
&corev1alpha1.ExtensionVersion{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request {
|
||||
var requests []reconcile.Request
|
||||
extensionVersion := object.(*corev1alpha1.ExtensionVersion)
|
||||
extensionName := extensionVersion.Labels[corev1alpha1.ExtensionReferenceLabel]
|
||||
if extensionName != "" {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: extensionName,
|
||||
},
|
||||
})
|
||||
}
|
||||
return requests
|
||||
}),
|
||||
builder.WithPredicates(predicate.Funcs{
|
||||
GenericFunc: func(event event.GenericEvent) bool {
|
||||
return false
|
||||
},
|
||||
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
|
||||
return false
|
||||
},
|
||||
}),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *ExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
extension := &corev1alpha1.Extension{}
|
||||
if err := r.Client.Get(ctx, req.NamespacedName, extension); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
r.logger.V(4).Info("reconcile", "extension", extension.Name)
|
||||
|
||||
if extension.ObjectMeta.DeletionTimestamp != nil {
|
||||
return r.reconcileDelete(ctx, extension)
|
||||
}
|
||||
|
||||
if !controllerutil.ContainsFinalizer(extension, extensionProtection) {
|
||||
expected := extension.DeepCopy()
|
||||
controllerutil.AddFinalizer(expected, extensionProtection)
|
||||
return ctrl.Result{}, r.Patch(ctx, expected, client.MergeFrom(extension))
|
||||
}
|
||||
|
||||
if err := r.syncExtensionStatus(ctx, extension); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("failed to sync extension status: %s", err)
|
||||
}
|
||||
|
||||
r.logger.V(4).Info("synced", "extension", extension.Name)
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// reconcileDelete delete the extension.
|
||||
func (r *ExtensionReconciler) reconcileDelete(ctx context.Context, extension *corev1alpha1.Extension) (ctrl.Result, error) {
|
||||
deletePolicy := metav1.DeletePropagationBackground
|
||||
if err := r.DeleteAllOf(ctx, &corev1alpha1.ExtensionVersion{}, &client.DeleteAllOfOptions{
|
||||
ListOptions: client.ListOptions{
|
||||
LabelSelector: labels.SelectorFromSet(labels.Set{corev1alpha1.ExtensionReferenceLabel: extension.Name}),
|
||||
},
|
||||
DeleteOptions: client.DeleteOptions{PropagationPolicy: &deletePolicy},
|
||||
}); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("failed to delete related ExtensionVersion: %s", err)
|
||||
}
|
||||
|
||||
// Remove the finalizer from the extension
|
||||
controllerutil.RemoveFinalizer(extension, extensionProtection)
|
||||
if err := r.Update(ctx, extension); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *ExtensionReconciler) syncExtensionStatus(ctx context.Context, extension *corev1alpha1.Extension) error {
|
||||
versionList := corev1alpha1.ExtensionVersionList{}
|
||||
if err := r.List(ctx, &versionList, client.MatchingLabels{
|
||||
corev1alpha1.ExtensionReferenceLabel: extension.Name,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
versions := make([]corev1alpha1.ExtensionVersionInfo, 0, len(versionList.Items))
|
||||
for i := range versionList.Items {
|
||||
if versionList.Items[i].DeletionTimestamp.IsZero() {
|
||||
versions = append(versions, corev1alpha1.ExtensionVersionInfo{
|
||||
Version: versionList.Items[i].Spec.Version,
|
||||
CreationTimestamp: versionList.Items[i].CreationTimestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(versions, func(i, j int) bool {
|
||||
return versions[i].Version < versions[j].Version
|
||||
})
|
||||
|
||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: extension.Name}, extension); err != nil {
|
||||
return err
|
||||
}
|
||||
expected := extension.DeepCopy()
|
||||
if recommended, err := getRecommendedExtensionVersion(versionList.Items, r.k8sVersion); err == nil {
|
||||
expected.Status.RecommendedVersion = recommended
|
||||
} else {
|
||||
r.logger.Error(err, "failed to get recommended extension version")
|
||||
}
|
||||
expected.Status.Versions = versions
|
||||
if expected.Status.RecommendedVersion != extension.Status.RecommendedVersion ||
|
||||
!reflect.DeepEqual(expected.Status.Versions, extension.Status.Versions) {
|
||||
return r.Update(ctx, expected)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update extension status: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
1716
pkg/controller/core/installplan_controller.go
Normal file
1716
pkg/controller/core/installplan_controller.go
Normal file
File diff suppressed because it is too large
Load Diff
98
pkg/controller/core/installplan_webhook.go
Normal file
98
pkg/controller/core/installplan_webhook.go
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
|
||||
|
||||
kscontroller "kubesphere.io/kubesphere/pkg/controller"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
var _ admission.CustomValidator = &InstallPlanWebhook{}
|
||||
var _ kscontroller.Controller = &InstallPlanWebhook{}
|
||||
|
||||
func (r *InstallPlanWebhook) Name() string {
|
||||
return "installplan-webhook"
|
||||
}
|
||||
|
||||
type InstallPlanWebhook struct {
|
||||
client.Client
|
||||
}
|
||||
|
||||
func trimSpace(data string) string {
|
||||
lines := strings.Split(data, "\n")
|
||||
var buf bytes.Buffer
|
||||
max := len(lines)
|
||||
for i, line := range lines {
|
||||
buf.Write([]byte(strings.TrimRightFunc(line, unicode.IsSpace)))
|
||||
if i < max-1 {
|
||||
buf.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (r *InstallPlanWebhook) Default(ctx context.Context, obj runtime.Object) error {
|
||||
installPlan := obj.(*corev1alpha1.InstallPlan)
|
||||
installPlan.Spec.Config = trimSpace(installPlan.Spec.Config)
|
||||
if installPlan.Spec.ClusterScheduling != nil {
|
||||
for k, v := range installPlan.Spec.ClusterScheduling.Overrides {
|
||||
installPlan.Spec.ClusterScheduling.Overrides[k] = trimSpace(v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InstallPlanWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
|
||||
return r.validateInstallPlan(ctx, obj.(*corev1alpha1.InstallPlan))
|
||||
}
|
||||
|
||||
func (r *InstallPlanWebhook) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) (admission.Warnings, error) {
|
||||
return r.validateInstallPlan(ctx, newObj.(*corev1alpha1.InstallPlan))
|
||||
}
|
||||
|
||||
func (r *InstallPlanWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *InstallPlanWebhook) validateInstallPlan(_ context.Context, installPlan *corev1alpha1.InstallPlan) (admission.Warnings, error) {
|
||||
var data interface{}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(installPlan.Spec.Config), &data); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal extension config: %v", err)
|
||||
}
|
||||
|
||||
if installPlan.Spec.ClusterScheduling != nil {
|
||||
for cluster, config := range installPlan.Spec.ClusterScheduling.Overrides {
|
||||
if err := yaml.Unmarshal([]byte(config), &data); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal cluster %s agent config: %v", cluster, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *InstallPlanWebhook) SetupWithManager(mgr *kscontroller.Manager) error {
|
||||
r.Client = mgr.GetClient()
|
||||
return ctrl.NewWebhookManagedBy(mgr).
|
||||
WithValidator(r).
|
||||
WithDefaulter(r).
|
||||
For(&corev1alpha1.InstallPlan{}).
|
||||
Complete()
|
||||
}
|
||||
553
pkg/controller/core/repository_controller.go
Normal file
553
pkg/controller/core/repository_controller.go
Normal file
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
kscontroller "kubesphere.io/kubesphere/pkg/controller"
|
||||
|
||||
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
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/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/klog/v2"
|
||||
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
|
||||
"kubesphere.io/utils/helm"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
)
|
||||
|
||||
const (
|
||||
repositoryProtection = "kubesphere.io/repository-protection"
|
||||
repositoryController = "repository"
|
||||
minimumRegistryPollInterval = 15 * time.Minute
|
||||
defaultRequeueInterval = 15 * time.Second
|
||||
generateNameFormat = "repository-%s"
|
||||
extensionFileName = "extension.yaml"
|
||||
)
|
||||
|
||||
var extensionRepoConflict = fmt.Errorf("extension repo mismatch")
|
||||
|
||||
var _ kscontroller.Controller = &RepositoryReconciler{}
|
||||
var _ reconcile.Reconciler = &RepositoryReconciler{}
|
||||
|
||||
func (r *RepositoryReconciler) Name() string {
|
||||
return repositoryController
|
||||
}
|
||||
|
||||
func (r *RepositoryReconciler) Enabled(clusterRole string) bool {
|
||||
return strings.EqualFold(clusterRole, string(clusterv1alpha1.ClusterRoleHost))
|
||||
}
|
||||
|
||||
type RepositoryReconciler struct {
|
||||
client.Client
|
||||
recorder record.EventRecorder
|
||||
logger logr.Logger
|
||||
}
|
||||
|
||||
func (r *RepositoryReconciler) SetupWithManager(mgr *kscontroller.Manager) error {
|
||||
r.Client = mgr.GetClient()
|
||||
r.logger = ctrl.Log.WithName("controllers").WithName(repositoryController)
|
||||
r.recorder = mgr.GetEventRecorderFor(repositoryController)
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named(repositoryController).
|
||||
For(&corev1alpha1.Repository{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *RepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := r.logger.WithValues("repository", req.String())
|
||||
logger.V(4).Info("sync repository")
|
||||
ctx = klog.NewContext(ctx, logger)
|
||||
|
||||
repo := &corev1alpha1.Repository{}
|
||||
if err := r.Client.Get(ctx, req.NamespacedName, repo); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
if !repo.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
return r.reconcileDelete(ctx, repo)
|
||||
}
|
||||
|
||||
if !controllerutil.ContainsFinalizer(repo, repositoryProtection) {
|
||||
expected := repo.DeepCopy()
|
||||
controllerutil.AddFinalizer(expected, repositoryProtection)
|
||||
return ctrl.Result{}, r.Patch(ctx, expected, client.MergeFrom(repo))
|
||||
}
|
||||
return r.reconcileRepository(ctx, repo)
|
||||
}
|
||||
|
||||
// reconcileDelete delete the repository and pod.
|
||||
func (r *RepositoryReconciler) reconcileDelete(ctx context.Context, repo *corev1alpha1.Repository) (ctrl.Result, error) {
|
||||
// Remove the finalizer from the subscription and update it.
|
||||
controllerutil.RemoveFinalizer(repo, repositoryProtection)
|
||||
if err := r.Update(ctx, repo); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// createOrUpdateExtension create a new extension if the extension does not exist.
|
||||
// Or it will update info of the extension.
|
||||
func (r *RepositoryReconciler) createOrUpdateExtension(ctx context.Context, repo *corev1alpha1.Repository, extensionName string, extensionVersion *corev1alpha1.ExtensionVersion) (*corev1alpha1.Extension, error) {
|
||||
logger := klog.FromContext(ctx)
|
||||
extension := &corev1alpha1.Extension{ObjectMeta: metav1.ObjectMeta{Name: extensionName}}
|
||||
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, extension, func() error {
|
||||
originRepoName := extension.Labels[corev1alpha1.RepositoryReferenceLabel]
|
||||
if originRepoName != "" && originRepoName != repo.Name {
|
||||
logger.Error(extensionRepoConflict, "conflict", "extension", extensionName, "want", originRepoName, "got", repo.Name)
|
||||
return extensionRepoConflict
|
||||
}
|
||||
|
||||
if extension.Labels == nil {
|
||||
extension.Labels = make(map[string]string)
|
||||
}
|
||||
if extensionVersion.Spec.Category != "" {
|
||||
extension.Labels[corev1alpha1.CategoryLabel] = extensionVersion.Spec.Category
|
||||
}
|
||||
extension.Labels[corev1alpha1.RepositoryReferenceLabel] = repo.Name
|
||||
extension.Spec.ExtensionInfo = extensionVersion.Spec.ExtensionInfo
|
||||
if err := controllerutil.SetOwnerReference(repo, extension, r.Scheme()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update extension: %s", err)
|
||||
}
|
||||
|
||||
logger.V(4).Info("extension successfully updated", "operation", op, "name", extension.Name)
|
||||
return extension, nil
|
||||
}
|
||||
|
||||
func (r *RepositoryReconciler) createOrUpdateExtensionVersion(ctx context.Context, extension *corev1alpha1.Extension, extensionVersion *corev1alpha1.ExtensionVersion) error {
|
||||
logger := klog.FromContext(ctx)
|
||||
version := &corev1alpha1.ExtensionVersion{ObjectMeta: metav1.ObjectMeta{Name: extensionVersion.Name}}
|
||||
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, version, func() error {
|
||||
if version.Labels == nil {
|
||||
version.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range extensionVersion.Labels {
|
||||
version.Labels[k] = v
|
||||
}
|
||||
version.Spec = extensionVersion.Spec
|
||||
if err := controllerutil.SetOwnerReference(extension, version, r.Scheme()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update extension version: %s", err)
|
||||
}
|
||||
|
||||
logger.V(4).Info("extension version successfully updated", "operation", op, "name", extensionVersion.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RepositoryReconciler) syncExtensionsFromURL(ctx context.Context, repo *corev1alpha1.Repository, repoURL string) error {
|
||||
logger := klog.FromContext(ctx)
|
||||
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
cred := helm.RepoCredential{}
|
||||
if repo.Spec.BasicAuth != nil {
|
||||
cred.Username = repo.Spec.BasicAuth.Username
|
||||
cred.Password = repo.Spec.BasicAuth.Password
|
||||
}
|
||||
index, err := helm.LoadRepoIndex(ctx, repoURL, cred)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for extensionName, versions := range index.Entries {
|
||||
extensionVersions := make([]corev1alpha1.ExtensionVersion, 0, len(versions))
|
||||
for _, version := range versions {
|
||||
if version.Metadata == nil {
|
||||
logger.Info("version metadata is empty", "repo", repo.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if version.Name != extensionName {
|
||||
logger.Info("invalid extension version found", "want", extensionName, "got", version.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
var chartURL string
|
||||
if len(version.URLs) > 0 {
|
||||
versionURL := version.URLs[0]
|
||||
u, err := url.Parse(versionURL)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to parse chart URL", "url", versionURL)
|
||||
continue
|
||||
}
|
||||
if u.Host == "" {
|
||||
chartURL = fmt.Sprintf("%s/%s", repoURL, versionURL)
|
||||
} else {
|
||||
chartURL = u.String()
|
||||
}
|
||||
}
|
||||
|
||||
extensionVersionSpec, err := r.loadExtensionVersionSpecFrom(ctx, chartURL, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load extension version spec: %s", err)
|
||||
}
|
||||
|
||||
if extensionVersionSpec == nil {
|
||||
logger.V(4).Info("extension version spec not found: %s", chartURL)
|
||||
continue
|
||||
}
|
||||
extensionVersionSpec.Created = metav1.NewTime(version.Created)
|
||||
extensionVersionSpec.Digest = version.Digest
|
||||
extensionVersionSpec.Repository = repo.Name
|
||||
extensionVersionSpec.ChartDataRef = nil
|
||||
extensionVersionSpec.ChartURL = chartURL
|
||||
|
||||
extensionVersion := corev1alpha1.ExtensionVersion{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s-%s", extensionName, extensionVersionSpec.Version),
|
||||
Labels: map[string]string{
|
||||
corev1alpha1.RepositoryReferenceLabel: repo.Name,
|
||||
corev1alpha1.ExtensionReferenceLabel: extensionName,
|
||||
},
|
||||
Annotations: version.Metadata.Annotations,
|
||||
},
|
||||
Spec: *extensionVersionSpec,
|
||||
}
|
||||
if extensionVersionSpec.Category != "" {
|
||||
extensionVersion.Labels[corev1alpha1.CategoryLabel] = extensionVersionSpec.Category
|
||||
}
|
||||
extensionVersions = append(extensionVersions, extensionVersion)
|
||||
}
|
||||
|
||||
latestExtensionVersion := getLatestExtensionVersion(extensionVersions)
|
||||
if latestExtensionVersion == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
extension, err := r.createOrUpdateExtension(ctx, repo, extensionName, latestExtensionVersion)
|
||||
if err != nil {
|
||||
if errors.Is(err, extensionRepoConflict) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to create or update extension: %s", err)
|
||||
}
|
||||
|
||||
for _, extensionVersion := range extensionVersions {
|
||||
if err := r.createOrUpdateExtensionVersion(ctx, extension, &extensionVersion); err != nil {
|
||||
return fmt.Errorf("failed to create or update extension version: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.removeSuspendedExtensionVersion(ctx, repo, extension, extensionVersions); err != nil {
|
||||
return fmt.Errorf("failed to remove suspended extension version: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
extensions := &corev1alpha1.ExtensionList{}
|
||||
if err := r.List(ctx, extensions, client.MatchingLabels{corev1alpha1.RepositoryReferenceLabel: repo.Name}); err != nil {
|
||||
return fmt.Errorf("failed to list extensions: %s", err)
|
||||
}
|
||||
|
||||
for _, extension := range extensions.Items {
|
||||
if _, ok := index.Entries[extension.Name]; !ok {
|
||||
if err := r.removeSuspendedExtensionVersion(ctx, repo, &extension, []corev1alpha1.ExtensionVersion{}); err != nil {
|
||||
return fmt.Errorf("failed to remove suspended extension version: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RepositoryReconciler) reconcileRepository(ctx context.Context, repo *corev1alpha1.Repository) (ctrl.Result, error) {
|
||||
registryPollInterval := minimumRegistryPollInterval
|
||||
if repo.Spec.UpdateStrategy != nil && repo.Spec.UpdateStrategy.Interval.Duration > minimumRegistryPollInterval {
|
||||
registryPollInterval = repo.Spec.UpdateStrategy.Interval.Duration
|
||||
}
|
||||
|
||||
var repoURL string
|
||||
// URL and Image are immutable after creation
|
||||
if repo.Spec.URL != "" {
|
||||
repoURL = repo.Spec.URL
|
||||
} else if repo.Spec.Image != "" {
|
||||
var deployment appsv1.Deployment
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: constants.KubeSphereNamespace, Name: fmt.Sprintf(generateNameFormat, repo.Name)}, &deployment); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
if err := r.deployRepository(ctx, repo); err != nil {
|
||||
r.recorder.Event(repo, corev1.EventTypeWarning, "RepositoryDeployFailed", err.Error())
|
||||
return ctrl.Result{}, fmt.Errorf("failed to deploy repository: %s", err)
|
||||
}
|
||||
r.recorder.Event(repo, corev1.EventTypeNormal, "RepositoryDeployed", "")
|
||||
return ctrl.Result{Requeue: true, RequeueAfter: defaultRequeueInterval}, nil
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("failed to fetch deployment: %s", err)
|
||||
}
|
||||
|
||||
restartAt, _ := time.Parse(time.RFC3339, deployment.Spec.Template.Annotations["kubesphere.io/restartedAt"])
|
||||
if restartAt.IsZero() {
|
||||
restartAt = deployment.ObjectMeta.CreationTimestamp.Time
|
||||
}
|
||||
// restart and pull the latest docker image
|
||||
if time.Now().After(repo.Status.LastSyncTime.Add(registryPollInterval)) && time.Now().After(restartAt.Add(registryPollInterval)) {
|
||||
rawData := []byte(fmt.Sprintf("{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"kubesphere.io/restartedAt\":\"%s\"}}}}}", time.Now().Format(time.RFC3339)))
|
||||
if err := r.Patch(ctx, &deployment, client.RawPatch(types.StrategicMergePatchType, rawData)); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
r.recorder.Event(repo, corev1.EventTypeNormal, "RepositoryRestarted", "")
|
||||
return ctrl.Result{Requeue: true, RequeueAfter: defaultRequeueInterval}, nil
|
||||
}
|
||||
|
||||
if deployment.Status.AvailableReplicas != deployment.Status.Replicas {
|
||||
return ctrl.Result{Requeue: true, RequeueAfter: defaultRequeueInterval}, nil
|
||||
}
|
||||
|
||||
// ready to sync
|
||||
repoURL = fmt.Sprintf("http://%s.%s.svc", deployment.Name, constants.KubeSphereNamespace)
|
||||
}
|
||||
|
||||
outOfSync := repo.Status.LastSyncTime == nil || time.Now().After(repo.Status.LastSyncTime.Add(registryPollInterval))
|
||||
if repoURL != "" && outOfSync {
|
||||
if err := r.syncExtensionsFromURL(ctx, repo, repoURL); err != nil {
|
||||
r.recorder.Eventf(repo, corev1.EventTypeWarning, kscontroller.SyncFailed, "failed to sync extensions from %s: %s", repoURL, err)
|
||||
return ctrl.Result{}, fmt.Errorf("failed to sync extensions: %s", err)
|
||||
}
|
||||
r.recorder.Eventf(repo, corev1.EventTypeNormal, kscontroller.Synced, "sync extensions from %s successfully", repoURL)
|
||||
repo = repo.DeepCopy()
|
||||
repo.Status.LastSyncTime = &metav1.Time{Time: time.Now()}
|
||||
if err := r.Update(ctx, repo); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("failed to update repository: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ctrl.Result{Requeue: true, RequeueAfter: registryPollInterval}, nil
|
||||
}
|
||||
|
||||
func (r *RepositoryReconciler) deployRepository(ctx context.Context, repo *corev1alpha1.Repository) error {
|
||||
generateName := fmt.Sprintf(generateNameFormat, repo.Name)
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: generateName,
|
||||
Namespace: constants.KubeSphereNamespace,
|
||||
Labels: map[string]string{corev1alpha1.RepositoryReferenceLabel: repo.Name},
|
||||
},
|
||||
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{corev1alpha1.RepositoryReferenceLabel: repo.Name},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{corev1alpha1.RepositoryReferenceLabel: repo.Name},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "repository",
|
||||
Image: repo.Spec.Image,
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: "CHART_URL",
|
||||
Value: fmt.Sprintf("http://%s.%s.svc", generateName, constants.KubeSphereNamespace),
|
||||
},
|
||||
},
|
||||
LivenessProbe: &corev1.Probe{
|
||||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/health",
|
||||
Port: intstr.FromInt32(8080),
|
||||
},
|
||||
},
|
||||
PeriodSeconds: 10,
|
||||
InitialDelaySeconds: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := controllerutil.SetOwnerReference(repo, deployment, r.Scheme()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.Create(ctx, deployment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: generateName,
|
||||
Namespace: constants.KubeSphereNamespace,
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Ports: []corev1.ServicePort{
|
||||
{
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
TargetPort: intstr.FromInt32(8080),
|
||||
},
|
||||
},
|
||||
Selector: map[string]string{
|
||||
corev1alpha1.RepositoryReferenceLabel: repo.Name,
|
||||
},
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
}
|
||||
|
||||
if err := controllerutil.SetOwnerReference(repo, service, r.Scheme()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, service); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RepositoryReconciler) loadExtensionVersionSpecFrom(ctx context.Context, chartURL string, repo *corev1alpha1.Repository) (*corev1alpha1.ExtensionVersionSpec, error) {
|
||||
logger := klog.FromContext(ctx)
|
||||
var result *corev1alpha1.ExtensionVersionSpec
|
||||
|
||||
err := retry.OnError(retry.DefaultRetry, func(err error) bool {
|
||||
return true
|
||||
}, func() error {
|
||||
req, err := http.NewRequest(http.MethodGet, chartURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if repo.Spec.BasicAuth != nil {
|
||||
req.SetBasicAuth(repo.Spec.BasicAuth.Username, repo.Spec.BasicAuth.Password)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf(string(data))
|
||||
}
|
||||
|
||||
files, err := loader.LoadArchiveFiles(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.Name == extensionFileName {
|
||||
extensionVersionSpec := &corev1alpha1.ExtensionVersionSpec{}
|
||||
if err := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(file.Data), 1024).Decode(extensionVersionSpec); err != nil {
|
||||
logger.V(4).Info("invalid extension version spec: %s", string(file.Data))
|
||||
return nil
|
||||
}
|
||||
result = extensionVersionSpec
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
logger.V(6).Info("extension.yaml not found", "chart", chartURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(result.Icon, "http://") ||
|
||||
strings.HasPrefix(result.Icon, "https://") ||
|
||||
strings.HasPrefix(result.Icon, "data:image") {
|
||||
return nil
|
||||
}
|
||||
|
||||
absPath := strings.TrimPrefix(result.Icon, "./")
|
||||
var iconData []byte
|
||||
for _, file := range files {
|
||||
if file.Name == absPath {
|
||||
iconData = file.Data
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if iconData == nil {
|
||||
logger.V(4).Info("invalid extension icon path: %s", absPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
mimeType := mime.TypeByExtension(path.Ext(result.Icon))
|
||||
if mimeType == "" {
|
||||
mimeType = http.DetectContentType(iconData)
|
||||
}
|
||||
|
||||
base64EncodedData := base64.StdEncoding.EncodeToString(iconData)
|
||||
result.Icon = fmt.Sprintf("data:%s;base64,%s", mimeType, base64EncodedData)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch chart data from %s: %s", chartURL, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RepositoryReconciler) removeSuspendedExtensionVersion(ctx context.Context, repo *corev1alpha1.Repository, extension *corev1alpha1.Extension, versions []corev1alpha1.ExtensionVersion) error {
|
||||
extensionVersions := &corev1alpha1.ExtensionVersionList{}
|
||||
if err := r.List(ctx, extensionVersions, client.MatchingLabels{corev1alpha1.ExtensionReferenceLabel: extension.Name, corev1alpha1.RepositoryReferenceLabel: repo.Name}); err != nil {
|
||||
return fmt.Errorf("failed to list extension versions: %s", err)
|
||||
}
|
||||
for _, version := range extensionVersions.Items {
|
||||
if checkIfSuspended(versions, version) {
|
||||
r.logger.V(4).Info("delete suspended extension version", "name", version.Name, "version", version.Spec.Version)
|
||||
if err := r.Delete(ctx, &version); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to delete extension version: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIfSuspended(versions []corev1alpha1.ExtensionVersion, version corev1alpha1.ExtensionVersion) bool {
|
||||
for _, v := range versions {
|
||||
if v.Name == version.Name && v.Spec.Version == version.Spec.Version {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
266
pkg/controller/core/util.go
Normal file
266
pkg/controller/core/util.go
Normal file
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
goerrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
yaml3 "gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/storage/driver"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/klog/v2"
|
||||
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
|
||||
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/utils/hashutil"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/version"
|
||||
)
|
||||
|
||||
func getRecommendedExtensionVersion(versions []corev1alpha1.ExtensionVersion, k8sVersion *semver.Version) (string, error) {
|
||||
if len(versions) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
ksVersion, err := semver.NewVersion(version.Get().GitVersion)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse KubeSphere version failed: %v", err)
|
||||
}
|
||||
|
||||
var matchedVersions []*semver.Version
|
||||
|
||||
for _, v := range versions {
|
||||
var kubeVersionMatched, ksVersionMatched bool
|
||||
|
||||
if v.Spec.KubeVersion == "" {
|
||||
kubeVersionMatched = true
|
||||
} else {
|
||||
targetKubeVersion, err := semver.NewConstraint(v.Spec.KubeVersion)
|
||||
if err != nil {
|
||||
// If the semver is invalid, just ignore it.
|
||||
klog.Warningf("failed to parse Kubernetes version constraints: kubeVersion: %s, err: %s", v.Spec.KubeVersion, err)
|
||||
continue
|
||||
}
|
||||
kubeVersionMatched = targetKubeVersion.Check(k8sVersion)
|
||||
}
|
||||
|
||||
if v.Spec.KSVersion == "" {
|
||||
ksVersionMatched = true
|
||||
} else {
|
||||
targetKSVersion, err := semver.NewConstraint(v.Spec.KSVersion)
|
||||
if err != nil {
|
||||
klog.Warningf("failed to parse KubeSphere version constraints: ksVersion: %s, err: %s", v.Spec.KSVersion, err)
|
||||
continue
|
||||
}
|
||||
ksVersionMatched = targetKSVersion.Check(ksVersion)
|
||||
}
|
||||
|
||||
if kubeVersionMatched && ksVersionMatched {
|
||||
targetVersion, err := semver.NewVersion(v.Spec.Version)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("parse version failed, extension version: %s, err: %s", v.Spec.Version, err)
|
||||
continue
|
||||
}
|
||||
matchedVersions = append(matchedVersions, targetVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchedVersions) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
sort.Slice(matchedVersions, func(i, j int) bool {
|
||||
return matchedVersions[i].Compare(matchedVersions[j]) >= 0
|
||||
})
|
||||
|
||||
return matchedVersions[0].Original(), nil
|
||||
}
|
||||
|
||||
func getLatestExtensionVersion(versions []corev1alpha1.ExtensionVersion) *corev1alpha1.ExtensionVersion {
|
||||
if len(versions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var latestVersion *corev1alpha1.ExtensionVersion
|
||||
var latestSemver *semver.Version
|
||||
|
||||
for i := range versions {
|
||||
currSemver, err := semver.NewVersion(versions[i].Spec.Version)
|
||||
if err == nil {
|
||||
if latestSemver == nil {
|
||||
// the first valid semver
|
||||
latestSemver = currSemver
|
||||
latestVersion = &versions[i]
|
||||
} else if latestSemver.LessThan(currSemver) {
|
||||
// find a newer valid semver
|
||||
latestSemver = currSemver
|
||||
latestVersion = &versions[i]
|
||||
}
|
||||
} else {
|
||||
// If the semver is invalid, just ignore it.
|
||||
klog.Warningf("parse version failed, extension version: %s, err: %s", versions[i].Name, err)
|
||||
}
|
||||
}
|
||||
return latestVersion
|
||||
}
|
||||
|
||||
func isReleaseNotFoundError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(err.Error(), driver.ErrReleaseNotFound.Error())
|
||||
}
|
||||
|
||||
func clusterConfig(sub *corev1alpha1.InstallPlan, clusterName string) []byte {
|
||||
if clusterName == "" {
|
||||
return []byte(sub.Spec.Config)
|
||||
}
|
||||
for cluster, config := range sub.Spec.ClusterScheduling.Overrides {
|
||||
if cluster == clusterName {
|
||||
return merge(sub.Spec.Config, config)
|
||||
}
|
||||
}
|
||||
return []byte(sub.Spec.Config)
|
||||
}
|
||||
|
||||
func merge(config string, override string) []byte {
|
||||
config = strings.TrimSpace(config)
|
||||
override = strings.TrimSpace(override)
|
||||
|
||||
if config == "" && override == "" {
|
||||
return []byte("")
|
||||
}
|
||||
|
||||
if override == "" {
|
||||
return []byte(config)
|
||||
}
|
||||
|
||||
if config == "" {
|
||||
return []byte(override)
|
||||
}
|
||||
|
||||
baseConf := map[string]interface{}{}
|
||||
if err := yaml3.Unmarshal([]byte(config), &baseConf); err != nil {
|
||||
klog.Warningf("failed to unmarshal config: %v", err)
|
||||
}
|
||||
|
||||
overrideConf := map[string]interface{}{}
|
||||
if err := yaml3.Unmarshal([]byte(override), overrideConf); err != nil {
|
||||
klog.Warningf("failed to unmarshal config: %v", err)
|
||||
}
|
||||
|
||||
finalConf := mergeValues(baseConf, overrideConf)
|
||||
data, _ := yaml3.Marshal(finalConf)
|
||||
return data
|
||||
}
|
||||
|
||||
// mergeValues will merge source and destination map, preferring values from the source map
|
||||
func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
|
||||
for k, v := range src {
|
||||
// If the key doesn't exist already, then just set the key to that value
|
||||
if _, exists := dest[k]; !exists {
|
||||
dest[k] = v
|
||||
continue
|
||||
}
|
||||
nextMap, ok := v.(map[string]interface{})
|
||||
// If it isn't another map, overwrite the value
|
||||
if !ok {
|
||||
dest[k] = v
|
||||
continue
|
||||
}
|
||||
// Edge case: If the key exists in the destination, but isn't a map
|
||||
destMap, isMap := dest[k].(map[string]interface{})
|
||||
// If the source map has a map for this key, prefer it
|
||||
if !isMap {
|
||||
dest[k] = v
|
||||
continue
|
||||
}
|
||||
// If we got to this point, it is a map in both, so merge them
|
||||
dest[k] = mergeValues(destMap, nextMap)
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
func usesPermissions(mainChart *chart.Chart) (rbacv1.ClusterRole, rbacv1.Role) {
|
||||
var clusterRole rbacv1.ClusterRole
|
||||
var role rbacv1.Role
|
||||
for _, file := range mainChart.Files {
|
||||
if file.Name == permissionDefinitionFile {
|
||||
// decoder := yaml.NewDecoder(bytes.NewReader(file.Data))
|
||||
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(file.Data), 1024)
|
||||
for {
|
||||
result := new(rbacv1.Role)
|
||||
// create new spec here
|
||||
// pass a reference to spec reference
|
||||
err := decoder.Decode(&result)
|
||||
// check it was parsed
|
||||
if result == nil {
|
||||
continue
|
||||
}
|
||||
// break the loop in case of EOF
|
||||
if goerrors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return clusterRole, role
|
||||
}
|
||||
if result.Kind == "ClusterRole" {
|
||||
clusterRole.Rules = append(clusterRole.Rules, result.Rules...)
|
||||
}
|
||||
if result.Kind == "Role" {
|
||||
role.Rules = append(role.Rules, result.Rules...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return clusterRole, role
|
||||
}
|
||||
|
||||
func hasCluster(clusters []clusterv1alpha1.Cluster, clusterName string) bool {
|
||||
for _, cluster := range clusters {
|
||||
if cluster.Name == clusterName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func versionChanged(plan *corev1alpha1.InstallPlan, cluster string) bool {
|
||||
var oldVersion string
|
||||
if cluster == "" {
|
||||
oldVersion = plan.Status.Version
|
||||
} else if plan.Status.ClusterSchedulingStatuses != nil {
|
||||
oldVersion = plan.Status.ClusterSchedulingStatuses[cluster].Version
|
||||
}
|
||||
newVersion := plan.Spec.Extension.Version
|
||||
if oldVersion == "" {
|
||||
return false
|
||||
}
|
||||
return newVersion != oldVersion
|
||||
}
|
||||
|
||||
func configChanged(sub *corev1alpha1.InstallPlan, cluster string) bool {
|
||||
var oldConfigHash string
|
||||
if cluster == "" {
|
||||
oldConfigHash = sub.Status.InstallationStatus.ConfigHash
|
||||
} else {
|
||||
oldConfigHash = sub.Status.ClusterSchedulingStatuses[cluster].ConfigHash
|
||||
}
|
||||
newConfigHash := hashutil.FNVString(clusterConfig(sub, cluster))
|
||||
if oldConfigHash == "" {
|
||||
return true
|
||||
}
|
||||
return newConfigHash != oldConfigHash
|
||||
}
|
||||
117
pkg/controller/core/util_test.go
Normal file
117
pkg/controller/core/util_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
|
||||
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/version"
|
||||
)
|
||||
|
||||
func TestGetRecommendedExtensionVersion(t *testing.T) {
|
||||
k8sVersion120, _ := semver.NewVersion("1.20.0")
|
||||
k8sVersion125, _ := semver.NewVersion("1.25.4")
|
||||
tests := []struct {
|
||||
name string
|
||||
versions []corev1alpha1.ExtensionVersion
|
||||
k8sVersion *semver.Version
|
||||
ksVersion string
|
||||
wanted string
|
||||
}{
|
||||
{
|
||||
name: "normal test",
|
||||
versions: []corev1alpha1.ExtensionVersion{
|
||||
{
|
||||
Spec: corev1alpha1.ExtensionVersionSpec{ // match
|
||||
Version: "1.0.0",
|
||||
KubeVersion: ">=1.19.0",
|
||||
KSVersion: ">=4.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1alpha1.ExtensionVersionSpec{ // match
|
||||
Version: "1.1.0",
|
||||
KubeVersion: ">=1.20.0",
|
||||
KSVersion: ">=4.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1alpha1.ExtensionVersionSpec{ // KubeVersion not match
|
||||
Version: "1.2.0",
|
||||
KubeVersion: ">=1.21.0",
|
||||
KSVersion: ">=4.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1alpha1.ExtensionVersionSpec{ // KSVersion not match
|
||||
Version: "1.3.0",
|
||||
KubeVersion: ">=1.20.0",
|
||||
KSVersion: ">=4.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
k8sVersion: k8sVersion120,
|
||||
ksVersion: "4.0.0",
|
||||
wanted: "1.1.0",
|
||||
},
|
||||
{
|
||||
name: "no matches test",
|
||||
versions: []corev1alpha1.ExtensionVersion{
|
||||
{
|
||||
Spec: corev1alpha1.ExtensionVersionSpec{ // KubeVersion not match
|
||||
Version: "1.2.0",
|
||||
KubeVersion: ">=1.21.0",
|
||||
KSVersion: ">=4.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1alpha1.ExtensionVersionSpec{ // KSVersion not match
|
||||
Version: "1.3.0",
|
||||
KubeVersion: ">=1.20.0",
|
||||
KSVersion: ">=4.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
k8sVersion: k8sVersion120,
|
||||
ksVersion: "4.0.0",
|
||||
wanted: "",
|
||||
},
|
||||
{
|
||||
name: "match 1.3.0",
|
||||
versions: []corev1alpha1.ExtensionVersion{
|
||||
{
|
||||
Spec: corev1alpha1.ExtensionVersionSpec{
|
||||
Version: "1.2.0",
|
||||
KubeVersion: ">=1.19.0",
|
||||
KSVersion: ">=3.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1alpha1.ExtensionVersionSpec{
|
||||
Version: "1.3.0",
|
||||
KubeVersion: ">=1.19.0",
|
||||
KSVersion: ">=4.0.0-alpha",
|
||||
},
|
||||
},
|
||||
},
|
||||
k8sVersion: k8sVersion125,
|
||||
ksVersion: "4.0.0-beta.5+ae34",
|
||||
wanted: "1.3.0",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
version.SetGitVersion(tt.ksVersion)
|
||||
if got, _ := getRecommendedExtensionVersion(tt.versions, tt.k8sVersion); got != tt.wanted {
|
||||
t.Errorf("getRecommendedExtensionVersion() = %v, want %v", got, tt.wanted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user