Files
kubesphere/pkg/simple/client/application/helper.go
KubeSphere CI Bot c9c856dfda Fix conflict and cherry-pick (cherry-pick from #0e8c6d5) (#6413)
Fix conflict and cherry-pick (cherry-pick from #0e8c6d5)

Co-authored-by: inksnw <inksnw@gmail.com>
2025-03-11 11:22:58 +08:00

598 lines
18 KiB
Go

package application
import (
"bufio"
"bytes"
"context"
"crypto/md5"
"crypto/sha1"
"crypto/tls"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"kubesphere.io/utils/helm"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
pkgconstants "kubesphere.io/kubesphere/pkg/constants"
k8serr "k8s.io/apimachinery/pkg/api/errors"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/registry"
helmrelease "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/golang/example/stringutil"
"github.com/speps/go-hashids"
"kubesphere.io/kubesphere/pkg/utils/idutils"
"k8s.io/klog/v2"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/getter"
helmrepo "helm.sh/helm/v3/pkg/repo"
"kubesphere.io/utils/s3"
"github.com/blang/semver/v4"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml"
appv2 "kubesphere.io/api/application/v2"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/scheme"
)
type AppRequest struct {
RepoName string `json:"repoName,omitempty"`
AppName string `json:"appName,omitempty"`
OriginalName string `json:"originalName,omitempty"`
AliasName string `json:"aliasName,omitempty"`
VersionName string `json:"versionName,omitempty"`
AppHome string `json:"appHome,omitempty"`
Url string `json:"url,omitempty"`
Icon string `json:"icon,omitempty"`
Digest string `json:"digest,omitempty"`
Workspace string `json:"workspace,omitempty"`
Description string `json:"description,omitempty"`
CategoryName string `json:"categoryName,omitempty"`
AppType string `json:"appType,omitempty"`
Package []byte `json:"package,omitempty"`
PullUrl string `json:"pullUrl,omitempty"`
Credential appv2.RepoCredential `json:"credential,omitempty"`
Maintainers []appv2.Maintainer `json:"maintainers,omitempty"`
Abstraction string `json:"abstraction,omitempty"`
Attachments []string `json:"attachments,omitempty"`
FromRepo bool `json:"fromRepo,omitempty"`
Resources []appv2.GroupVersionResource `json:"resources,omitempty"`
}
func GetMaintainers(maintainers []*chart.Maintainer) []appv2.Maintainer {
result := make([]appv2.Maintainer, len(maintainers))
for i, maintainer := range maintainers {
result[i] = appv2.Maintainer{
Name: maintainer.Name,
Email: maintainer.Email,
URL: maintainer.URL,
}
}
return result
}
func CreateOrUpdateApp(client runtimeclient.Client, vRequests []AppRequest, cmStore, ossStore s3.Interface, owns ...metav1.OwnerReference) error {
ctx := context.Background()
if len(vRequests) == 0 {
return errors.New("version request is empty")
}
request := vRequests[0]
app := appv2.Application{}
app.Name = request.AppName
operationResult, err := controllerutil.CreateOrUpdate(ctx, client, &app, func() error {
app.Spec = appv2.ApplicationSpec{
Icon: request.Icon,
AppHome: request.AppHome,
AppType: request.AppType,
Abstraction: request.Abstraction,
Attachments: request.Attachments,
}
if len(owns) > 0 {
app.OwnerReferences = owns
}
labels := app.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
labels[appv2.RepoIDLabelKey] = request.RepoName
labels[appv2.AppTypeLabelKey] = request.AppType
if request.CategoryName != "" {
labels[appv2.AppCategoryNameKey] = request.CategoryName
} else {
labels[appv2.AppCategoryNameKey] = appv2.UncategorizedCategoryID
}
labels[constants.WorkspaceLabelKey] = request.Workspace
app.SetLabels(labels)
ant := app.GetAnnotations()
if ant == nil {
ant = make(map[string]string)
}
ant[constants.DisplayNameAnnotationKey] = request.AliasName
ant[constants.DescriptionAnnotationKey] = request.Description
ant[appv2.AppOriginalNameLabelKey] = request.OriginalName
if len(request.Maintainers) > 0 {
ant[appv2.AppMaintainersKey] = request.Maintainers[0].Name
}
app.SetAnnotations(ant)
return nil
})
if err != nil {
klog.Errorf("failed create or update app %s, err:%v", app.Name, err)
return err
}
if operationResult == controllerutil.OperationResultCreated {
if request.FromRepo {
app.Status.State = appv2.ReviewStatusActive
} else {
app.Status.State = appv2.ReviewStatusDraft
}
}
app.Status.UpdateTime = &metav1.Time{Time: time.Now()}
patch, _ := json.Marshal(app)
if err = client.Status().Patch(ctx, &app, runtimeclient.RawPatch(runtimeclient.Merge.Type(), patch)); err != nil {
klog.Errorf("failed to update app status, err:%v", err)
return err
}
for _, vRequest := range vRequests {
if err = CreateOrUpdateAppVersion(ctx, client, app, vRequest, cmStore, ossStore); err != nil {
klog.Errorf("failed to create or update app version, err:%v", err)
return err
}
}
err = UpdateLatestAppVersion(ctx, client, app)
if err != nil {
klog.Errorf("failed to update latest app version, err:%v", err)
return err
}
return nil
}
func CreateOrUpdateAppVersion(ctx context.Context, client runtimeclient.Client, app appv2.Application, vRequest AppRequest, cmStore, ossStore s3.Interface) error {
//1. create or update app version
appVersion := appv2.ApplicationVersion{}
legalVersion := FormatVersion(vRequest.VersionName)
appVersion.Name = fmt.Sprintf("%s-%s", app.Name, legalVersion)
mutateFn := func() error {
if err := controllerutil.SetControllerReference(&app, &appVersion, scheme.Scheme); err != nil {
klog.Errorf("%s SetControllerReference failed, err:%v", appVersion.Name, err)
return err
}
appVersion.Spec = appv2.ApplicationVersionSpec{
VersionName: vRequest.VersionName,
AppHome: vRequest.AppHome,
Icon: vRequest.Icon,
Created: &metav1.Time{Time: time.Now()},
Digest: vRequest.Digest,
AppType: vRequest.AppType,
Maintainer: vRequest.Maintainers,
PullUrl: vRequest.PullUrl,
}
appVersion.Finalizers = []string{appv2.StoreCleanFinalizer}
labels := appVersion.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
labels[appv2.RepoIDLabelKey] = vRequest.RepoName
labels[appv2.AppIDLabelKey] = vRequest.AppName
labels[appv2.AppTypeLabelKey] = vRequest.AppType
labels[constants.WorkspaceLabelKey] = vRequest.Workspace
appVersion.SetLabels(labels)
ant := appVersion.GetAnnotations()
if ant == nil {
ant = make(map[string]string)
}
ant[constants.DisplayNameAnnotationKey] = vRequest.AliasName
ant[constants.DescriptionAnnotationKey] = vRequest.Description
if len(vRequest.Maintainers) > 0 {
ant[appv2.AppMaintainersKey] = vRequest.Maintainers[0].Name
}
appVersion.SetAnnotations(ant)
return nil
}
_, err := controllerutil.CreateOrUpdate(ctx, client, &appVersion, mutateFn)
if err != nil {
klog.Errorf("failed create or update app version %s, err:%v", appVersion.Name, err)
return err
}
if !vRequest.FromRepo {
err = FailOverUpload(cmStore, ossStore, appVersion.Name, bytes.NewReader(vRequest.Package), len(vRequest.Package))
if err != nil {
klog.Errorf("upload package failed, error: %s", err)
return err
}
}
//3. update app version status
if vRequest.FromRepo {
appVersion.Status.State = appv2.ReviewStatusActive
} else {
appVersion.Status.State = appv2.ReviewStatusDraft
}
appVersion.Status.Updated = &metav1.Time{Time: time.Now()}
patch, _ := json.Marshal(appVersion)
if err = client.Status().Patch(ctx, &appVersion, runtimeclient.RawPatch(runtimeclient.Merge.Type(), patch)); err != nil {
klog.Errorf("failed to update app version status, err:%v", err)
return err
}
return err
}
func UpdateLatestAppVersion(ctx context.Context, client runtimeclient.Client, app appv2.Application) (err error) {
//4. update app latest version
err = client.Get(ctx, runtimeclient.ObjectKey{Name: app.Name, Namespace: app.Namespace}, &app)
if err != nil {
klog.Errorf("failed to get app, err:%v", err)
return err
}
appVersionList := appv2.ApplicationVersionList{}
lbs := labels.SelectorFromSet(labels.Set{appv2.AppIDLabelKey: app.Name})
opt := runtimeclient.ListOptions{LabelSelector: lbs}
err = client.List(ctx, &appVersionList, &opt)
if err != nil {
klog.Errorf("failed to list app version, err:%v", err)
return err
}
if len(appVersionList.Items) == 0 {
return nil
}
latestAppVersion := appVersionList.Items[0].Spec.VersionName
for _, v := range appVersionList.Items {
parsedVersion, err := semver.Make(strings.TrimPrefix(v.Spec.VersionName, "v"))
if err != nil {
klog.Warningf("failed to parse version: %s, use first version %s", v.Spec.VersionName, latestAppVersion)
continue
}
if parsedVersion.GT(semver.MustParse(strings.TrimPrefix(latestAppVersion, "v"))) {
latestAppVersion = v.Spec.VersionName
}
}
ant := app.GetAnnotations()
ant[appv2.LatestAppVersionKey] = latestAppVersion
app.SetAnnotations(ant)
err = client.Update(ctx, &app)
return err
}
func HelmPull(u string, cred appv2.RepoCredential) (*bytes.Buffer, error) {
parsedURL, err := url.Parse(u)
if err != nil {
return nil, err
}
var resp *bytes.Buffer
skipTLS := true
if cred.InsecureSkipTLSVerify != nil && !*cred.InsecureSkipTLSVerify {
skipTLS = false
}
indexURL := parsedURL.String()
g, _ := getter.NewHTTPGetter()
options := []getter.Option{
getter.WithTimeout(5 * time.Minute),
getter.WithURL(u),
getter.WithInsecureSkipVerifyTLS(skipTLS),
getter.WithTLSClientConfig(cred.CertFile, cred.KeyFile, cred.CAFile),
getter.WithBasicAuth(cred.Username, cred.Password)}
if skipTLS {
options = append(options, getter.WithTransport(
&http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
))
}
resp, err = g.Get(indexURL, options...)
return resp, err
}
func LoadRepoIndex(u string, cred appv2.RepoCredential) (idx helmrepo.IndexFile, err error) {
if registry.IsOCI(u) {
return LoadRepoIndexFromOci(u, cred)
}
if !strings.HasSuffix(u, "/") {
u = fmt.Sprintf("%s/index.yaml", u)
} else {
u = fmt.Sprintf("%sindex.yaml", u)
}
resp, err := HelmPull(u, cred)
if err != nil {
return idx, err
}
if err = yaml.Unmarshal(resp.Bytes(), &idx); err != nil {
return idx, err
}
idx.SortEntries()
return idx, nil
}
func ReadYaml(data []byte) (jsonList []json.RawMessage, err error) {
reader := bytes.NewReader(data)
bufReader := bufio.NewReader(reader)
r := yaml.NewYAMLReader(bufReader)
for {
d, err := r.Read()
if err != nil && err == io.EOF {
break
}
jsonData, err := yaml.ToJSON(d)
if err != nil {
return nil, err
}
_, _, err = Decode(jsonData)
if err != nil {
return nil, err
}
jsonList = append(jsonList, jsonData)
}
return jsonList, nil
}
func Decode(data []byte) (obj runtime.Object, gvk *schema.GroupVersionKind, err error) {
decoder := unstructured.UnstructuredJSONScheme
obj, gvk, err = decoder.Decode(data, nil, nil)
return obj, gvk, err
}
func UpdateHelmStatus(kubeConfig []byte, release *helmrelease.Release) (deployed bool, err error) {
config, err := clientcmd.RESTConfigFromKubeConfig(kubeConfig)
if err != nil {
klog.Errorf("failed to get rest config, err:%v", err)
return deployed, err
}
clientSet, err := kubernetes.NewForConfig(config)
if err != nil {
klog.Errorf("failed to get kubernetes client, err:%v", err)
return deployed, err
}
actionConfig := new(action.Configuration)
store := storage.Init(driver.NewSecrets(clientSet.CoreV1().Secrets(release.Namespace)))
actionConfig.Releases = store
deployed, err = checkReady(release, clientSet, kubeConfig)
if err != nil {
klog.Errorf("failed to check helm ready, err:%v", err)
return deployed, err
}
if !deployed {
klog.Infof("helm release %s not ready", release.Name)
return deployed, nil
}
klog.Infof("helm release %s now ready", release.Name)
release.SetStatus("deployed", "Successfully deployed")
if err = actionConfig.Releases.Update(release); err != nil {
klog.Errorf("failed to update release: %v", err)
return deployed, err
}
klog.Infof("update release %s status successfully", release.Name)
return true, err
}
func checkReady(release *helmrelease.Release, clientSet *kubernetes.Clientset, kubeConfig []byte) (allReady bool, err error) {
checker := kube.NewReadyChecker(clientSet, nil, kube.PausedAsReady(true), kube.CheckJobs(true))
helmConf, err := helm.InitHelmConf(kubeConfig, release.Namespace)
if err != nil {
klog.Errorf("failed to init helm conf, err:%v", err)
return allReady, err
}
allResources := make([]*resource.Info, 0)
for _, i := range release.Hooks {
hookResources, err := helmConf.KubeClient.Build(bytes.NewBufferString(i.Manifest), false)
if err != nil {
klog.Errorf("failed to get helm hookResources, err:%v", err)
return allReady, err
}
allResources = append(allResources, hookResources...)
}
klog.Infof("%s get helm hookResources %d", release.Name, len(allResources))
chartResources, err := helmConf.KubeClient.Build(bytes.NewBufferString(release.Manifest), false)
if err != nil {
klog.Errorf("failed to get helm resources, err:%v", err)
return allReady, err
}
allResources = append(allResources, chartResources...)
klog.Infof("%s get helm chartResources %d", release.Name, len(chartResources))
for idx, j := range allResources {
kind := j.Object.GetObjectKind().GroupVersionKind().Kind
klog.Infof("[%d/%d] check helm release %s %s: %s/%s", idx+1, len(allResources),
release.Name, kind, j.Namespace, j.Name)
ready, err := checker.IsReady(context.Background(), j)
if k8serr.IsNotFound(err) {
//pre-job-->chart-resource-->post-job
//If a certain step times out, the subsequent steps will not be created,
//and the status is considered failed, no repair will be made.
klog.Warningf("[%d/%d] helm release %s resource %s: %s/%s not found", idx+1, len(allResources), release.Name, kind, j.Namespace, j.Name)
return false, nil
}
if err != nil {
klog.Errorf("failed to check resource ready, err:%v", err)
return allReady, err
}
if !ready {
klog.Infof("[%d/%d] helm release %s resource %s: %s/%s not ready", idx+1, len(allResources), release.Name, kind, j.Namespace, j.Name)
return allReady, nil
}
}
return true, nil
}
func GvkToGvr(gvk *schema.GroupVersionKind, mapper meta.RESTMapper) (schema.GroupVersionResource, error) {
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if meta.IsNoMatchError(err) || err != nil {
return schema.GroupVersionResource{}, err
}
return mapping.Resource, nil
}
func GetInfoFromBytes(bytes json.RawMessage, mapper meta.RESTMapper) (gvr schema.GroupVersionResource, utd *unstructured.Unstructured, err error) {
obj, gvk, err := Decode(bytes)
if err != nil {
return gvr, utd, err
}
gvr, err = GvkToGvr(gvk, mapper)
if err != nil {
return gvr, utd, err
}
utd, err = ConvertToUnstructured(obj)
return gvr, utd, err
}
func ConvertToUnstructured(obj any) (*unstructured.Unstructured, error) {
objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
return &unstructured.Unstructured{Object: objMap}, err
}
func ComplianceCheck(values, tempLate []byte, mapper meta.RESTMapper, ns string) (result []json.RawMessage, err error) {
yamlList, err := ReadYaml(values)
if err != nil {
return nil, err
}
yamlTempList, err := ReadYaml(tempLate)
if err != nil {
return nil, err
}
if len(yamlTempList) != len(yamlList) {
return nil, errors.New("yamlList and yamlTempList length not equal")
}
for idx := range yamlTempList {
_, utd, err := GetInfoFromBytes(yamlList[idx], mapper)
if err != nil {
return nil, err
}
_, utdTemp, err := GetInfoFromBytes(yamlTempList[idx], mapper)
if err != nil {
return nil, err
}
if utdTemp.GetKind() != utd.GetKind() || utdTemp.GetAPIVersion() != utd.GetAPIVersion() {
return nil, errors.New("yamlList and yamlTempList not equal")
}
if utd.GetNamespace() != ns {
return nil, errors.New("subresource must have same namespace with app release")
}
}
return yamlList, nil
}
func GetUuid36(prefix string) string {
id := idutils.GetIntId()
hd := hashids.NewData()
hd.Alphabet = idutils.Alphabet36
h, err := hashids.NewWithData(hd)
if err != nil {
panic(err)
}
i, err := h.Encode([]int{int(id)})
if err != nil {
panic(err)
}
//hashids.minAlphabetLength = 16
add := stringutil.Reverse(i)[:5]
return prefix + add
}
func GenerateShortNameMD5Hash(input string) string {
input = strings.ToLower(input)
errs := validation.IsDNS1123Subdomain(input)
if len(input) > 14 || len(errs) != 0 {
hash := md5.New()
hash.Write([]byte(input))
hashInBytes := hash.Sum(nil)
hashString := hex.EncodeToString(hashInBytes)
return hashString[:10]
}
return input
}
func FormatVersion(input string) string {
if len(validation.IsDNS1123Subdomain(input)) == 0 {
return input
}
hash := sha1.Sum([]byte(input))
formattedVersion := hex.EncodeToString(hash[:])[:12]
return formattedVersion
}
func GetHelmKubeConfig(ctx context.Context, cluster *clusterv1alpha1.Cluster, runClient client.Client) (config []byte, err error) {
if cluster.Spec.Connection.Type == clusterv1alpha1.ConnectionTypeProxy {
klog.Infof("cluster %s is proxy cluster", cluster.Name)
secret := &corev1.Secret{}
key := types.NamespacedName{Namespace: pkgconstants.KubeSphereNamespace, Name: "kubeconfig-admin"}
err = runClient.Get(ctx, key, secret)
if err != nil {
klog.Errorf("failed to get kubeconfig-admin secret: %v", err)
return nil, err
}
config = secret.Data["config"]
return config, err
}
return cluster.Spec.Connection.KubeConfig, nil
}