未验证 提交 0a40cfdf 编写于 作者: K KubeSphere CI Bot 提交者: GitHub

Merge pull request #3465 from xyz-li/app-fix

Fix nil pointer and delete helmRelease 
......@@ -228,8 +228,9 @@ func run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{})
klog.Fatal("Unable to create helm category controller")
}
var opS3Client s3.Interface
if !s.OpenPitrixOptions.AppStoreConfIsEmpty() {
storageClient, err := s3.NewS3Client(s.OpenPitrixOptions.S3Options)
opS3Client, err = s3.NewS3Client(s.OpenPitrixOptions.S3Options)
if err != nil {
klog.Fatalf("failed to connect to s3, please check openpitrix s3 service status, error: %v", err)
}
......@@ -242,15 +243,17 @@ func run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{})
if err != nil {
klog.Fatalf("Unable to create helm application version controller, error: %s ", err)
}
}
err = (&helmrelease.ReconcileHelmRelease{
StorageClient: storageClient,
KsFactory: informerFactory.KubeSphereSharedInformerFactory(),
}).SetupWithManager(mgr)
err = (&helmrelease.ReconcileHelmRelease{
// nil interface is valid value.
StorageClient: opS3Client,
KsFactory: informerFactory.KubeSphereSharedInformerFactory(),
MultiClusterEnable: s.MultiClusterOptions.Enable,
}).SetupWithManager(mgr)
if err != nil {
klog.Fatalf("Unable to create helm release controller, error: %s", err)
}
if err != nil {
klog.Fatalf("Unable to create helm release controller, error: %s", err)
}
selector, _ := labels.Parse(s.ApplicationSelector)
......
......@@ -22,7 +22,7 @@ spec:
- jsonPath: .spec.name
name: name
type: string
- jsonPath: .metadata.labels.kubesphere\\.io/workspace
- jsonPath: .metadata.labels.kubesphere\.io/workspace
name: Workspace
type: string
- jsonPath: .spec.url
......
......@@ -92,7 +92,7 @@ type HelmRepoStatus struct {
// +kubebuilder:resource:scope=Cluster,path=helmrepos,shortName=hrepo
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="name",type=string,JSONPath=`.spec.name`
// +kubebuilder:printcolumn:name="Workspace",type=string,JSONPath=`.metadata.labels.kubesphere\\.io/workspace`
// +kubebuilder:printcolumn:name="Workspace",type="string",JSONPath=".metadata.labels.kubesphere\\.io/workspace"
// +kubebuilder:printcolumn:name="url",type=string,JSONPath=`.spec.url`
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
......
......@@ -205,7 +205,7 @@ func (conf *Config) ToMap() map[string]bool {
if conf.OpenPitrixOptions == nil {
result["openpitrix.appstore"] = false
} else {
result["openpitrix.appstore"] = conf.OpenPitrixOptions.AppStoreConfIsEmpty()
result["openpitrix.appstore"] = !conf.OpenPitrixOptions.AppStoreConfIsEmpty()
}
continue
}
......
/*
Copyright 2019 The KubeSphere Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package helmrelease
import (
"context"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/apis/application/v1alpha1"
"kubesphere.io/kubesphere/pkg/simple/client/openpitrix/helmrepoindex"
"path"
"strings"
)
func (r *ReconcileHelmRelease) GetChartData(rls *v1alpha1.HelmRelease) (chartName string, chartData []byte, err error) {
if rls.Spec.RepoId != "" && rls.Spec.RepoId != v1alpha1.AppStoreRepoId {
// load chart data from helm repo
repo := v1alpha1.HelmRepo{}
err := r.Get(context.TODO(), types.NamespacedName{Name: rls.Spec.RepoId}, &repo)
if err != nil {
klog.Errorf("get helm repo %s failed, error: %v", rls.Spec.RepoId, err)
return chartName, chartData, ErrGetRepoFailed
}
index, err := helmrepoindex.ByteArrayToSavedIndex([]byte(repo.Status.Data))
if version := index.GetApplicationVersion(rls.Spec.ApplicationId, rls.Spec.ApplicationVersionId); version != nil {
url := version.Spec.URLs[0]
if !(strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "s3://")) {
url = repo.Spec.Url + "/" + url
}
buf, err := helmrepoindex.LoadChart(context.TODO(), url, &repo.Spec.Credential)
if err != nil {
klog.Infof("load chart failed, error: %s", err)
return chartName, chartData, ErrLoadChartFailed
}
chartData = buf.Bytes()
chartName = version.Name
} else {
klog.Errorf("get app version: %s failed", rls.Spec.ApplicationVersionId)
return chartName, chartData, ErrGetAppVersionFailed
}
} else {
// load chart data from helm application version
appVersion := &v1alpha1.HelmApplicationVersion{}
err = r.Get(context.TODO(), types.NamespacedName{Name: rls.Spec.ApplicationVersionId}, appVersion)
if err != nil {
klog.Errorf("get app version %s failed, error: %v", rls.Spec.ApplicationVersionId, err)
return chartName, chartData, ErrGetAppVersionFailed
}
if r.StorageClient == nil {
return "", nil, ErrS3Config
}
chartData, err = r.StorageClient.Read(path.Join(appVersion.GetWorkspace(), appVersion.Name))
if err != nil {
klog.Errorf("load chart from storage failed, error: %s", err)
return chartName, chartData, ErrLoadChartFromStorageFailed
}
chartName = appVersion.GetTrueName()
}
return
}
......@@ -19,37 +19,29 @@ package helmrelease
import (
"context"
"errors"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/record"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/apis/application/v1alpha1"
clusterv1alpha1 "kubesphere.io/kubesphere/pkg/apis/cluster/v1alpha1"
"kubesphere.io/kubesphere/pkg/client/informers/externalversions"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/simple/client/openpitrix/helmrepoindex"
"kubesphere.io/kubesphere/pkg/simple/client/openpitrix/helmwrapper"
"kubesphere.io/kubesphere/pkg/simple/client/s3"
"kubesphere.io/kubesphere/pkg/utils/clusterclient"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
"kubesphere.io/kubesphere/pkg/utils/stringutils"
"math"
"path"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"strings"
"time"
)
const (
HelmReleaseFinalizer = "helmrelease.application.kubesphere.io"
IndexerName = "clusterNamespace"
)
var (
......@@ -58,6 +50,7 @@ var (
ErrAppVersionDataIsEmpty = errors.New("app version data is empty")
ErrGetAppVersionFailed = errors.New("get app version failed")
ErrLoadChartFailed = errors.New("load chart failed")
ErrS3Config = errors.New("invalid s3 config")
ErrLoadChartFromStorageFailed = errors.New("load chart from storage failed")
)
......@@ -65,14 +58,16 @@ var _ reconcile.Reconciler = &ReconcileHelmRelease{}
// ReconcileWorkspace reconciles a Workspace object
type ReconcileHelmRelease struct {
StorageClient s3.Interface
KsFactory externalversions.SharedInformerFactory
clusterClients clusterclient.ClusterClients
StorageClient s3.Interface
KsFactory externalversions.SharedInformerFactory
client.Client
recorder record.EventRecorder
// mock helm install && uninstall
helmMock bool
informer cache.SharedIndexInformer
clusterClients clusterclient.ClusterClients
MultiClusterEnable bool
}
//
......@@ -111,8 +106,29 @@ func (r *ReconcileHelmRelease) Reconcile(request reconcile.Request) (reconcile.R
// The object is not being deleted, so if it does not have our finalizer,
// then lets add the finalizer and update the object.
if !sliceutil.HasString(instance.ObjectMeta.Finalizers, HelmReleaseFinalizer) {
clusterName := instance.GetRlsCluster()
if r.MultiClusterEnable && clusterName != "" {
clusterInfo, err := r.clusterClients.Get(clusterName)
if err != nil {
// cluster not exists, delete the crd
klog.Warningf("cluster %s not found, delete the helm release %s/%s",
clusterName, instance.GetRlsNamespace(), instance.GetTrueName())
return reconcile.Result{}, r.Delete(context.TODO(), instance)
}
// Host cluster will self-healing, delete host cluster won't cause deletion of helm release
if !r.clusterClients.IsHostCluster(clusterInfo) {
// add owner References
instance.OwnerReferences = append(instance.OwnerReferences, metav1.OwnerReference{
APIVersion: clusterv1alpha1.SchemeGroupVersion.String(),
Kind: clusterv1alpha1.ResourceKindCluster,
Name: clusterInfo.Name,
UID: clusterInfo.UID,
})
}
}
instance.ObjectMeta.Finalizers = append(instance.ObjectMeta.Finalizers, HelmReleaseFinalizer)
// add owner References
if err := r.Update(context.Background(), instance); err != nil {
return reconcile.Result{}, err
}
......@@ -146,67 +162,17 @@ func (r *ReconcileHelmRelease) Reconcile(request reconcile.Request) (reconcile.R
return r.reconcile(instance)
}
func (r *ReconcileHelmRelease) GetChartData(rls *v1alpha1.HelmRelease) (chartName string, chartData []byte, err error) {
if rls.Spec.RepoId != "" && rls.Spec.RepoId != v1alpha1.AppStoreRepoId {
// load chart data from helm repo
repo := v1alpha1.HelmRepo{}
err := r.Get(context.TODO(), types.NamespacedName{Name: rls.Spec.RepoId}, &repo)
if err != nil {
klog.Errorf("get helm repo %s failed, error: %v", rls.Spec.RepoId, err)
return chartName, chartData, ErrGetRepoFailed
}
index, err := helmrepoindex.ByteArrayToSavedIndex([]byte(repo.Status.Data))
if version := index.GetApplicationVersion(rls.Spec.ApplicationId, rls.Spec.ApplicationVersionId); version != nil {
url := version.Spec.URLs[0]
if !(strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "s3://")) {
url = repo.Spec.Url + "/" + url
}
buf, err := helmrepoindex.LoadChart(context.TODO(), url, &repo.Spec.Credential)
if err != nil {
klog.Infof("load chart failed, error: %s", err)
return chartName, chartData, ErrLoadChartFailed
}
chartData = buf.Bytes()
chartName = version.Name
} else {
klog.Errorf("get app version: %s failed", rls.Spec.ApplicationVersionId)
return chartName, chartData, ErrGetAppVersionFailed
}
} else {
// load chart data from helm application version
appVersion := &v1alpha1.HelmApplicationVersion{}
err = r.Get(context.TODO(), types.NamespacedName{Name: rls.Spec.ApplicationVersionId}, appVersion)
if err != nil {
klog.Errorf("get app version %s failed, error: %v", rls.Spec.ApplicationVersionId, err)
return chartName, chartData, ErrGetAppVersionFailed
}
chartData, err = r.StorageClient.Read(path.Join(appVersion.GetWorkspace(), appVersion.Name))
if err != nil {
klog.Errorf("load chart from storage failed, error: %s", err)
return chartName, chartData, ErrLoadChartFromStorageFailed
}
chartName = appVersion.GetTrueName()
}
return
}
// Check the state of the instance then decide what to do.
func (r *ReconcileHelmRelease) reconcile(instance *v1alpha1.HelmRelease) (reconcile.Result, error) {
if instance.Status.State == v1alpha1.HelmStatusActive && instance.Status.Version == instance.Spec.Version {
// check release status
return reconcile.Result{
// recheck release status after 10 minutes
RequeueAfter: 10 * time.Minute,
}, nil
// todo check release status
return reconcile.Result{}, nil
}
ft := failedTimes(instance.Status.DeployStatus)
if v1alpha1.HelmStatusFailed == instance.Status.State && ft > 0 {
// exponential backoff, max delay 180s
// failed too much times, exponential backoff, max delay 180s
retryAfter := time.Duration(math.Min(math.Exp2(float64(ft)), 180)) * time.Second
var lastDeploy time.Time
......@@ -226,16 +192,18 @@ func (r *ReconcileHelmRelease) reconcile(instance *v1alpha1.HelmRelease) (reconc
// no operation
return reconcile.Result{}, nil
case v1alpha1.HelmStatusActive:
// Release used to be active, but instance.Status.Version not equal to instance.Spec.Version
instance.Status.State = v1alpha1.HelmStatusUpgrading
// Update the state first.
err = r.Status().Update(context.TODO(), instance)
return reconcile.Result{}, err
case v1alpha1.HelmStatusCreating:
// create new release
err = r.createOrUpgradeHelmRelease(instance, false)
case v1alpha1.HelmStatusFailed:
// check failed times
err = r.createOrUpgradeHelmRelease(instance, false)
case v1alpha1.HelmStatusUpgrading:
// We can update the release now.
err = r.createOrUpgradeHelmRelease(instance, true)
case v1alpha1.HelmStatusRollbacking:
// TODO: rollback helm release
......@@ -260,6 +228,7 @@ func (r *ReconcileHelmRelease) reconcile(instance *v1alpha1.HelmRelease) (reconc
instance.Status.LastDeployed = &now
if len(instance.Status.DeployStatus) > 0 {
instance.Status.DeployStatus = append([]v1alpha1.HelmReleaseDeployStatus{deployStatus}, instance.Status.DeployStatus...)
// At most ten records will be saved.
if len(instance.Status.DeployStatus) >= 10 {
instance.Status.DeployStatus = instance.Status.DeployStatus[:10:10]
}
......@@ -301,7 +270,7 @@ func (r *ReconcileHelmRelease) createOrUpgradeHelmRelease(rls *v1alpha1.HelmRele
clusterName := rls.GetRlsCluster()
var clusterConfig string
if clusterName != "" && r.KsFactory != nil {
if r.MultiClusterEnable && clusterName != "" {
clusterConfig, err = r.clusterClients.GetClusterKubeconfig(clusterName)
if err != nil {
klog.Errorf("get cluster %s config failed", clusterConfig)
......@@ -327,6 +296,7 @@ func (r *ReconcileHelmRelease) createOrUpgradeHelmRelease(rls *v1alpha1.HelmRele
}
func (r *ReconcileHelmRelease) uninstallHelmRelease(rls *v1alpha1.HelmRelease) error {
if rls.Status.State != v1alpha1.HelmStatusDeleting {
rls.Status.State = v1alpha1.HelmStatusDeleting
rls.Status.LastUpdate = metav1.Now()
......@@ -339,12 +309,20 @@ func (r *ReconcileHelmRelease) uninstallHelmRelease(rls *v1alpha1.HelmRelease) e
clusterName := rls.GetRlsCluster()
var clusterConfig string
var err error
if clusterName != "" && r.KsFactory != nil {
clusterConfig, err = r.clusterClients.GetClusterKubeconfig(clusterName)
if r.MultiClusterEnable && clusterName != "" {
clusterInfo, err := r.clusterClients.Get(clusterName)
if err != nil {
klog.Errorf("get cluster %s config failed", clusterConfig)
return err
klog.V(2).Infof("cluster %s was deleted, skip helm release uninstall", clusterName)
return nil
}
// If user deletes helmRelease first and then delete cluster immediately, this may cause helm resources leak.
if clusterInfo.DeletionTimestamp != nil {
klog.V(2).Infof("cluster %s is deleting, skip helm release uninstall", clusterName)
return nil
}
clusterConfig = string(clusterInfo.Spec.Connection.KubeConfig)
}
hw := helmwrapper.NewHelmWrapper(clusterConfig, rls.GetRlsNamespace(), rls.Spec.Name, helmwrapper.SetMock(r.helmMock))
......@@ -359,118 +337,11 @@ func (r *ReconcileHelmRelease) uninstallHelmRelease(rls *v1alpha1.HelmRelease) e
func (r *ReconcileHelmRelease) SetupWithManager(mgr ctrl.Manager) error {
r.Client = mgr.GetClient()
if r.KsFactory != nil {
if r.KsFactory != nil && r.MultiClusterEnable {
r.clusterClients = clusterclient.NewClusterClient(r.KsFactory.Cluster().V1alpha1().Clusters())
r.informer = r.KsFactory.Application().V1alpha1().HelmReleases().Informer()
err := r.informer.AddIndexers(map[string]cache.IndexFunc{
IndexerName: func(obj interface{}) ([]string, error) {
rls := obj.(*v1alpha1.HelmRelease)
return []string{fmt.Sprintf("%s/%s", rls.GetRlsCluster(), rls.GetRlsNamespace())}, nil
},
})
if err != nil {
return err
}
go func() {
<-mgr.Elected()
go r.cleanHelmReleaseWhenNamespaceDeleted()
}()
}
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.HelmRelease{}).
Complete(r)
}
func (r *ReconcileHelmRelease) getClusterConfig(cluster string) (string, error) {
if cluster == "" {
return "", nil
}
clusterConfig, err := r.clusterClients.GetClusterKubeconfig(cluster)
if err != nil {
klog.Errorf("get cluster %s config failed", clusterConfig)
return "", err
}
return clusterConfig, nil
}
// When namespace have been removed from member cluster, we need clean all
// the helmRelease from the host cluster.
func (r *ReconcileHelmRelease) cleanHelmReleaseWhenNamespaceDeleted() {
ticker := time.NewTicker(2 * time.Minute)
for _ = range ticker.C {
keys := r.informer.GetIndexer().ListIndexFuncValues(IndexerName)
for _, clusterNs := range keys {
klog.V(4).Infof("clean resource in %s", clusterNs)
parts := stringutils.Split(clusterNs, "/")
if len(parts) == 2 {
cluster, ns := parts[0], parts[1]
items, err := r.informer.GetIndexer().ByIndex(IndexerName, clusterNs)
if err != nil {
klog.Errorf("get items from index failed, error: %s", err)
continue
}
kubeconfig, err := r.getClusterConfig(cluster)
if err != nil {
klog.Errorf("get cluster %s config failed, error: %s", cluster, err)
continue
}
// connect to member or host cluster
var restConfig *restclient.Config
if kubeconfig == "" {
restConfig, err = restclient.InClusterConfig()
} else {
cc, err := clientcmd.NewClientConfigFromBytes([]byte(kubeconfig))
if err != nil {
klog.Errorf("get client config for cluster %s failed, error: %s", cluster, err)
continue
}
restConfig, err = cc.ClientConfig()
}
if err != nil {
klog.Errorf("build rest config for cluster %s failed, error: %s", cluster, err)
continue
}
clientSet, err := kubernetes.NewForConfig(restConfig)
if err != nil {
klog.Errorf("create client set failed, error: %s", err)
continue
}
// check namespace exists or not
namespace, err := clientSet.CoreV1().Namespaces().Get(context.TODO(), ns, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
klog.V(2).Infof("delete all helm release in %s", clusterNs)
for ind := range items {
rls := items[ind].(*v1alpha1.HelmRelease)
err := r.Client.Delete(context.TODO(), rls)
if err != nil && !apierrors.IsNotFound(err) {
klog.Errorf("delete release %s failed", rls.Name)
}
}
} else {
klog.Errorf("get namespace %s from cluster %s failed, error: %s", ns, cluster, err)
continue
}
} else {
for ind := range items {
rls := items[ind].(*v1alpha1.HelmRelease)
if namespace.CreationTimestamp.After(rls.CreationTimestamp.Time) {
klog.V(2).Infof("delete helm release %s in %s", rls.Namespace, clusterNs)
// todo, namespace is newer than helmRelease, should we delete the helmRelease
}
}
}
}
}
}
}
......@@ -82,6 +82,9 @@ func (h *openpitrixHandler) CreateRepo(req *restful.Request, resp *restful.Respo
api.HandleBadRequest(resp, nil, err)
return
}
userInfo := parsedUrl.User
// trim credential from url
parsedUrl.User = nil
repo := v1alpha1.HelmRepo{
ObjectMeta: metav1.ObjectMeta{
......@@ -95,16 +98,16 @@ func (h *openpitrixHandler) CreateRepo(req *restful.Request, resp *restful.Respo
},
Spec: v1alpha1.HelmRepoSpec{
Name: createRepoRequest.Name,
Url: createRepoRequest.URL,
Url: parsedUrl.String(),
SyncPeriod: 0,
Description: stringutils.ShortenString(createRepoRequest.Description, 512),
},
}
if strings.HasPrefix(createRepoRequest.URL, "https://") || strings.HasPrefix(createRepoRequest.URL, "http://") {
if parsedUrl.User != nil {
repo.Spec.Credential.Username = parsedUrl.User.Username()
repo.Spec.Credential.Password, _ = parsedUrl.User.Password()
if userInfo != nil {
repo.Spec.Credential.Username = userInfo.Username()
repo.Spec.Credential.Password, _ = userInfo.Password()
}
} else if strings.HasPrefix(createRepoRequest.URL, "s3://") {
cfg := v1alpha1.S3Config{}
......
......@@ -291,6 +291,7 @@ func buildLabelSelector(conditions *params.Conditions) map[string]string {
}
func (c *applicationOperator) ListApps(conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error) {
apps, err := c.listApps(conditions)
if err != nil {
klog.Error(err)
......@@ -306,7 +307,7 @@ func (c *applicationOperator) ListApps(conditions *params.Conditions, orderBy st
items := make([]interface{}, 0, limit)
for i, j := offset, 0; i < len(apps) && j < limit; {
for i, j := offset, 0; i < len(apps) && j < limit; i, j = i+1, j+1 {
versions, err := c.getAppVersionsByAppId(apps[i].GetHelmApplicationId())
if err != nil && !apierrors.IsNotFound(err) {
return nil, err
......@@ -315,8 +316,6 @@ func (c *applicationOperator) ListApps(conditions *params.Conditions, orderBy st
ctg, _ := c.ctgLister.Get(apps[i].GetHelmCategoryId())
items = append(items, convertApp(apps[i], versions, ctg, 0))
i++
j++
}
return &models.PageableResponse{Items: items, TotalCount: len(apps)}, nil
}
......@@ -612,6 +611,9 @@ func (c *applicationOperator) listApps(conditions *params.Conditions) (ret []*v1
return ret, nil
}
} else {
if c.backingStoreClient == nil {
return []*v1alpha1.HelmApplication{}, nil
}
ret, err = c.appLister.List(labels.SelectorFromSet(buildLabelSelector(conditions)))
}
......
......@@ -38,7 +38,6 @@ import (
"kubesphere.io/kubesphere/pkg/models"
"kubesphere.io/kubesphere/pkg/server/params"
"kubesphere.io/kubesphere/pkg/simple/client/openpitrix/helmrepoindex"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
"kubesphere.io/kubesphere/pkg/utils/stringutils"
"math"
"reflect"
......@@ -218,11 +217,7 @@ func (c *applicationOperator) ListAppVersions(conditions *params.Conditions, ord
return nil, err
}
var status []string
if len(conditions.Match[Status]) > 0 {
status = strings.Split(conditions.Match[Status], "|")
}
versions = filterAppVersionByState(versions, status)
versions = filterAppVersions(versions, conditions)
if reverse {
sort.Sort(sort.Reverse(AppVersions(versions)))
} else {
......@@ -231,47 +226,32 @@ func (c *applicationOperator) ListAppVersions(conditions *params.Conditions, ord
items := make([]interface{}, 0, int(math.Min(float64(limit), float64(len(versions)))))
for i, j := offset, 0; i < len(versions) && j < limit; {
for i, j := offset, 0; i < len(versions) && j < limit; i, j = i+1, j+1 {
items = append(items, convertAppVersion(versions[i]))
i++
j++
}
return &models.PageableResponse{Items: items, TotalCount: len(versions)}, nil
}
func (c *applicationOperator) ListAppVersionReviews(conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error) {
var allStatus []string
if status := conditions.Match[Status]; status != "" {
allStatus = strings.Split(status, "|")
}
appVersions, err := c.versionLister.List(labels.Everything())
if err != nil {
klog.Error(err)
return nil, err
}
filtered := make([]*v1alpha1.HelmApplicationVersion, 0, len(appVersions)/2)
for _, version := range appVersions {
if sliceutil.HasString(allStatus, version.Status.State) {
filtered = append(filtered, version)
}
}
filtered := filterAppReviews(appVersions, conditions)
if reverse {
sort.Sort(sort.Reverse(AppVersions(filtered)))
sort.Sort(sort.Reverse(AppVersionReviews(filtered)))
} else {
sort.Sort(AppVersions(filtered))
sort.Sort(AppVersionReviews(filtered))
}
items := make([]interface{}, 0, len(filtered))
for i, j := offset, 0; i < len(filtered) && j < limit; {
for i, j := offset, 0; i < len(filtered) && j < limit; i, j = i+1, j+1 {
review := convertAppVersionReview(filtered[i])
items = append(items, review)
i++
j++
}
return &models.PageableResponse{Items: items, TotalCount: len(filtered)}, nil
......@@ -309,10 +289,8 @@ func (c *applicationOperator) ListAppVersionAudits(conditions *params.Conditions
items := make([]interface{}, 0, limit)
for i, j := offset, 0; i < len(allAudits) && j < limit; {
for i, j := offset, 0; i < len(allAudits) && j < limit; i, j = i+1, j+1 {
items = append(items, allAudits[i])
i++
j++
}
return &models.PageableResponse{Items: items, TotalCount: len(allAudits)}, nil
......
......@@ -39,25 +39,28 @@ func newAttachmentOperator(storeClient s3.Interface) AttachmentInterface {
}
func (c *attachmentOperator) DescribeAttachment(id string) (*Attachment, error) {
if c.backingStoreClient == nil {
return nil, invalidS3Config
}
data, err := c.backingStoreClient.Read(id)
if err != nil {
klog.Errorf("read attachment %s failed, error: %s", id, err)
return nil, downloadFileFailed
}
att := &Attachment{AttachmentID: id}
if err != nil {
return nil, err
} else {
att.AttachmentContent = map[string]strfmt.Base64{
att := &Attachment{AttachmentID: id,
AttachmentContent: map[string]strfmt.Base64{
"raw": data,
}
},
}
return att, nil
}
func (c *attachmentOperator) CreateAttachment(data []byte) (*Attachment, error) {
if c.backingStoreClient == nil {
return nil, invalidS3Config
}
id := idutils.GetUuid36(v1alpha1.HelmAttachmentPrefix)
err := c.backingStoreClient.Upload(id, id, bytes.NewBuffer(data))
......@@ -72,6 +75,9 @@ func (c *attachmentOperator) CreateAttachment(data []byte) (*Attachment, error)
}
func (c *attachmentOperator) DeleteAttachments(ids []string) error {
if c.backingStoreClient == nil {
return invalidS3Config
}
for _, id := range ids {
err := c.backingStoreClient.Delete(id)
if err != nil {
......
......@@ -182,10 +182,8 @@ func (c *categoryOperator) ListCategories(conditions *params.Conditions, orderBy
sort.Sort(HelmCategoryList(ctgs))
items := make([]interface{}, 0, limit)
for i, j := offset, 0; i < len(ctgs) && j < limit; {
for i, j := offset, 0; i < len(ctgs) && j < limit; i, j = i+1, j+1 {
items = append(items, convertCategory(ctgs[i]))
i++
j++
}
return &models.PageableResponse{Items: items, TotalCount: len(ctgs)}, nil
......
......@@ -310,11 +310,9 @@ func (c *releaseOperator) ListApplications(workspace, clusterName, namespace str
result := models.PageableResponse{TotalCount: len(releases)}
result.Items = make([]interface{}, 0, int(math.Min(float64(limit), float64(len(releases)))))
for i, j := offset, 0; i < len(releases) && j < limit; {
for i, j := offset, 0; i < len(releases) && j < limit; i, j = i+1, j+1 {
app := convertApplication(releases[i], nil)
result.Items = append(result.Items, app)
i++
j++
}
return &result, nil
......@@ -330,13 +328,25 @@ func (c *releaseOperator) DescribeApplication(workspace, clusterName, namespace,
}
app := &Application{}
if rls != nil && rls.GetRlsCluster() != "" {
var clusterConfig string
if rls != nil {
// TODO check clusterName, workspace, namespace
clusterConfig, err := c.clusterClients.GetClusterKubeconfig(rls.GetRlsCluster())
if err != nil {
klog.Errorf("get cluster config failed, error: %s", err)
return nil, err
if clusterName != "" {
cluster, err := c.clusterClients.Get(clusterName)
if err != nil {
klog.Errorf("get cluster config failed, error: %s", err)
return nil, err
}
if !c.clusterClients.IsHostCluster(cluster) {
clusterConfig, err = c.clusterClients.GetClusterKubeconfig(rls.GetRlsCluster())
if err != nil {
klog.Errorf("get cluster config failed, error: %s", err)
return nil, err
}
}
}
// If clusterConfig is empty, this application will be installed in current host.
hw := helmwrapper.NewHelmWrapper(clusterConfig, namespace, rls.Spec.Name)
manifest, err := hw.Manifest()
......
......@@ -15,6 +15,7 @@ package openpitrix
import (
"context"
"encoding/json"
"fmt"
"github.com/go-openapi/strfmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
......@@ -34,6 +35,7 @@ import (
"kubesphere.io/kubesphere/pkg/simple/client/openpitrix/helmrepoindex"
"kubesphere.io/kubesphere/pkg/utils/reposcache"
"kubesphere.io/kubesphere/pkg/utils/stringutils"
"net/url"
"sigs.k8s.io/controller-runtime/pkg/client"
"sort"
"strings"
......@@ -166,16 +168,54 @@ func (c *repoOperator) ModifyRepo(id string, request *ModifyRepoRequest) error {
if request.Description != nil {
repoCopy.Spec.Description = stringutils.ShortenString(*request.Description, DescriptionLen)
}
if request.URL != nil {
repoCopy.Spec.Url = *request.URL
}
// TODO modify credential
if request.Name != nil {
repoCopy.Labels[constants.NameLabelKey] = *request.Name
if request.Name != nil && len(*request.Name) > 0 && *request.Name != repoCopy.Name {
items, err := c.repoLister.List(labels.SelectorFromSet(map[string]string{constants.WorkspaceLabelKey: repo.GetWorkspace()}))
if err != nil && !apierrors.IsNotFound(err) {
klog.Errorf("list helm repo failed: %s", err)
return err
}
for _, exists := range items {
if exists.GetTrueName() == *request.Name {
klog.Error(repoItemExists, "name: ", *request.Name)
return repoItemExists
}
}
repoCopy.Spec.Name = *request.Name
}
if request.Workspace != nil {
repoCopy.Labels[constants.WorkspaceLabelKey] = *request.Workspace
// modify credential
if request.URL != nil && len(*request.URL) > 0 {
parsedUrl, err := url.Parse(*request.URL)
if err != nil {
return err
}
userInfo := parsedUrl.User
// trim the credential from url
parsedUrl.User = nil
cred := &v1alpha1.HelmRepoCredential{}
if strings.HasPrefix(*request.URL, "https://") || strings.HasPrefix(*request.URL, "http://") {
if userInfo != nil {
cred.Password, _ = userInfo.Password()
cred.Username = userInfo.Username()
} else {
// trim the old credential
cred.Password, _ = userInfo.Password()
cred.Username = userInfo.Username()
}
} else if strings.HasPrefix(*request.URL, "s3://") {
cfg := v1alpha1.S3Config{}
err := json.Unmarshal([]byte(*request.Credential), &cfg)
if err != nil {
return err
}
cred.S3Config = cfg
}
repoCopy.Spec.Credential = *cred
repoCopy.Spec.Url = parsedUrl.String()
}
patch := client.MergeFrom(repo)
......@@ -242,10 +282,8 @@ func (c *repoOperator) ListRepos(conditions *params.Conditions, orderBy string,
}
items := make([]interface{}, 0, limit)
for i, j := offset, 0; i < len(repos) && j < limit; {
for i, j := offset, 0; i < len(repos) && j < limit; i, j = i+1, j+1 {
items = append(items, convertRepo(repos[i]))
i++
j++
}
return &models.PageableResponse{Items: items, TotalCount: len(repos)}, nil
}
......@@ -253,7 +291,7 @@ func (c *repoOperator) ListRepos(conditions *params.Conditions, orderBy string,
func helmRepoFilter(namePrefix string, list []*v1alpha1.HelmRepo) (res []*v1alpha1.HelmRepo) {
for _, repo := range list {
name := repo.GetTrueName()
if strings.HasPrefix(name, namePrefix) {
if strings.Contains(name, namePrefix) {
res = append(res, repo)
}
}
......@@ -288,10 +326,8 @@ func (c *repoOperator) ListRepoEvents(repoId string, conditions *params.Conditio
}
items := make([]interface{}, 0, limit)
for i, j := offset, 0; i < len(repo.Status.SyncState) && j < limit; {
for i, j := offset, 0; i < len(repo.Status.SyncState) && j < limit; i, j = i+1, j+1 {
items = append(items, convertRepoEvent(&repo.ObjectMeta, &repo.Status.SyncState[j]))
i++
j++
}
return &models.PageableResponse{Items: items, TotalCount: len(repo.Status.SyncState)}, nil
......
......@@ -322,7 +322,7 @@ func convertApp(app *v1alpha1.HelmApplication, versions []*v1alpha1.HelmApplicat
}
if versions != nil && len(versions) > 0 {
sort.Sort(AppVersions(versions))
out.LatestAppVersion = convertAppVersion(versions[0])
out.LatestAppVersion = convertAppVersion(versions[len(versions)-1])
} else {
out.LatestAppVersion = &AppVersion{}
}
......@@ -393,7 +393,7 @@ func convertRepo(in *v1alpha1.HelmRepo) *Repo {
out.RepoId = in.GetHelmRepoId()
out.Name = in.GetTrueName()
out.Status = "active"
out.Status = in.Status.State
date := strfmt.DateTime(time.Unix(in.CreationTimestamp.Unix(), 0))
out.CreateTime = &date
......@@ -439,6 +439,43 @@ func (l HelmApplicationList) Less(i, j int) bool {
}
}
type AppVersionReviews []*v1alpha1.HelmApplicationVersion
// Len returns the length.
func (c AppVersionReviews) Len() int { return len(c) }
// Swap swaps the position of two items in the versions slice.
func (c AppVersionReviews) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
// Less returns true if the version of entry a is less than the version of entry b.
func (c AppVersionReviews) Less(a, b int) bool {
aVersion := c[a]
bVersion := c[b]
if len(aVersion.Status.Audit) > 0 && len(bVersion.Status.Audit) > 0 {
t1 := aVersion.Status.Audit[0].Time
t2 := bVersion.Status.Audit[0].Time
if t1.Before(&t2) {
return true
} else if t2.Before(&t1) {
return false
}
}
i, err := semver.NewVersion(aVersion.GetSemver())
if err != nil {
return true
}
j, err := semver.NewVersion(bVersion.GetSemver())
if err != nil {
return false
}
if i.Equal(j) {
return aVersion.CreationTimestamp.Before(&bVersion.CreationTimestamp)
}
return j.LessThan(i)
}
type AppVersions []*v1alpha1.HelmApplicationVersion
// Len returns the length.
......@@ -463,7 +500,7 @@ func (c AppVersions) Less(a, b int) bool {
if i.Equal(j) {
return aVersion.CreationTimestamp.Before(&bVersion.CreationTimestamp)
}
return j.LessThan(i)
return i.LessThan(j)
}
// buildApplicationVersion build an application version
......@@ -524,7 +561,7 @@ func filterAppByName(app *v1alpha1.HelmApplication, namePart string) bool {
}
name := app.GetTrueName()
if strings.HasSuffix(name, namePart) || strings.HasPrefix(name, namePart) {
if strings.Contains(name, namePart) {
return true
}
return false
......@@ -545,6 +582,67 @@ func filterAppByStates(app *v1alpha1.HelmApplication, state []string) bool {
return false
}
func filterAppReviews(versions []*v1alpha1.HelmApplicationVersion, conditions *params.Conditions) []*v1alpha1.HelmApplicationVersion {
if conditions == nil || len(conditions.Match) == 0 || len(versions) == 0 {
return versions
}
curr := 0
for i := 0; i < len(versions); i++ {
if conditions.Match[Keyword] != "" {
if !(strings.Contains(versions[i].Spec.Name, conditions.Match[Keyword])) {
continue
}
}
if conditions.Match[Status] != "" {
states := strings.Split(conditions.Match[Status], "|")
state := versions[i].State()
if !sliceutil.HasString(states, state) {
continue
}
}
if curr != i {
versions[curr] = versions[i]
}
curr++
}
return versions[:curr:curr]
}
func filterAppVersions(versions []*v1alpha1.HelmApplicationVersion, conditions *params.Conditions) []*v1alpha1.HelmApplicationVersion {
if conditions == nil || len(conditions.Match) == 0 || len(versions) == 0 {
return versions
}
curr := 0
for i := 0; i < len(versions); i++ {
if conditions.Match[Keyword] != "" {
if !(strings.Contains(versions[i].Spec.Version, conditions.Match[Keyword]) ||
strings.Contains(versions[i].Spec.AppVersion, conditions.Match[Keyword])) {
continue
}
}
if conditions.Match[Status] != "" {
states := strings.Split(conditions.Match[Status], "|")
state := versions[i].State()
if !sliceutil.HasString(states, state) {
continue
}
}
if curr != i {
versions[curr] = versions[i]
}
curr++
}
return versions[:curr:curr]
}
func filterApps(apps []*v1alpha1.HelmApplication, conditions *params.Conditions) []*v1alpha1.HelmApplication {
if conditions == nil || len(conditions.Match) == 0 || len(apps) == 0 {
return apps
......@@ -575,18 +673,6 @@ func filterApps(apps []*v1alpha1.HelmApplication, conditions *params.Conditions)
return apps[:curr:curr]
}
func filterReleaseByName(rls *v1alpha1.HelmRelease, namePart string) bool {
if len(namePart) == 0 {
return true
}
name := rls.GetTrueName()
if strings.HasSuffix(name, namePart) || strings.HasPrefix(name, namePart) {
return true
}
return false
}
func filterReleaseByStates(rls *v1alpha1.HelmRelease, state []string) bool {
if len(state) == 0 {
return true
......@@ -626,8 +712,11 @@ func filterReleases(releases []*v1alpha1.HelmRelease, conditions *params.Conditi
curr := 0
for i := 0; i < len(releases); i++ {
if conditions.Match[Keyword] != "" {
fv := filterReleaseByName(releases[i], conditions.Match[Keyword])
keyword := conditions.Match[Keyword]
if keyword != "" {
fv := strings.Contains(releases[i].GetTrueName(), keyword) ||
strings.Contains(releases[i].Spec.ChartVersion, keyword) ||
strings.Contains(releases[i].Spec.ChartAppVersion, keyword)
if !fv {
continue
}
......
......@@ -63,7 +63,7 @@ func LoadRepoIndex(ctx context.Context, u string, cred *v1alpha1.HelmRepoCredent
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func loadIndex(data []byte) (*helmrepo.IndexFile, error) {
i := &helmrepo.IndexFile{}
if err := yaml.UnmarshalStrict(data, i); err != nil {
if err := yaml.Unmarshal(data, i); err != nil {
return i, err
}
i.SortEntries()
......@@ -73,6 +73,8 @@ func loadIndex(data []byte) (*helmrepo.IndexFile, error) {
return i, nil
}
var empty = struct{}{}
// merge new index with index from crd
func MergeRepoIndex(index *helmrepo.IndexFile, existsSavedIndex *SavedIndex) *SavedIndex {
saved := &SavedIndex{}
......@@ -92,7 +94,7 @@ func MergeRepoIndex(index *helmrepo.IndexFile, existsSavedIndex *SavedIndex) *Sa
saved.Generated = index.Generated
saved.PublicKeys = index.PublicKeys
allNames := make(map[string]bool, len(index.Entries))
allAppNames := make(map[string]struct{}, len(index.Entries))
for name, versions := range index.Entries {
// add new applications
if application, exists := saved.Applications[name]; !exists {
......@@ -112,6 +114,7 @@ func MergeRepoIndex(index *helmrepo.IndexFile, existsSavedIndex *SavedIndex) *Sa
}
charts = append(charts, chart)
}
application.Charts = charts
saved.Applications[name] = application
} else {
......@@ -121,6 +124,7 @@ func MergeRepoIndex(index *helmrepo.IndexFile, existsSavedIndex *SavedIndex) *Sa
savedChartVersion[ver.Version] = struct{}{}
}
charts := application.Charts
var newVersion = make(map[string]struct{}, len(versions))
for _, ver := range versions {
// add new chart version
if _, exists := savedChartVersion[ver.Version]; !exists {
......@@ -131,15 +135,34 @@ func MergeRepoIndex(index *helmrepo.IndexFile, existsSavedIndex *SavedIndex) *Sa
}
charts = append(charts, chart)
}
application.Charts = charts
saved.Applications[name] = application
newVersion[ver.Version] = empty
}
// delete not exists chart version
for last, curr := 0, 0; curr < len(charts); {
chart := charts[curr]
version := chart.Version
if _, exists := newVersion[version]; !exists {
// version not exists, check next one
curr++
} else {
// If last and curr point to the same place, there is nothing to do, just move to next.
if last != curr {
charts[last] = charts[curr]
}
last++
curr++
}
}
application.Charts = charts[:len(newVersion)]
saved.Applications[name] = application
}
allNames[name] = true
allAppNames[name] = empty
}
for name := range saved.Applications {
if _, exists := allNames[name]; !exists {
if _, exists := allAppNames[name]; !exists {
delete(saved.Applications, name)
}
}
......
......@@ -21,8 +21,10 @@ import (
"encoding/json"
"fmt"
"gopkg.in/yaml.v3"
helmrelease "helm.sh/helm/v3/pkg/release"
"k8s.io/klog"
kpath "k8s.io/utils/path"
"kubesphere.io/kubesphere/pkg/server/errors"
"kubesphere.io/kubesphere/pkg/utils/idutils"
"os"
"os/exec"
......@@ -39,6 +41,7 @@ const (
var (
UninstallNotFoundFormat = "Error: uninstall: Release not loaded: %s: release: not found"
StatusNotFoundFormat = "Error: release: not found"
releaseExists = "release exists"
kustomizationFile = "kustomization.yaml"
postRenderExecFile = "helm-post-render.sh"
......@@ -54,37 +57,6 @@ type HelmRes struct {
Message string
}
type releaseStatus struct {
Name string `json:"name,omitempty"`
Info *Info `json:"info,omitempty"`
}
// copy from helm
// Info describes release information.
type Info struct {
// FirstDeployed is when the release was first deployed.
FirstDeployed time.Time `json:"first_deployed,omitempty"`
// LastDeployed is when the release was last deployed.
LastDeployed time.Time `json:"last_deployed,omitempty"`
// Deleted tracks when this object was deleted.
Deleted time.Time `json:"deleted"`
// Description is human-friendly "log entry" about this release.
Description string `json:"description,omitempty"`
// Status is the current state of the release
Status string `json:"status,omitempty"`
// Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"`
}
type helmRlsStatus struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Revision int `json:"revision"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
}
var _ HelmWrapper = &helmWrapper{}
type HelmWrapper interface {
......@@ -96,7 +68,7 @@ type HelmWrapper interface {
Manifest() (string, error)
}
func (c *helmWrapper) Status() (status *releaseStatus, err error) {
func (c *helmWrapper) Status() (status *helmrelease.Release, err error) {
if err = c.ensureWorkspace(); err != nil {
return nil, err
}
......@@ -127,6 +99,11 @@ func (c *helmWrapper) Status() (status *releaseStatus, err error) {
err = cmd.Run()
if err != nil {
helmErr := strings.TrimSpace(stderr.String())
if helmErr == StatusNotFoundFormat {
klog.V(2).Infof("namespace: %s, name: %s, run command failed, stderr: %s, error: %v", c.Namespace, c.ReleaseName, stderr, err)
return nil, errors.New(helmErr)
}
klog.Errorf("namespace: %s, name: %s, run command failed, stderr: %s, error: %v", c.Namespace, c.ReleaseName, stderr, err)
return
} else {
......@@ -134,7 +111,7 @@ func (c *helmWrapper) Status() (status *releaseStatus, err error) {
klog.V(8).Infof("namespace: %s, name: %s, run command success, stdout: %s", c.Namespace, c.ReleaseName, stdout)
}
status = &releaseStatus{}
status = &helmrelease.Release{}
err = json.Unmarshal(stdout.Bytes(), status)
if err != nil {
klog.Errorf("namespace: %s, name: %s, json unmarshal failed, error: %s", c.Namespace, c.ReleaseName, err)
......@@ -420,7 +397,20 @@ func (c *helmWrapper) Upgrade(chartName, chartData, values string) (res HelmRes,
// helm install
func (c *helmWrapper) Install(chartName, chartData, values string) (res HelmRes, err error) {
return c.install(chartName, chartData, values, false)
sts, err := c.Status()
if err == nil {
// helm release has been installed
if sts.Info != nil && sts.Info.Status == "deployed" {
return HelmRes{}, nil
}
return HelmRes{}, errors.New(releaseExists)
} else {
if err.Error() == StatusNotFoundFormat {
// continue to install
return c.install(chartName, chartData, values, false)
}
return HelmRes{}, err
}
}
func (c *helmWrapper) install(chartName, chartData, values string, upgrade bool) (res HelmRes, err error) {
......
......@@ -28,7 +28,7 @@ func TestHelmInstall(t *testing.T) {
SetAnnotations(map[string]string{constants.CreatorAnnotationKey: "1234"}),
SetMock(true))
res, err := wr.Install("dummy-chart", "", "dummy-value")
res, err := wr.install("dummy-chart", "", "dummy-value", false)
if err != nil {
t.Fail()
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册