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:
KubeSphere CI Bot
2024-09-06 11:05:52 +08:00
committed by GitHub
parent b5015ec7b9
commit 447a51f08b
8557 changed files with 546695 additions and 1146174 deletions

View File

@@ -0,0 +1,84 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"bytes"
"fmt"
"io"
"strings"
"kubesphere.io/kubesphere/pkg/simple/client/application"
restful "github.com/emicklei/go-restful/v3"
"github.com/go-openapi/strfmt"
"k8s.io/klog/v2"
"kubesphere.io/kubesphere/pkg/api"
)
type Attachment struct {
AttachmentContent map[string]strfmt.Base64 `json:"attachment_content,omitempty"`
AttachmentID string `json:"attachment_id,omitempty"`
}
func (h *appHandler) DescribeAttachment(req *restful.Request, resp *restful.Response) {
attachmentId := req.PathParameter("attachment")
data, err := application.FailOverGet(h.cmStore, h.ossStore, attachmentId, h.client, false)
if requestDone(err, resp) {
return
}
result := &Attachment{AttachmentID: attachmentId,
AttachmentContent: map[string]strfmt.Base64{
"raw": data,
},
}
resp.WriteEntity(result)
}
func (h *appHandler) CreateAttachment(req *restful.Request, resp *restful.Response) {
err := req.Request.ParseMultipartForm(10 << 20)
if err != nil {
api.HandleBadRequest(resp, nil, err)
return
}
var att *Attachment
// just save one attachment
for fName := range req.Request.MultipartForm.File {
f, _, err := req.Request.FormFile(fName)
if err != nil {
api.HandleBadRequest(resp, nil, err)
return
}
data, _ := io.ReadAll(f)
f.Close()
id := application.GetUuid36(fmt.Sprintf("%s-att-", fName))
err = application.FailOverUpload(h.cmStore, h.ossStore, id, bytes.NewBuffer(data), len(data))
if err != nil {
klog.Errorf("upload attachment failed, err: %s", err)
api.HandleBadRequest(resp, nil, err)
return
}
klog.V(4).Infof("upload attachment success")
att = &Attachment{AttachmentID: id}
break
}
resp.WriteEntity(att)
}
func (h *appHandler) DeleteAttachments(req *restful.Request, resp *restful.Response) {
attachmentId := req.PathParameter("attachment")
ids := strings.Split(attachmentId, ",")
err := application.FailOverDelete(h.cmStore, h.ossStore, ids)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
}

View File

@@ -0,0 +1,105 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"errors"
"fmt"
"github.com/emicklei/go-restful/v3"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"kubesphere.io/kubesphere/pkg/api"
)
func (h *appHandler) exampleCr(req *restful.Request, resp *restful.Response) {
name := req.PathParameter("name")
crd := v1.CustomResourceDefinition{}
err := h.client.Get(req.Request.Context(), client.ObjectKey{Name: name}, &crd)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
cr, err := convertCRDToCR(crd)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
resp.WriteEntity(cr)
}
func convertCRDToCR(crd v1.CustomResourceDefinition) (dstCr unstructured.Unstructured, err error) {
cr := unstructured.Unstructured{}
cr.SetName(fmt.Sprintf("%s-Instance", crd.Spec.Names.Singular))
cr.SetGroupVersionKind(schema.GroupVersionKind{
Group: crd.Spec.Group,
Kind: crd.Spec.Names.Kind,
})
var selectedVersion *v1.CustomResourceDefinitionVersion
for _, version := range crd.Spec.Versions {
if version.Served && version.Storage {
selectedVersion = &version
break
}
}
if selectedVersion == nil {
return dstCr, errors.New("no served and storage version found in CRD")
}
cr.SetAPIVersion(selectedVersion.Name)
generateProps(selectedVersion, cr, "spec")
generateProps(selectedVersion, cr, "status")
return cr, nil
}
func generateProps(selectedVersion *v1.CustomResourceDefinitionVersion, cr unstructured.Unstructured, name string) {
data := make(map[string]any)
specProps := selectedVersion.Schema.OpenAPIV3Schema.Properties[name].Properties
for key, value := range specProps {
data[key] = getDefaultValue(value)
}
cr.Object[name] = data
}
func getDefaultValue(value v1.JSONSchemaProps) any {
switch value.Type {
case "object":
return parseObject(value.Properties)
case "integer":
if value.Minimum != nil {
return *value.Minimum
}
return 0
case "boolean":
return false
case "array":
if value.Items.Schema != nil {
return []any{getDefaultValue(*value.Items.Schema)}
}
return []any{}
case "string":
if len(value.Enum) > 0 {
return string(value.Enum[0].Raw)
}
return ""
default:
return nil
}
}
func parseObject(obj map[string]v1.JSONSchemaProps) map[string]any {
res := make(map[string]any)
for key, value := range obj {
res[key] = getDefaultValue(value)
}
return res
}

View File

@@ -0,0 +1,105 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"fmt"
goruntime "runtime"
"kubesphere.io/kubesphere/pkg/server/params"
"github.com/emicklei/go-restful/v3"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/apiserver/query"
resv1beta1 "kubesphere.io/kubesphere/pkg/models/resources/v1beta1"
)
func (h *appHandler) conflictedDone(req *restful.Request, resp *restful.Response, pathParam string, obj client.Object) bool {
existed, err := h.checkConflicted(req, pathParam, obj)
if err != nil {
api.HandleInternalError(resp, nil, err)
return true
}
if existed {
kind := obj.GetObjectKind().GroupVersionKind().Kind
api.HandleConflict(resp, req, fmt.Errorf("%s %s already exists", kind, obj.GetName()))
return true
}
return false
}
func (h *appHandler) checkConflicted(req *restful.Request, pathParam string, obj client.Object) (bool, error) {
key := runtimeclient.ObjectKey{Name: obj.GetName(), Namespace: obj.GetNamespace()}
if req.PathParameter(pathParam) != "" {
//if route like /repos/{repo} and request path has repo, then it's update, filling obj
err := h.client.Get(req.Request.Context(), key, obj)
if err != nil {
return false, err
}
return false, nil
}
//if route like /repos, then it's create
err := h.client.Get(req.Request.Context(), key, obj)
if err != nil && apierrors.IsNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func requestDone(err error, resp *restful.Response) bool {
_, file, line, _ := goruntime.Caller(1)
if err != nil {
if apierrors.IsNotFound(err) {
api.HandleNotFound(resp, nil, err)
return true
}
klog.Errorf("%s:%d request done with error: %v", file, line, err)
api.HandleInternalError(resp, nil, err)
return true
}
return false
}
func removeQueryArg(req *restful.Request, args ...string) {
//The default filter is a whitelist, so delete some of our custom logical parameters
for _, i := range args {
q := req.Request.URL.Query()
q.Del(i)
req.Request.URL.RawQuery = q.Encode()
}
}
func convertToListResult(obj runtime.Object, req *restful.Request) (listResult api.ListResult) {
removeQueryArg(req, params.ConditionsParam, "global", "create")
_ = meta.EachListItem(obj, omitManagedFields)
queryParams := query.ParseQueryParameter(req)
list, _ := meta.ExtractList(obj)
items, _, totalCount := resv1beta1.DefaultList(list, queryParams, resv1beta1.DefaultCompare, resv1beta1.DefaultFilter)
listResult.Items = items
listResult.TotalItems = totalCount
return listResult
}
func omitManagedFields(o runtime.Object) error {
a, err := meta.Accessor(o)
if err != nil {
return err
}
a.SetManagedFields(nil)
return nil
}

View File

@@ -0,0 +1,342 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"encoding/json"
"fmt"
"io"
"mime"
"strconv"
"strings"
"time"
"kubesphere.io/kubesphere/pkg/utils/stringutils"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
"github.com/emicklei/go-restful/v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/klog/v2"
appv2 "kubesphere.io/api/application/v2"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/server/errors"
"kubesphere.io/kubesphere/pkg/server/params"
"kubesphere.io/kubesphere/pkg/simple/client/application"
)
const maxFileSize = 1 * 1024 * 1024 // 1 MB in bytes
func (h *appHandler) CreateOrUpdateApp(req *restful.Request, resp *restful.Response) {
createAppRequest, err := parseUpload(req)
if requestDone(err, resp) {
return
}
if h.ossStore == nil && len(createAppRequest.Package) > maxFileSize {
api.HandleBadRequest(resp, nil, fmt.Errorf("System has no OSS store, the maximum file size is %d", maxFileSize))
return
}
newReq, err := parseRequest(createAppRequest)
if requestDone(err, resp) {
return
}
data := map[string]any{
"icon": newReq.Icon,
"appName": newReq.AppName,
"versionName": newReq.VersionName,
"appHome": newReq.AppHome,
"description": newReq.Description,
"aliasName": newReq.AliasName,
"resources": newReq.Resources,
}
validate, _ := strconv.ParseBool(req.QueryParameter("validate"))
if validate {
resp.WriteAsJson(data)
return
}
app := &appv2.Application{}
app.Name = newReq.AppName
if h.conflictedDone(req, resp, "app", app) {
return
}
newReq.FromRepo = false
vRequests := []application.AppRequest{newReq}
err = application.CreateOrUpdateApp(h.client, vRequests, h.cmStore, h.ossStore)
if requestDone(err, resp) {
return
}
resp.WriteAsJson(data)
}
func parseUpload(req *restful.Request) (createAppRequest application.AppRequest, err error) {
contentType := req.Request.Header.Get("Content-Type")
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
klog.Errorf("parse media type failed, err: %s", err)
return createAppRequest, err
}
if mediaType == "multipart/form-data" {
file, header, err := req.Request.FormFile("file")
if err != nil {
klog.Errorf("parse form file failed, err: %s", err)
return createAppRequest, err
}
klog.Info("upload file:", header.Filename)
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
klog.Errorf("read file failed, err: %s", err)
return createAppRequest, err
}
err = json.Unmarshal([]byte(req.Request.FormValue("jsonData")), &createAppRequest)
if err != nil {
klog.Errorf("parse json data failed, err: %s", err)
return createAppRequest, err
}
if createAppRequest.Package != nil {
return createAppRequest, errors.New("When using multipart/form-data to upload files, the package field does not need to be included in the json data.")
}
createAppRequest.Package = data
return createAppRequest, nil
}
if mediaType == "application/json" {
err = req.ReadEntity(&createAppRequest)
if err != nil {
klog.Errorf("parse json data failed, err: %s", err)
return createAppRequest, err
}
return createAppRequest, nil
}
return createAppRequest, errors.New("unsupported media type")
}
func (h *appHandler) ListApps(req *restful.Request, resp *restful.Response) {
workspace := req.PathParameter("workspace")
conditions, err := params.ParseConditions(req)
if requestDone(err, resp) {
return
}
opt := runtimeclient.ListOptions{}
labelSelectorStr := req.QueryParameter("labelSelector")
labelSelector, err := labels.Parse(labelSelectorStr)
if err != nil {
api.HandleBadRequest(resp, nil, err)
return
}
opt.LabelSelector = labelSelector
result := appv2.ApplicationList{}
err = h.client.List(req.Request.Context(), &result, &opt)
if requestDone(err, resp) {
return
}
filtered := appv2.ApplicationList{}
for _, app := range result.Items {
curApp := app
states := strings.Split(conditions.Match[Status], "|")
if conditions.Match[Status] != "" && !sliceutil.HasString(states, curApp.Status.State) {
continue
}
allowList := []string{appv2.SystemWorkspace, workspace}
if workspace != "" && !stringutils.StringIn(app.Labels[constants.WorkspaceLabelKey], allowList) {
continue
}
filtered.Items = append(filtered.Items, curApp)
}
resp.WriteEntity(convertToListResult(&filtered, req))
}
func (h *appHandler) DescribeApp(req *restful.Request, resp *restful.Response) {
key := runtimeclient.ObjectKey{Name: req.PathParameter("app")}
app := &appv2.Application{}
err := h.client.Get(req.Request.Context(), key, app)
if requestDone(err, resp) {
return
}
app.SetManagedFields(nil)
resp.WriteEntity(app)
}
func (h *appHandler) DeleteApp(req *restful.Request, resp *restful.Response) {
appId := req.PathParameter("app")
app := &appv2.Application{}
err := h.client.Get(req.Request.Context(), runtimeclient.ObjectKey{Name: appId}, app)
if requestDone(err, resp) {
return
}
err = application.FailOverDelete(h.cmStore, h.ossStore, app.Spec.Attachments)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
err = h.client.Delete(req.Request.Context(), &appv2.Application{ObjectMeta: metav1.ObjectMeta{Name: appId}})
if requestDone(err, resp) {
return
}
resp.WriteEntity(errors.None)
}
func (h *appHandler) DoAppAction(req *restful.Request, resp *restful.Response) {
var doActionRequest appv2.ApplicationVersionStatus
err := req.ReadEntity(&doActionRequest)
if err != nil {
klog.V(4).Infoln(err)
api.HandleBadRequest(resp, nil, err)
return
}
ctx := req.Request.Context()
user, _ := request.UserFrom(req.Request.Context())
if user != nil {
doActionRequest.UserName = user.GetName()
}
app := &appv2.Application{}
app.Name = req.PathParameter("app")
err = h.client.Get(ctx, runtimeclient.ObjectKey{Name: app.Name}, app)
if requestDone(err, resp) {
return
}
// app state check, draft -> active -> suspended -> active
switch doActionRequest.State {
case appv2.ReviewStatusActive:
if app.Status.State != appv2.ReviewStatusDraft &&
app.Status.State != appv2.ReviewStatusSuspended {
err = fmt.Errorf("app %s is not in draft or suspended status", app.Name)
break
}
// active state is only allowed if at least one app version is in active or passed state
appVersionList := &appv2.ApplicationVersionList{}
opt := &runtimeclient.ListOptions{LabelSelector: labels.SelectorFromSet(labels.Set{appv2.AppIDLabelKey: app.Name})}
h.client.List(ctx, appVersionList, opt)
okActive := false
if len(appVersionList.Items) > 0 {
for _, v := range appVersionList.Items {
if v.Status.State == appv2.ReviewStatusActive ||
v.Status.State == appv2.ReviewStatusPassed {
okActive = true
break
}
}
}
if !okActive {
err = fmt.Errorf("app %s has no active or passed appversion", app.Name)
}
case appv2.ReviewStatusSuspended:
if app.Status.State != appv2.ReviewStatusActive {
err = fmt.Errorf("app %s is not in active status", app.Name)
}
}
if requestDone(err, resp) {
return
}
if doActionRequest.State == appv2.ReviewStatusActive {
if app.Labels == nil {
app.Labels = map[string]string{}
}
app.Labels[appv2.AppStoreLabelKey] = "true"
h.client.Update(ctx, app)
}
app.Status.State = doActionRequest.State
app.Status.UpdateTime = &metav1.Time{Time: time.Now()}
err = h.client.Status().Update(ctx, app)
if requestDone(err, resp) {
return
}
//update appversion status
versions := &appv2.ApplicationVersionList{}
opt := &runtimeclient.ListOptions{
LabelSelector: labels.SelectorFromSet(labels.Set{appv2.AppIDLabelKey: app.Name}),
}
err = h.client.List(ctx, versions, opt)
if requestDone(err, resp) {
return
}
for _, version := range versions.Items {
if version.Status.State == appv2.StatusActive || version.Status.State == appv2.ReviewStatusSuspended {
err = DoAppVersionAction(ctx, version.Name, doActionRequest, h.client)
if err != nil {
klog.V(4).Infoln(err)
api.HandleInternalError(resp, nil, err)
return
}
}
}
resp.WriteEntity(errors.None)
}
func (h *appHandler) PatchApp(req *restful.Request, resp *restful.Response) {
var err error
appId := req.PathParameter("app")
appBody := &application.AppRequest{}
err = req.ReadEntity(appBody)
if requestDone(err, resp) {
return
}
ctx := req.Request.Context()
app := &appv2.Application{}
app.Name = appId
err = h.client.Get(ctx, runtimeclient.ObjectKey{Name: app.Name}, app)
if requestDone(err, resp) {
return
}
if app.GetLabels() == nil {
app.SetLabels(map[string]string{})
}
app.Labels[appv2.AppCategoryNameKey] = appBody.CategoryName
app.Spec.Icon = appBody.Icon
ant := app.GetAnnotations()
if ant == nil {
ant = make(map[string]string)
}
ant[constants.DescriptionAnnotationKey] = appBody.Description
ant[constants.DisplayNameAnnotationKey] = appBody.AliasName
app.SetAnnotations(ant)
app.Spec.Attachments = appBody.Attachments
app.Spec.Abstraction = appBody.Abstraction
app.Spec.AppHome = appBody.AppHome
err = h.client.Update(ctx, app)
if requestDone(err, resp) {
return
}
klog.V(4).Infof("update app %s successfully", app.Name)
resp.WriteAsJson(app)
}

View File

@@ -0,0 +1,296 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"bytes"
"encoding/json"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"github.com/emicklei/go-restful/v3"
"golang.org/x/net/context"
"helm.sh/helm/v3/pkg/action"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
appv2 "kubesphere.io/api/application/v2"
"kubesphere.io/api/constants"
"kubesphere.io/utils/helm"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/server/errors"
"kubesphere.io/kubesphere/pkg/simple/client/application"
)
func (h *appHandler) CreateOrUpdateAppRls(req *restful.Request, resp *restful.Response) {
var createRlsRequest appv2.ApplicationRelease
err := req.ReadEntity(&createRlsRequest)
if requestDone(err, resp) {
return
}
list := []string{appv2.AppIDLabelKey, constants.ClusterNameLabelKey, constants.WorkspaceLabelKey, constants.NamespaceLabelKey}
for _, i := range list {
value, ok := createRlsRequest.GetLabels()[i]
if !ok || value == "" {
err = errors.New("must set %s", i)
api.HandleBadRequest(resp, nil, err)
return
}
}
apprls := appv2.ApplicationRelease{}
apprls.Name = createRlsRequest.Name
if h.conflictedDone(req, resp, "application", &apprls) {
return
}
if createRlsRequest.Spec.AppType != appv2.AppTypeHelm {
runtimeClient, _, _, err := h.getCluster(createRlsRequest.GetRlsCluster())
if requestDone(err, resp) {
return
}
template, err := application.FailOverGet(h.cmStore, h.ossStore, createRlsRequest.Spec.AppVersionID, h.client, true)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
_, err = application.ComplianceCheck(createRlsRequest.Spec.Values, template,
runtimeClient.RESTMapper(), createRlsRequest.GetRlsNamespace())
if requestDone(err, resp) {
return
}
}
user, _ := request.UserFrom(req.Request.Context())
creator := ""
if user != nil {
creator = user.GetName()
}
copyRls := apprls.DeepCopy()
mutateFn := func() error {
createRlsRequest.DeepCopyInto(&apprls)
apprls.ResourceVersion = copyRls.ResourceVersion
if apprls.Labels == nil {
apprls.Labels = map[string]string{}
}
apprls.Labels[appv2.AppVersionIDLabelKey] = createRlsRequest.Spec.AppVersionID
if apprls.Annotations == nil {
apprls.Annotations = map[string]string{}
}
apprls.Annotations[constants.CreatorAnnotationKey] = creator
return nil
}
_, err = controllerutil.CreateOrUpdate(req.Request.Context(), h.client, &apprls, mutateFn)
if requestDone(err, resp) {
return
}
resp.WriteEntity(errors.None)
}
func (h *appHandler) DescribeAppRls(req *restful.Request, resp *restful.Response) {
applicationId := req.PathParameter("application")
ctx := req.Request.Context()
key := runtimeclient.ObjectKey{Name: applicationId}
app := &appv2.ApplicationRelease{}
err := h.client.Get(ctx, key, app)
if requestDone(err, resp) {
return
}
app.SetManagedFields(nil)
if app.Spec.AppType == appv2.AppTypeYaml || app.Spec.AppType == appv2.AppTypeEdge {
data, err := h.getRealTimeYaml(ctx, app)
if err != nil {
klog.Errorf("getRealTimeYaml: %s", err.Error())
app.Status.RealTimeResources = nil
resp.WriteEntity(app)
return
}
app.Status.RealTimeResources = data
resp.WriteEntity(app)
return
}
data, err := h.getRealTimeHelm(ctx, app)
if err != nil {
klog.Errorf("getRealTimeHelm: %s", err.Error())
app.Status.RealTimeResources = nil
resp.WriteEntity(app)
return
}
app.Status.RealTimeResources = data
resp.WriteEntity(app)
}
func (h *appHandler) getRealTimeYaml(ctx context.Context, app *appv2.ApplicationRelease) (data []json.RawMessage, err error) {
runtimeClient, dynamicClient, cluster, err := h.getCluster(app.GetRlsCluster())
if err != nil {
klog.Errorf("cluster: %s url: %s: %s", cluster.Name, cluster.Spec.Connection.KubernetesAPIEndpoint, err)
return nil, err
}
jsonList, err := application.ReadYaml(app.Spec.Values)
if err != nil {
klog.Errorf("ReadYaml: %s", err.Error())
return nil, err
}
for _, i := range jsonList {
gvr, utd, err := application.GetInfoFromBytes(i, runtimeClient.RESTMapper())
if err != nil {
klog.Errorf("GetInfoFromBytes: %s", err.Error())
return nil, err
}
var utdRealTime *unstructured.Unstructured
utdRealTime, err = dynamicClient.Resource(gvr).Namespace(utd.GetNamespace()).
Get(ctx, utd.GetName(), metav1.GetOptions{})
if err != nil {
klog.Errorf("cluster: %s url: %s resource: %s/%s/%s: %s",
cluster.Name, cluster.Spec.Connection.KubernetesAPIEndpoint, utd.GetNamespace(), gvr.Resource, utd.GetName(), err)
realTimeJson := errorRealTime(utd, err.Error())
data = append(data, realTimeJson)
continue
}
utdRealTime.SetManagedFields(nil)
realTimeJson, err := utdRealTime.MarshalJSON()
if err != nil {
klog.Errorf("MarshalJSON: %s", err.Error())
return nil, err
}
data = append(data, realTimeJson)
}
return data, err
}
func (h *appHandler) getRealTimeHelm(ctx context.Context, app *appv2.ApplicationRelease) (data []json.RawMessage, err error) {
runtimeClient, dynamicClient, cluster, err := h.getCluster(app.GetRlsCluster())
if err != nil {
klog.Errorf("cluster: %s url: %s: %s", cluster.Name, cluster.Spec.Connection.KubernetesAPIEndpoint, err)
return nil, err
}
helmConf, err := helm.InitHelmConf(cluster.Spec.Connection.KubeConfig, app.GetRlsNamespace())
if err != nil {
klog.Errorf("InitHelmConf cluster: %s url: %s: %s", cluster.Name, cluster.Spec.Connection.KubernetesAPIEndpoint, err)
return nil, err
}
rel, err := action.NewGet(helmConf).Run(app.Name)
if err != nil {
klog.Errorf("cluster: %s url: %s release: %s: %s", cluster.Name, cluster.Spec.Connection.KubernetesAPIEndpoint, app.Name, err)
return nil, err
}
resources, _ := helmConf.KubeClient.Build(bytes.NewBufferString(rel.Manifest), true)
for _, i := range resources {
utd, err := application.ConvertToUnstructured(i.Object)
if err != nil {
klog.Errorf("ConvertToUnstructured: %s", err.Error())
return nil, err
}
marshalJSON, err := utd.MarshalJSON()
if err != nil {
klog.Errorf("MarshalJSON: %s", err.Error())
return nil, err
}
gvr, utd, err := application.GetInfoFromBytes(marshalJSON, runtimeClient.RESTMapper())
if err != nil {
klog.Errorf("GetInfoFromBytes: %s", err.Error())
return nil, err
}
utdRealTime, err := dynamicClient.Resource(gvr).Namespace(utd.GetNamespace()).
Get(ctx, utd.GetName(), metav1.GetOptions{})
if err != nil {
klog.Errorf("cluster: %s url: %s resource: %s/%s/%s: %s",
cluster.Name, cluster.Spec.Connection.KubernetesAPIEndpoint, utd.GetNamespace(), gvr.Resource, utd.GetName(), err)
realTimeJson := errorRealTime(utd, err.Error())
data = append(data, realTimeJson)
continue
}
utdRealTime.SetManagedFields(nil)
realTimeJson, err := utdRealTime.MarshalJSON()
if err != nil {
klog.Errorf("MarshalJSON: %s", err.Error())
return nil, err
}
data = append(data, realTimeJson)
}
return data, nil
}
func errorRealTime(utd *unstructured.Unstructured, msg string) json.RawMessage {
fake := &unstructured.Unstructured{}
fake.SetKind(utd.GetKind())
fake.SetAPIVersion(utd.GetAPIVersion())
fake.SetName(utd.GetName())
fake.SetNamespace(utd.GetNamespace())
fake.SetLabels(utd.GetLabels())
fake.SetAnnotations(utd.GetAnnotations())
unstructured.SetNestedField(fake.Object, msg, "status", "state")
marshalJSON, _ := fake.MarshalJSON()
return marshalJSON
}
func (h *appHandler) DeleteAppRls(req *restful.Request, resp *restful.Response) {
applicationId := req.PathParameter("application")
app := &appv2.ApplicationRelease{}
app.Name = applicationId
err := h.client.Delete(req.Request.Context(), app)
if requestDone(err, resp) {
return
}
resp.WriteEntity(errors.None)
}
func (h *appHandler) ListAppRls(req *restful.Request, resp *restful.Response) {
labelValues := map[string]string{
appv2.AppIDLabelKey: req.QueryParameter("appID"),
constants.NamespaceLabelKey: req.PathParameter("namespace"),
constants.WorkspaceLabelKey: req.PathParameter("workspace"),
constants.ClusterNameLabelKey: req.PathParameter("cluster"),
}
labelSet := map[string]string{}
for key, value := range labelValues {
if value != "" {
labelSet[key] = value
}
}
opt := runtimeclient.ListOptions{}
labelSelectorStr := req.QueryParameter("labelSelector")
labelSelector, err := labels.Parse(labelSelectorStr)
if err != nil {
api.HandleBadRequest(resp, nil, err)
return
}
for k, v := range labelSet {
routeLabel, _ := labels.NewRequirement(k, selection.Equals, []string{v})
labelSelector = labelSelector.Add(*routeLabel)
}
opt.LabelSelector = labelSelector
appList := appv2.ApplicationReleaseList{}
err = h.client.List(req.Request.Context(), &appList, &opt)
if requestDone(err, resp) {
return
}
resp.WriteEntity(convertToListResult(&appList, req))
}

View File

@@ -0,0 +1,339 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"bytes"
"fmt"
"strconv"
"strings"
"github.com/emicklei/go-restful/v3"
"golang.org/x/net/context"
"helm.sh/helm/v3/pkg/chart/loader"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/klog/v2"
appv2 "kubesphere.io/api/application/v2"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/server/errors"
"kubesphere.io/kubesphere/pkg/server/params"
"kubesphere.io/kubesphere/pkg/simple/client/application"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
"kubesphere.io/kubesphere/pkg/utils/stringutils"
)
func (h *appHandler) CreateOrUpdateAppVersion(req *restful.Request, resp *restful.Response) {
createAppVersionRequest, err := parseUpload(req)
if requestDone(err, resp) {
return
}
if h.ossStore == nil && len(createAppVersionRequest.Package) > maxFileSize {
api.HandleBadRequest(resp, nil, fmt.Errorf("System has no OSS store, the maximum file size is %d", maxFileSize))
return
}
createAppVersionRequest.AppName = req.PathParameter("app")
vRequest, err := parseRequest(createAppVersionRequest)
if requestDone(err, resp) {
return
}
data := map[string]any{
"icon": vRequest.Icon,
"appName": vRequest.AppName,
"versionName": vRequest.VersionName,
"appHome": vRequest.AppHome,
"description": vRequest.Description,
"aliasName": vRequest.AliasName,
}
validate, _ := strconv.ParseBool(req.QueryParameter("validate"))
if validate {
resp.WriteAsJson(data)
return
}
appVersion := &appv2.ApplicationVersion{}
vRequest.VersionName = application.FormatVersion(vRequest.VersionName)
appVersion.Name = fmt.Sprintf("%s-%s", createAppVersionRequest.AppName, vRequest.VersionName)
if h.conflictedDone(req, resp, "version", appVersion) {
return
}
app := appv2.Application{}
err = h.client.Get(req.Request.Context(), runtimeclient.ObjectKey{Name: createAppVersionRequest.AppName}, &app)
if requestDone(err, resp) {
return
}
err = application.CreateOrUpdateAppVersion(req.Request.Context(), h.client, app, vRequest, h.cmStore, h.ossStore)
if requestDone(err, resp) {
return
}
err = application.UpdateLatestAppVersion(req.Request.Context(), h.client, app)
if err != nil {
klog.Errorf("failed to update latest app version, err:%v", err)
api.HandleInternalError(resp, nil, err)
return
}
resp.WriteAsJson(data)
}
func (h *appHandler) DeleteAppVersion(req *restful.Request, resp *restful.Response) {
versionId := req.PathParameter("version")
err := h.client.Delete(req.Request.Context(), &appv2.ApplicationVersion{ObjectMeta: metav1.ObjectMeta{Name: versionId}})
if requestDone(err, resp) {
return
}
resp.WriteEntity(errors.None)
}
func (h *appHandler) DescribeAppVersion(req *restful.Request, resp *restful.Response) {
versionId := req.PathParameter("version")
result := &appv2.ApplicationVersion{}
err := h.client.Get(req.Request.Context(), runtimeclient.ObjectKey{Name: versionId}, result)
if requestDone(err, resp) {
return
}
result.SetManagedFields(nil)
resp.WriteEntity(result)
}
func (h *appHandler) ListAppVersions(req *restful.Request, resp *restful.Response) {
var err error
workspace := req.PathParameter("workspace")
opt := runtimeclient.ListOptions{}
labelSelectorStr := req.QueryParameter("labelSelector")
labelSelector, err := labels.Parse(labelSelectorStr)
if err != nil {
api.HandleBadRequest(resp, nil, err)
return
}
vals := []string{req.PathParameter("app")}
appRequirement, err := labels.NewRequirement(appv2.AppIDLabelKey, selection.Equals, vals)
if err != nil {
api.HandleBadRequest(resp, nil, err)
return
}
labelSelector = labelSelector.Add(*appRequirement)
opt.LabelSelector = labelSelector
result := appv2.ApplicationVersionList{}
err = h.client.List(req.Request.Context(), &result, &opt)
if requestDone(err, resp) {
return
}
conditions, err := params.ParseConditions(req)
if requestDone(err, resp) {
return
}
filtered := appv2.ApplicationVersionList{}
for _, appv := range result.Items {
states := strings.Split(conditions.Match[Status], "|")
if conditions.Match[Status] != "" && !sliceutil.HasString(states, appv.Status.State) {
continue
}
allowList := []string{appv2.SystemWorkspace, workspace}
if workspace != "" && !stringutils.StringIn(appv.Labels[constants.WorkspaceLabelKey], allowList) {
continue
}
filtered.Items = append(filtered.Items, appv)
}
resp.WriteEntity(convertToListResult(&filtered, req))
}
func (h *appHandler) GetAppVersionPackage(req *restful.Request, resp *restful.Response) {
versionId := req.PathParameter("version")
app := req.PathParameter("app")
data, err := application.FailOverGet(h.cmStore, h.ossStore, versionId, h.client, true)
if err != nil {
klog.Errorf("get app version %s failed, error: %s", versionId, err)
api.HandleInternalError(resp, nil, err)
return
}
result := map[string]any{
"versionID": versionId,
"package": data,
"appID": app,
}
resp.WriteAsJson(result)
}
func (h *appHandler) GetAppVersionFiles(req *restful.Request, resp *restful.Response) {
versionId := req.PathParameter("version")
data, err := application.FailOverGet(h.cmStore, h.ossStore, versionId, h.client, true)
if err != nil {
klog.Errorf("get app version %s failed, error: %s", versionId, err)
api.HandleInternalError(resp, nil, err)
return
}
var result = make(map[string][]byte)
chartData, err := loader.LoadArchive(bytes.NewReader(data))
if err != nil {
result["all.yaml"] = data
resp.WriteAsJson(result)
return
}
for _, f := range chartData.Raw {
result[f.Name] = f.Data
}
resp.WriteAsJson(result)
}
func (h *appHandler) AppVersionAction(req *restful.Request, resp *restful.Response) {
versionID := req.PathParameter("version")
var doActionRequest appv2.ApplicationVersionStatus
err := req.ReadEntity(&doActionRequest)
if requestDone(err, resp) {
return
}
ctx := req.Request.Context()
appVersion := &appv2.ApplicationVersion{}
err = h.client.Get(ctx, runtimeclient.ObjectKey{Name: versionID}, appVersion)
if requestDone(err, resp) {
return
}
if doActionRequest.Message == "" {
doActionRequest.Message = appVersion.Status.Message
}
// app version check state draft -> submitted -> (rejected -> submitted ) -> passed-> active -> (suspended -> active), draft -> submitted -> active
err = DoAppVersionAction(ctx, versionID, doActionRequest, h.client)
if requestDone(err, resp) {
return
}
resp.WriteEntity(errors.None)
}
func DoAppVersionAction(ctx context.Context, versionId string, actionReq appv2.ApplicationVersionStatus, client runtimeclient.Client) error {
key := runtimeclient.ObjectKey{Name: versionId}
version := &appv2.ApplicationVersion{}
err := client.Get(ctx, key, version)
if err != nil {
klog.Errorf("get app version %s failed, error: %s", versionId, err)
return err
}
version.Status.State = actionReq.State
if actionReq.Message != "" {
version.Status.Message = actionReq.Message
}
if actionReq.UserName != "" {
version.Status.UserName = actionReq.UserName
}
version.Status.Updated = &metav1.Time{Time: metav1.Now().Time}
err = client.Status().Update(ctx, version)
if err != nil {
klog.Errorf("update app version %s failed, error: %s", versionId, err)
}
appID := version.Labels[appv2.AppIDLabelKey]
err = checkAppStatus(ctx, appID, actionReq.State, client)
if err != nil {
klog.Errorf("check app failed, error: %s", err)
return err
}
return err
}
func checkAppStatus(ctx context.Context, appID, action string, client runtimeclient.Client) error {
//If all appVersions are Suspended, then the app's status is not active
if action != appv2.ReviewStatusSuspended {
return nil
}
allVersion := appv2.ApplicationVersionList{}
opt := runtimeclient.ListOptions{
LabelSelector: labels.SelectorFromSet(labels.Set{appv2.AppIDLabelKey: appID}),
}
err := client.List(ctx, &allVersion, &opt)
if err != nil {
klog.Errorf("list app versions failed, error: %s", err)
return err
}
active := false
for _, v := range allVersion.Items {
if v.Status.State == appv2.ReviewStatusActive {
active = true
break
}
}
app := appv2.Application{}
err = client.Get(ctx, runtimeclient.ObjectKey{Name: appID}, &app)
if err != nil {
klog.Errorf("get app %s failed, error: %s", appID, err)
return err
}
if !active && app.Status.State == appv2.ReviewStatusActive {
app.Status.State = appv2.ReviewStatusDraft
err = client.Status().Update(ctx, &app)
if err != nil {
klog.Errorf("update app %s failed, error: %s", appID, err)
return err
}
}
return err
}
func (h *appHandler) ListReviews(req *restful.Request, resp *restful.Response) {
conditions, err := params.ParseConditions(req)
if requestDone(err, resp) {
return
}
appVersions := appv2.ApplicationVersionList{}
opts := runtimeclient.ListOptions{
LabelSelector: labels.SelectorFromSet(labels.Set{appv2.RepoIDLabelKey: appv2.UploadRepoKey}),
}
err = h.client.List(req.Request.Context(), &appVersions, &opts)
if requestDone(err, resp) {
return
}
if conditions == nil || len(conditions.Match) == 0 {
resp.WriteEntity(convertToListResult(&appVersions, req))
return
}
filteredAppVersions := appv2.ApplicationVersionList{}
states := strings.Split(conditions.Match[Status], "|")
for _, version := range appVersions.Items {
if conditions.Match[Status] != "" && !sliceutil.HasString(states, version.Status.State) {
continue
}
filteredAppVersions.Items = append(filteredAppVersions.Items, version)
}
resp.WriteEntity(convertToListResult(&filteredAppVersions, req))
}

View File

@@ -0,0 +1,103 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"fmt"
"github.com/emicklei/go-restful/v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
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/api"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/server/errors"
)
func (h *appHandler) CreateOrUpdateCategory(req *restful.Request, resp *restful.Response) {
createCategoryRequest := &appv2.Category{}
err := req.ReadEntity(createCategoryRequest)
if requestDone(err, resp) {
return
}
category := &appv2.Category{}
category.Name = createCategoryRequest.Name
if h.conflictedDone(req, resp, "category", category) {
return
}
MutateFn := func() error {
if category.GetAnnotations() == nil {
category.SetAnnotations(make(map[string]string))
}
annotations := createCategoryRequest.GetAnnotations()
category.Annotations[constants.DisplayNameAnnotationKey] = annotations[constants.DisplayNameAnnotationKey]
category.Annotations[constants.DescriptionAnnotationKey] = annotations[constants.DescriptionAnnotationKey]
category.Spec.Icon = createCategoryRequest.Spec.Icon
return nil
}
_, err = controllerutil.CreateOrUpdate(req.Request.Context(), h.client, category, MutateFn)
if requestDone(err, resp) {
return
}
resp.WriteAsJson(category)
}
func (h *appHandler) DeleteCategory(req *restful.Request, resp *restful.Response) {
categoryId := req.PathParameter("category")
if categoryId == appv2.UncategorizedCategoryID {
api.HandleBadRequest(resp, req, fmt.Errorf("%s is default Category can't be delete", appv2.UncategorizedCategoryID))
return
}
category := &appv2.Category{}
err := h.client.Get(req.Request.Context(), runtimeclient.ObjectKey{Name: categoryId}, category)
if requestDone(err, resp) {
return
}
if category.Status.Total > 0 {
msg := fmt.Sprintf("can not delete helm category: %s which owns applications", categoryId)
klog.Warningf(msg)
api.HandleInternalError(resp, nil, errors.New(msg))
return
}
err = h.client.Delete(req.Request.Context(), &appv2.Category{ObjectMeta: metav1.ObjectMeta{Name: categoryId}})
if requestDone(err, resp) {
return
}
resp.WriteEntity(errors.None)
}
func (h *appHandler) DescribeCategory(req *restful.Request, resp *restful.Response) {
categoryId := req.PathParameter("category")
result := &appv2.Category{}
err := h.client.Get(req.Request.Context(), runtimeclient.ObjectKey{Name: categoryId}, result)
if requestDone(err, resp) {
return
}
result.SetManagedFields(nil)
resp.WriteEntity(result)
}
func (h *appHandler) ListCategories(req *restful.Request, resp *restful.Response) {
cList := &appv2.CategoryList{}
err := h.client.List(req.Request.Context(), cList)
if requestDone(err, resp) {
return
}
resp.WriteEntity(convertToListResult(cList, req))
}

View File

@@ -0,0 +1,167 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"fmt"
"net/url"
"kubesphere.io/kubesphere/pkg/simple/client/application"
"kubesphere.io/kubesphere/pkg/api"
"github.com/emicklei/go-restful/v3"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/klog/v2"
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/server/errors"
"kubesphere.io/kubesphere/pkg/utils/stringutils"
)
func (h *appHandler) CreateOrUpdateRepo(req *restful.Request, resp *restful.Response) {
repoRequest := &appv2.Repo{}
err := req.ReadEntity(repoRequest)
if requestDone(err, resp) {
return
}
if repoRequest.Name == appv2.UploadRepoKey {
api.HandleBadRequest(resp, req, fmt.Errorf("repo name %s is not allowed", appv2.UploadRepoKey))
return
}
repoId := req.PathParameter("repo")
if repoId == "" {
repoId = repoRequest.Name
}
parsedUrl, err := url.Parse(repoRequest.Spec.Url)
if requestDone(err, resp) {
return
}
if parsedUrl.User != nil {
repoRequest.Spec.Credential.Username = parsedUrl.User.Username()
repoRequest.Spec.Credential.Password, _ = parsedUrl.User.Password()
}
_, err = application.LoadRepoIndex(repoRequest.Spec.Url, repoRequest.Spec.Credential)
if requestDone(err, resp) {
return
}
if req.QueryParameter("validate") != "" {
data := map[string]any{"ok": true}
resp.WriteAsJson(data)
return
}
repo := &appv2.Repo{}
repo.Name = repoId
if h.conflictedDone(req, resp, "repo", repo) {
return
}
mutateFn := func() error {
repo.Spec = appv2.RepoSpec{
Url: parsedUrl.String(),
SyncPeriod: repoRequest.Spec.SyncPeriod,
Description: stringutils.ShortenString(repoRequest.Spec.Description, 512),
}
if parsedUrl.User != nil {
repo.Spec.Credential.Username = parsedUrl.User.Username()
repo.Spec.Credential.Password, _ = parsedUrl.User.Password()
}
if repo.GetLabels() == nil {
repo.SetLabels(map[string]string{})
}
repo.Labels[constants.WorkspaceLabelKey] = repoRequest.Labels[constants.WorkspaceLabelKey]
if repo.GetAnnotations() == nil {
repo.SetAnnotations(map[string]string{})
}
ant := repoRequest.GetAnnotations()
repo.Annotations[constants.DisplayNameAnnotationKey] = ant[constants.DisplayNameAnnotationKey]
return nil
}
_, err = controllerutil.CreateOrUpdate(req.Request.Context(), h.client, repo, mutateFn)
if requestDone(err, resp) {
return
}
data := map[string]interface{}{"repo_id": repoId}
resp.WriteAsJson(data)
}
func (h *appHandler) DeleteRepo(req *restful.Request, resp *restful.Response) {
repoId := req.PathParameter("repo")
err := h.client.Delete(req.Request.Context(), &appv2.Repo{ObjectMeta: metav1.ObjectMeta{Name: repoId}})
if requestDone(err, resp) {
return
}
klog.V(4).Info("delete repo: ", repoId)
resp.WriteEntity(errors.None)
}
func (h *appHandler) DescribeRepo(req *restful.Request, resp *restful.Response) {
repoId := req.PathParameter("repo")
key := runtimeclient.ObjectKey{Name: repoId}
repo := &appv2.Repo{}
err := h.client.Get(req.Request.Context(), key, repo)
if requestDone(err, resp) {
return
}
repo.SetManagedFields(nil)
resp.WriteEntity(repo)
}
func (h *appHandler) ListRepos(req *restful.Request, resp *restful.Response) {
helmRepoList := &appv2.RepoList{}
err := h.client.List(req.Request.Context(), helmRepoList)
if requestDone(err, resp) {
return
}
workspace := req.PathParameter("workspace")
filteredList := &appv2.RepoList{}
for _, repo := range helmRepoList.Items {
allowList := []string{appv2.SystemWorkspace, workspace}
if !stringutils.StringIn(repo.Labels[constants.WorkspaceLabelKey], allowList) {
continue
}
filteredList.Items = append(filteredList.Items, repo)
}
resp.WriteEntity(convertToListResult(filteredList, req))
}
func (h *appHandler) ListRepoEvents(req *restful.Request, resp *restful.Response) {
repoId := req.PathParameter("repo")
list := v1.EventList{}
selector := fields.SelectorFromSet(fields.Set{
"involvedObject.name": repoId,
})
opt := &runtimeclient.ListOptions{FieldSelector: selector}
err := h.client.List(req.Request.Context(), &list, opt)
if requestDone(err, resp) {
return
}
resp.WriteEntity(convertToListResult(&list, req))
}

View File

@@ -0,0 +1,151 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/labels"
"github.com/emicklei/go-restful/v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
appv2 "kubesphere.io/api/application/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"kubesphere.io/kubesphere/pkg/api"
)
func (h *appHandler) AppCrList(req *restful.Request, resp *restful.Response) {
clusterName := req.QueryParameter("cluster")
gvr := schema.GroupVersionResource{
Group: req.QueryParameter("group"),
Version: req.QueryParameter("version"),
Resource: req.QueryParameter("resource"),
}
opts := metav1.ListOptions{}
labelSelectorStr := req.QueryParameter("labelSelector")
labelSelector, err := labels.Parse(labelSelectorStr)
if err != nil {
api.HandleBadRequest(resp, nil, err)
return
}
opts.LabelSelector = labelSelector.String()
_, dynamicClient, _, err := h.getCluster(clusterName)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
list, err := dynamicClient.Resource(gvr).List(context.Background(), opts)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
resp.WriteEntity(convertToListResult(list, req))
}
func checkPermissions(gvr schema.GroupVersionResource, app appv2.Application) (allow bool) {
for _, i := range app.Spec.Resources {
if gvr.Resource == i.Resource {
allow = true
break
}
}
return allow
}
func (h *appHandler) CreateOrUpdateCR(req *restful.Request, resp *restful.Response) {
gvr := schema.GroupVersionResource{
Group: req.QueryParameter("group"),
Version: req.QueryParameter("version"),
Resource: req.QueryParameter("resource"),
}
appID := req.QueryParameter("app")
app := appv2.Application{}
err := h.client.Get(req.Request.Context(), client.ObjectKey{Name: appID}, &app)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
allow := checkPermissions(gvr, app)
if !allow {
api.HandleForbidden(resp, nil, fmt.Errorf("resource %s not allow", gvr.Resource))
return
}
obj := unstructured.Unstructured{}
err = req.ReadEntity(&obj)
if err != nil {
api.HandleBadRequest(resp, nil, err)
return
}
js, err := obj.MarshalJSON()
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
clusterName := req.QueryParameter("cluster")
_, dynamicClient, _, err := h.getCluster(clusterName)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
opt := metav1.PatchOptions{FieldManager: "applicationOperator"}
_, err = dynamicClient.Resource(gvr).
Namespace(obj.GetNamespace()).
Patch(context.TODO(), obj.GetName(), types.ApplyPatchType, js, opt)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
}
func (h *appHandler) DescribeAppCr(req *restful.Request, resp *restful.Response) {
name := req.PathParameter("crname")
namespace := req.QueryParameter("namespace")
gvr := schema.GroupVersionResource{
Group: req.QueryParameter("group"),
Version: req.QueryParameter("version"),
Resource: req.QueryParameter("resource"),
}
clusterName := req.QueryParameter("cluster")
_, dynamicClient, _, err := h.getCluster(clusterName)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
get, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{})
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
get.SetManagedFields(nil)
resp.WriteEntity(get)
}
func (h *appHandler) DeleteAppCr(req *restful.Request, resp *restful.Response) {
name := req.PathParameter("crname")
namespace := req.QueryParameter("namespace")
gvr := schema.GroupVersionResource{
Group: req.QueryParameter("group"),
Version: req.QueryParameter("version"),
Resource: req.QueryParameter("resource"),
}
clusterName := req.QueryParameter("cluster")
_, dynamicClient, _, err := h.getCluster(clusterName)
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
err = dynamicClient.Resource(gvr).Namespace(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
if err != nil {
api.HandleInternalError(resp, nil, err)
return
}
}

View File

@@ -0,0 +1,136 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"fmt"
"k8s.io/klog/v2"
"kubesphere.io/kubesphere/pkg/simple/client/application"
"kubesphere.io/utils/s3"
"github.com/emicklei/go-restful/v3"
appv2 "kubesphere.io/api/application/v2"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"kubesphere.io/kubesphere/pkg/apiserver/rest"
"kubesphere.io/kubesphere/pkg/apiserver/runtime"
"kubesphere.io/kubesphere/pkg/utils/clusterclient"
)
type appHandler struct {
client runtimeclient.Client
clusterClient clusterclient.Interface
s3opts *s3.Options
ossStore s3.Interface
cmStore s3.Interface
}
func NewHandler(cacheClient runtimeclient.Client, clusterClient clusterclient.Interface, s3opts *s3.Options) rest.Handler {
handler := &appHandler{
client: cacheClient,
clusterClient: clusterClient,
s3opts: s3opts,
}
return handler
}
func NewFakeHandler() rest.Handler {
return &appHandler{}
}
type funcInfo struct {
Route string
Func func(req *restful.Request, resp *restful.Response)
Method func(subPath string) *restful.RouteBuilder
Doc string
NeedGlobal bool
Workspace bool
Namespace bool
Params []*restful.Parameter
}
func (h *appHandler) AddToContainer(c *restful.Container) (err error) {
ws := runtime.NewWebService(appv2.SchemeGroupVersion)
if h.cmStore, h.ossStore, err = application.InitStore(h.s3opts, h.client); err != nil {
klog.Errorf("failed to init store: %v", err)
return err
}
funcInfoList := []funcInfo{
{Route: "/repos", Func: h.ListRepos, Method: ws.GET, Workspace: true},
{Route: "/repos", Func: h.CreateOrUpdateRepo, Method: ws.POST, Workspace: true},
{Route: "/repos/{repo}", Func: h.CreateOrUpdateRepo, Method: ws.PATCH, Workspace: true},
{Route: "/repos/{repo}", Func: h.DeleteRepo, Method: ws.DELETE, Workspace: true},
{Route: "/repos/{repo}", Func: h.DescribeRepo, Method: ws.GET, Workspace: true},
{Route: "/repos/{repo}/events", Func: h.ListRepoEvents, Method: ws.GET, Workspace: true},
{Route: "/apps/{app}/action", Func: h.DoAppAction, Method: ws.POST},
{Route: "/apps", Func: h.CreateOrUpdateApp, Method: ws.POST, Workspace: true},
{Route: "/apps", Func: h.ListApps, Method: ws.GET, Workspace: true},
{Route: "/apps/{app}", Func: h.CreateOrUpdateApp, Method: ws.POST, Workspace: true},
{Route: "/apps/{app}", Func: h.PatchApp, Method: ws.PATCH, Workspace: true},
{Route: "/apps/{app}", Func: h.DescribeApp, Method: ws.GET, Workspace: true},
{Route: "/apps/{app}", Func: h.DeleteApp, Method: ws.DELETE, Workspace: true},
{Route: "/apps/{app}/versions", Func: h.CreateOrUpdateAppVersion, Method: ws.POST, Workspace: true},
{Route: "/apps/{app}/versions", Func: h.ListAppVersions, Method: ws.GET, Workspace: true},
{Route: "/apps/{app}/versions/{version}", Func: h.DeleteAppVersion, Method: ws.DELETE, Workspace: true},
{Route: "/apps/{app}/versions/{version}", Func: h.DescribeAppVersion, Method: ws.GET, Workspace: true},
{Route: "/apps/{app}/versions/{version}", Func: h.CreateOrUpdateAppVersion, Method: ws.POST, Workspace: true},
{Route: "/apps/{app}/versions/{version}/package", Func: h.GetAppVersionPackage, Method: ws.GET, Workspace: true},
{Route: "/apps/{app}/versions/{version}/files", Func: h.GetAppVersionFiles, Method: ws.GET, Workspace: true},
{Route: "/apps/{app}/versions/{version}/action", Func: h.AppVersionAction, Method: ws.POST, Workspace: true},
{Route: "/applications", Func: h.ListAppRls, Method: ws.GET, Workspace: true, Namespace: true},
{Route: "/applications", Func: h.CreateOrUpdateAppRls, Method: ws.POST, Workspace: true, Namespace: true},
{Route: "/applications/{application}", Func: h.CreateOrUpdateAppRls, Method: ws.POST, Namespace: true},
{Route: "/applications/{application}", Func: h.DescribeAppRls, Method: ws.GET, Namespace: true},
{Route: "/applications/{application}", Func: h.DeleteAppRls, Method: ws.DELETE, Namespace: true},
{Route: "/categories", Func: h.CreateOrUpdateCategory, Method: ws.POST},
{Route: "/categories", Func: h.ListCategories, Method: ws.GET},
{Route: "/categories/{category}", Func: h.DeleteCategory, Method: ws.DELETE},
{Route: "/categories/{category}", Func: h.CreateOrUpdateCategory, Method: ws.POST},
{Route: "/categories/{category}", Func: h.DescribeCategory, Method: ws.GET},
{Route: "/reviews", Func: h.ListReviews, Method: ws.GET},
{Route: "/attachments", Func: h.CreateAttachment, Method: ws.POST, Workspace: true},
{Route: "/attachments/{attachment}", Func: h.DescribeAttachment, Method: ws.GET, Workspace: true},
{Route: "/attachments/{attachment}", Func: h.DeleteAttachments, Method: ws.DELETE, Workspace: true},
{Route: "/apps/{app}/examplecr/{name}", Func: h.exampleCr, Method: ws.GET, Workspace: true},
{Route: "/apps/{app}/cr", Func: h.AppCrList, Method: ws.GET, Workspace: true, Namespace: true},
{Route: "/cr", Func: h.CreateOrUpdateCR, Method: ws.POST, Workspace: true, Namespace: true},
{Route: "/cr/{crname}", Func: h.DescribeAppCr, Method: ws.GET, Workspace: true, Namespace: true},
{Route: "/cr/{crname}", Func: h.DeleteAppCr, Method: ws.DELETE, Workspace: true, Namespace: true},
}
for _, info := range funcInfoList {
builder := info.Method(info.Route).To(info.Func).Doc(info.Doc)
for _, param := range info.Params {
builder = builder.Param(param)
}
ws.Route(builder)
if info.Workspace {
workspaceRoute := fmt.Sprintf("/workspaces/{workspace}%s", info.Route)
builder = info.Method(workspaceRoute).To(info.Func).Doc(info.Doc)
for _, param := range info.Params {
builder = builder.Param(param)
}
builder.Param(ws.PathParameter("workspace", "workspace"))
ws.Route(builder)
}
if info.Namespace {
namespaceRoute := fmt.Sprintf("/namespaces/{namespace}%s", info.Route)
builder = info.Method(namespaceRoute).To(info.Func).Doc(info.Doc)
for _, param := range info.Params {
builder = builder.Param(param)
}
builder.Param(ws.PathParameter("namespace", "namespace"))
ws.Route(builder)
}
}
c.Add(ws)
return nil
}

View File

@@ -0,0 +1,166 @@
/*
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package v2
import (
"bufio"
"bytes"
"errors"
"io"
"strings"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/klog/v2"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"k8s.io/client-go/dynamic"
appv2 "kubesphere.io/api/application/v2"
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"kubesphere.io/kubesphere/pkg/simple/client/application"
)
const (
Status = "status"
)
func parseRequest(createRequest application.AppRequest) (appRequest application.AppRequest, err error) {
if createRequest.AppType == appv2.AppTypeHelm {
req, err := parseHelmRequest(createRequest)
return req, err
}
_, err = application.ReadYaml(createRequest.Package)
return createRequest, err
}
func parseHelmRequest(createRequest application.AppRequest) (req application.AppRequest, err error) {
if createRequest.Package == nil || len(createRequest.Package) == 0 {
return req, errors.New("package is empty")
}
chartPack, err := loader.LoadArchive(bytes.NewReader(createRequest.Package))
if err != nil {
return createRequest, err
}
crdFiles := chartPack.CRDObjects()
for _, i := range crdFiles {
dataList, err := readYaml(i.File.Data)
if err != nil {
klog.Errorf("failed to read %s yaml: %v", i.Filename, err)
return createRequest, err
}
for _, d := range dataList {
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(d), 1024)
crd := v1.CustomResourceDefinition{}
err = decoder.Decode(&crd)
if err != nil {
klog.Error(err, "Failed to decode crd file")
return application.AppRequest{}, err
}
var servedVersion *v1.CustomResourceDefinitionVersion
for _, v := range crd.Spec.Versions {
if v.Served && v.Storage {
servedVersion = &v
}
}
if servedVersion == nil {
klog.Warningf("no served and storage version found in crd %s", crd.Name)
continue
}
ins := appv2.GroupVersionResource{
Group: crd.Spec.Group,
Version: servedVersion.Name,
Resource: crd.Spec.Names.Plural,
}
createRequest.Resources = append(createRequest.Resources, ins)
}
}
shortName := application.GenerateShortNameMD5Hash(chartPack.Metadata.Name)
fillEmptyFields(&createRequest, chartPack, shortName)
return createRequest, nil
}
func readYaml(data []byte) (yamlList [][]byte, err error) {
// Read yaml file which has multi line yaml and split it into a list of yaml documents.
r := yaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data)))
for {
d, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
klog.Errorf("failed to read yaml: %v", err)
return nil, err
}
//skip empty yaml or empty ---
if len(strings.TrimSpace(string(d))) > 3 {
yamlList = append(yamlList, d)
}
}
return yamlList, nil
}
func fillEmptyFields(createRequest *application.AppRequest, chartPack *chart.Chart, shortName string) {
if createRequest.AppName == "" {
createRequest.AppName = application.GetUuid36(shortName + "-")
}
if createRequest.OriginalName == "" {
createRequest.OriginalName = chartPack.Metadata.Name
}
if createRequest.AppHome == "" {
createRequest.AppHome = chartPack.Metadata.Home
}
if createRequest.Icon == "" {
createRequest.Icon = chartPack.Metadata.Icon
}
if createRequest.VersionName == "" {
createRequest.VersionName = chartPack.Metadata.Version
}
if createRequest.Maintainers == nil || len(createRequest.Maintainers) == 0 {
createRequest.Maintainers = application.GetMaintainers(chartPack.Metadata.Maintainers)
}
if createRequest.RepoName == "" {
createRequest.RepoName = appv2.UploadRepoKey
}
if createRequest.Description == "" {
createRequest.Description = chartPack.Metadata.Description
}
if createRequest.Abstraction == "" {
createRequest.Abstraction = chartPack.Metadata.Description
}
if createRequest.AliasName == "" {
createRequest.AliasName = chartPack.Metadata.Name
}
}
func (h *appHandler) getCluster(clusterName string) (runtimeclient.Client, *dynamic.DynamicClient, *clusterv1alpha1.Cluster, error) {
klog.Infof("get cluster %s", clusterName)
runtimeClient, err := h.clusterClient.GetRuntimeClient(clusterName)
if err != nil {
return nil, nil, nil, err
}
clusterClient, err := h.clusterClient.GetClusterClient(clusterName)
if err != nil {
return nil, nil, nil, err
}
dynamicClient, err := dynamic.NewForConfig(clusterClient.RestConfig)
if err != nil {
return nil, nil, nil, err
}
cluster, err := h.clusterClient.Get(clusterName)
if err != nil {
return nil, nil, nil, err
}
return runtimeClient, dynamicClient, cluster, nil
}