diff --git a/src/backend/common/query.go b/src/backend/common/query.go index 7c46b33bf..6fc383972 100644 --- a/src/backend/common/query.go +++ b/src/backend/common/query.go @@ -4,13 +4,17 @@ type QueryParam struct { PageNo int64 `json:"pageNo"` PageSize int64 `json:"pageSize"` Query map[string]interface{} `json:"query"` - Sortby []string `json:"sortby"` + Sortby string `json:"sortby"` Groupby []string `json:"groupby"` Relate string `json:"relate"` } func (q *QueryParam) Offset() int64 { - return (q.PageNo - 1) * q.PageSize + offset := (q.PageNo - 1) * q.PageSize + if offset < 0 { + offset = 0 + } + return offset } func (q *QueryParam) Limit() int64 { diff --git a/src/backend/controllers/base/api.go b/src/backend/controllers/base/api.go index e42a90fe2..72a303fd5 100644 --- a/src/backend/controllers/base/api.go +++ b/src/backend/controllers/base/api.go @@ -1,52 +1,15 @@ package base import ( - "encoding/json" "fmt" "net/http" "strconv" - "strings" - "github.com/Qihoo360/wayne/src/backend/common" "github.com/Qihoo360/wayne/src/backend/models" - "github.com/Qihoo360/wayne/src/backend/util/hack" "github.com/Qihoo360/wayne/src/backend/util/logs" - "github.com/Qihoo360/wayne/src/backend/util/slice" "github.com/astaxie/beego/orm" - "github.com/go-sql-driver/mysql" - "k8s.io/apimachinery/pkg/api/errors" ) -const ( - defaultPageNo = 1 - defaultPageSize = 10 -) - -type Result struct { - Data interface{} `json:"data"` -} - -type ErrorSubCode int - -const ( - ErrorSubCodeInsufficientResource = 403001 -) - -var _ error = &ErrorResult{} - -// Error implements the Error interface. -func (e *ErrorResult) Error() string { - return fmt.Sprintf("code:%d,subCode:%d,msg:%s", e.Code, e.SubCode, e.Msg) -} - -type ErrorResult struct { - // http code - Code int `json:"code"` - // The custom code - SubCode int `json:"subCode"` - Msg string `json:"msg"` -} - type APIController struct { LoggedInController @@ -117,239 +80,38 @@ func (c *APIController) CheckPermission(perType string, perAction string) { func (c *APIController) Success(data interface{}) { c.publishRequestMessage(http.StatusOK, data) - c.Ctx.Output.SetStatus(http.StatusOK) - c.Data["json"] = Result{Data: data} - c.ServeJSON() + c.ResultHandlerController.Success(data) } // Abort stops controller handler and show the error data, e.g. Prepare func (c *APIController) AbortForbidden(msg string) { c.publishRequestMessage(http.StatusForbidden, msg) - logs.Info("Abort Forbidden error. %s", msg) - c.CustomAbort(http.StatusForbidden, hack.String(c.ErrorResult(http.StatusForbidden, msg))) + c.ResultHandlerController.AbortForbidden(msg) } func (c *APIController) AbortInternalServerError(msg string) { c.publishRequestMessage(http.StatusInternalServerError, msg) - logs.Error("Abort InternalServerError error. %s", msg) - c.CustomAbort(http.StatusInternalServerError, hack.String(c.ErrorResult(http.StatusInternalServerError, msg))) + c.ResultHandlerController.AbortInternalServerError(msg) } func (c *APIController) AbortBadRequest(msg string) { c.publishRequestMessage(http.StatusBadRequest, msg) - logs.Info("Abort BadRequest error. %s", msg) - c.CustomAbort(http.StatusBadRequest, hack.String(c.ErrorResult(http.StatusBadRequest, msg))) -} - -// format BadRequest with param name. -func (c *APIController) AbortBadRequestFormat(paramName string) { - msg := fmt.Sprintf("Invalid param %s !", paramName) - c.AbortBadRequest(msg) + c.ResultHandlerController.AbortBadRequest(msg) } func (c *APIController) AbortUnauthorized(msg string) { c.publishRequestMessage(http.StatusUnauthorized, msg) - logs.Info("Abort Unauthorized error. %s", msg) - c.CustomAbort(http.StatusUnauthorized, hack.String(c.ErrorResult(http.StatusUnauthorized, msg))) - -} + c.ResultHandlerController.AbortUnauthorized(msg) -func (c *APIController) ErrorResult(code int, msg string) []byte { - errorResult := ErrorResult{ - Code: code, - Msg: msg, - } - body, err := json.Marshal(errorResult) - if err != nil { - logs.Error("Json Marshal error. %v", err) - c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - return body } // Handle return http code and body normally, need return func (c *APIController) HandleError(err error) { - errorResult := &ErrorResult{ - Code: http.StatusInternalServerError, - } - switch e := err.(type) { - // deal with kubernetes errors - case errors.APIStatus: - errorResult.Code = int(e.Status().Code) - errorResult.SubCode = errorResult.Code - errorResult.Msg = e.Status().Message - case *mysql.MySQLError: - errorResult.Code = http.StatusBadRequest - errorResult.SubCode = int(e.Number) - errorResult.Msg = e.Message - case *ErrorResult: - errorResult = e - default: - if err == orm.ErrNoRows { - errorResult.Code = http.StatusNotFound - } - errorResult.SubCode = errorResult.Code - errorResult.Msg = http.StatusText(errorResult.Code) - } - - if errorResult.Code >= http.StatusInternalServerError { - logs.Error("System error. %v", err) - } else { - logs.Info("Info error. %v", err) - } - - c.Ctx.Output.SetStatus(errorResult.Code) - - body, err := json.Marshal(errorResult) - if err != nil { - logs.Error("Json Marshal error. %v", err) - c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - c.Ctx.Output.Body(body) - - c.publishRequestMessage(errorResult.Code, err) -} - -// TODO: 需要重构成独立的Controller,参考Django的generic views设计 -func (c *APIController) BuildQueryParam() *common.QueryParam { - pageNo := c.Input().Get("pageNo") - pageSize := c.Input().Get("pageSize") - if pageNo == "" { - pageNo = strconv.Itoa(defaultPageNo) - } - - if pageSize == "" { - pageSize = strconv.Itoa(defaultPageSize) - } - - no, err := strconv.ParseInt(pageNo, 10, 64) - if err != nil { - c.AbortBadRequest("Invalid pageNo in query.") - } - size, err := strconv.ParseInt(pageSize, 10, 64) - if err != nil { - c.AbortBadRequest("Invalid pageSize in query.") - } - - qmap := map[string]interface{}{} - deletedStr := c.Input().Get("deleted") - if deletedStr != "" { - deleted, err := strconv.ParseBool(deletedStr) - if err != nil { - c.AbortBadRequest("Invalid deleted in query.") - } - qmap["deleted"] = deleted - } - - filter := c.Input().Get("filter") - if filter != "" { - filters := strings.Split(filter, ",") - for _, param := range filters { - params := strings.Split(param, "=") - if len(params) != 2 { - continue - } - key, value := params[0], params[1] - // 兼容在filter中使用deleted参数 - if key == "deleted" { - deleted, err := strconv.ParseBool(value) - if err != nil { - continue - } - qmap[key] = deleted - continue - } - qmap[params[0]] = params[1] - } - } - - relate := "" - if c.Input().Get("relate") != "" { - relate = c.Input().Get("relate") - } - - var sortbys []string - sortby := c.Input().Get("sortby") - if sortby != "" { - sortbys = slice.CamelToSnake(strings.Split(sortby, ",")) - } - - return &common.QueryParam{PageNo: no, PageSize: size, Query: qmap, Sortby: sortbys, Relate: relate} -} - -func (c *APIController) GetIDFromURL() int64 { - return c.GetIntParamFromURL(":id") -} - -func (c *APIController) GetIntParamFromURL(param string) int64 { - paramStr := c.Ctx.Input.Param(param) - if len(paramStr) == 0 { - c.AbortBadRequest(fmt.Sprintf("Invalid %s in URL", param)) - } - - paramInt, err := strconv.ParseInt(paramStr, 10, 64) - if err != nil || paramInt < 0 { - c.AbortBadRequest(fmt.Sprintf("Invalid %s in URL", param)) - } - - return paramInt -} - -func (c *APIController) GetIntParamFromQuery(param string) int64 { - paramStr := c.Input().Get(param) - if len(paramStr) == 0 { - c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) - } - - paramInt, err := strconv.ParseInt(paramStr, 10, 64) - if err != nil || paramInt < 0 { - c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) - } - - return paramInt -} - -func (c *APIController) GetBoolParamFromQuery(param string) bool { - paramStr := c.Input().Get(param) - if len(paramStr) == 0 { - c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) - } - - paramBool, err := strconv.ParseBool(paramStr) - if err != nil { - c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) - } - - return paramBool -} - -func (c *APIController) GetBoolParamFromQueryWithDefault(param string, defaultValue bool) bool { - paramStr := c.Input().Get(param) - if len(paramStr) == 0 { - return defaultValue - } - - paramBool, err := strconv.ParseBool(paramStr) - if err != nil { - c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) - } - - return paramBool -} - -func (c *APIController) GetDeleteFromQuery() bool { - return c.GetBoolParamFromQueryWithDefault("deleted", false) -} - -func (c *APIController) GetLogicalFromQuery() bool { - return c.GetBoolParamFromQueryWithDefault("logical", true) -} + code := c.ResultHandlerController.HandleError(err) -func (c *APIController) GetIsOnlineFromQuery() bool { - return c.GetBoolParamFromQueryWithDefault("isOnline", false) + c.publishRequestMessage(code, err) } diff --git a/src/backend/controllers/base/const.go b/src/backend/controllers/base/const.go new file mode 100644 index 000000000..f1f27c3e6 --- /dev/null +++ b/src/backend/controllers/base/const.go @@ -0,0 +1,12 @@ +package base + +const ( + defaultPageNo = 1 + defaultPageSize = 10 +) + +type ErrorSubCode int + +const ( + ErrorSubCodeInsufficientResource = 403001 +) diff --git a/src/backend/controllers/base/logged.go b/src/backend/controllers/base/logged.go index 922c292d9..34b03ddc2 100644 --- a/src/backend/controllers/base/logged.go +++ b/src/backend/controllers/base/logged.go @@ -11,7 +11,6 @@ import ( "github.com/Qihoo360/wayne/src/backend/models" "github.com/Qihoo360/wayne/src/backend/util/hack" "github.com/Qihoo360/wayne/src/backend/util/logs" - "github.com/astaxie/beego" "github.com/dgrijalva/jwt-go" ) @@ -25,7 +24,7 @@ var ( ) type LoggedInController struct { - beego.Controller + ParamBuilderController User *models.User } diff --git a/src/backend/controllers/base/parambuilder.go b/src/backend/controllers/base/parambuilder.go new file mode 100644 index 000000000..4545e2529 --- /dev/null +++ b/src/backend/controllers/base/parambuilder.go @@ -0,0 +1,149 @@ +package base + +import ( + "fmt" + "strconv" + "strings" + + "github.com/Qihoo360/wayne/src/backend/common" + "github.com/Qihoo360/wayne/src/backend/util/snaker" +) + +type ParamBuilderController struct { + ResultHandlerController +} + +// TODO: 需要重构成独立的Controller,参考Django的generic views设计 +func (c *ParamBuilderController) BuildQueryParam() *common.QueryParam { + pageNo := c.Input().Get("pageNo") + pageSize := c.Input().Get("pageSize") + if pageNo == "" { + pageNo = strconv.Itoa(defaultPageNo) + } + + if pageSize == "" { + pageSize = strconv.Itoa(defaultPageSize) + } + + no, err := strconv.ParseInt(pageNo, 10, 64) + // pageNo must bigger than zero. + if err != nil || no < 1 { + c.AbortBadRequest("Invalid pageNo in query.") + } + // pageSize must bigger than zero. + size, err := strconv.ParseInt(pageSize, 10, 64) + if err != nil || size < 1 { + c.AbortBadRequest("Invalid pageSize in query.") + } + + qmap := map[string]interface{}{} + deletedStr := c.Input().Get("deleted") + if deletedStr != "" { + deleted, err := strconv.ParseBool(deletedStr) + if err != nil { + c.AbortBadRequest("Invalid deleted in query.") + } + qmap["deleted"] = deleted + } + + filter := c.Input().Get("filter") + if filter != "" { + filters := strings.Split(filter, ",") + for _, param := range filters { + params := strings.Split(param, "=") + if len(params) != 2 { + continue + } + key, value := params[0], params[1] + // 兼容在filter中使用deleted参数 + if key == "deleted" { + deleted, err := strconv.ParseBool(value) + if err != nil { + continue + } + qmap[key] = deleted + continue + } + qmap[params[0]] = params[1] + } + } + + relate := "" + if c.Input().Get("relate") != "" { + relate = c.Input().Get("relate") + } + + return &common.QueryParam{PageNo: no, PageSize: size, Query: qmap, Sortby: snaker.CamelToSnake(c.Input().Get("sortby")), Relate: relate} +} + +func (c *ParamBuilderController) GetIDFromURL() int64 { + return c.GetIntParamFromURL(":id") +} + +func (c *ParamBuilderController) GetIntParamFromURL(param string) int64 { + paramStr := c.Ctx.Input.Param(param) + if len(paramStr) == 0 { + c.AbortBadRequest(fmt.Sprintf("Invalid %s in URL", param)) + } + + paramInt, err := strconv.ParseInt(paramStr, 10, 64) + if err != nil || paramInt < 0 { + c.AbortBadRequest(fmt.Sprintf("Invalid %s in URL", param)) + } + + return paramInt +} + +func (c *ParamBuilderController) GetIntParamFromQuery(param string) int64 { + paramStr := c.Input().Get(param) + if len(paramStr) == 0 { + c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) + } + + paramInt, err := strconv.ParseInt(paramStr, 10, 64) + if err != nil || paramInt < 0 { + c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) + } + + return paramInt +} + +func (c *ParamBuilderController) GetBoolParamFromQuery(param string) bool { + paramStr := c.Input().Get(param) + if len(paramStr) == 0 { + c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) + } + + paramBool, err := strconv.ParseBool(paramStr) + if err != nil { + c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) + } + + return paramBool +} + +func (c *ParamBuilderController) GetBoolParamFromQueryWithDefault(param string, defaultValue bool) bool { + paramStr := c.Input().Get(param) + if len(paramStr) == 0 { + return defaultValue + } + + paramBool, err := strconv.ParseBool(paramStr) + if err != nil { + c.AbortBadRequest(fmt.Sprintf("Invalid %s in Query", param)) + } + + return paramBool +} + +func (c *ParamBuilderController) GetDeleteFromQuery() bool { + return c.GetBoolParamFromQueryWithDefault("deleted", false) +} + +func (c *ParamBuilderController) GetLogicalFromQuery() bool { + return c.GetBoolParamFromQueryWithDefault("logical", true) +} + +func (c *ParamBuilderController) GetIsOnlineFromQuery() bool { + return c.GetBoolParamFromQueryWithDefault("isOnline", false) +} diff --git a/src/backend/controllers/base/resulthandler.go b/src/backend/controllers/base/resulthandler.go new file mode 100644 index 000000000..63ced128a --- /dev/null +++ b/src/backend/controllers/base/resulthandler.go @@ -0,0 +1,133 @@ +package base + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/Qihoo360/wayne/src/backend/util/hack" + "github.com/Qihoo360/wayne/src/backend/util/logs" + "github.com/astaxie/beego" + "github.com/astaxie/beego/orm" + "github.com/go-sql-driver/mysql" + "k8s.io/apimachinery/pkg/api/errors" +) + +type ResultHandlerController struct { + beego.Controller +} + +type Result struct { + Data interface{} `json:"data"` +} + +var _ error = &ErrorResult{} + +// Error implements the Error interface. +func (e *ErrorResult) Error() string { + return fmt.Sprintf("code:%d,subCode:%d,msg:%s", e.Code, e.SubCode, e.Msg) +} + +type ErrorResult struct { + // http code + Code int `json:"code"` + // The custom code + SubCode int `json:"subCode"` + Msg string `json:"msg"` +} + +func (c *ResultHandlerController) Success(data interface{}) { + c.Ctx.Output.SetStatus(http.StatusOK) + c.Data["json"] = Result{Data: data} + c.ServeJSON() +} + +// Abort stops controller handler and show the error data, e.g. Prepare +func (c *ResultHandlerController) AbortForbidden(msg string) { + logs.Info("Abort Forbidden error. %s", msg) + c.CustomAbort(http.StatusForbidden, hack.String(c.errorResult(http.StatusForbidden, msg))) +} + +func (c *ResultHandlerController) AbortInternalServerError(msg string) { + logs.Error("Abort InternalServerError error. %s", msg) + c.CustomAbort(http.StatusInternalServerError, hack.String(c.errorResult(http.StatusInternalServerError, msg))) +} + +func (c *ResultHandlerController) AbortBadRequest(msg string) { + logs.Info("Abort BadRequest error. %s", msg) + c.CustomAbort(http.StatusBadRequest, hack.String(c.errorResult(http.StatusBadRequest, msg))) +} + +// format BadRequest with param name. +func (c *ResultHandlerController) AbortBadRequestFormat(paramName string) { + msg := fmt.Sprintf("Invalid param %s !", paramName) + c.AbortBadRequest(msg) +} + +func (c *ResultHandlerController) AbortUnauthorized(msg string) { + logs.Info("Abort Unauthorized error. %s", msg) + c.CustomAbort(http.StatusUnauthorized, hack.String(c.errorResult(http.StatusUnauthorized, msg))) + +} + +// Handle return http code and body normally, need return +func (c *ResultHandlerController) HandleError(err error) int { + errorResult := &ErrorResult{ + Code: http.StatusInternalServerError, + } + switch e := err.(type) { + // deal with kubernetes errors + case errors.APIStatus: + errorResult.Code = int(e.Status().Code) + errorResult.SubCode = errorResult.Code + errorResult.Msg = e.Status().Message + case *mysql.MySQLError: + errorResult.Code = http.StatusBadRequest + errorResult.SubCode = int(e.Number) + // MySQL error Duplicate entry; + // refer https://dev.mysql.com/doc/refman/5.6/en/error-messages-server.html + if e.Number == 1062 { + errorResult.Msg = "Resources already exist! " + } else { + errorResult.Msg = e.Message + } + case *ErrorResult: + errorResult = e + default: + if err == orm.ErrNoRows { + errorResult.Code = http.StatusNotFound + } + errorResult.SubCode = errorResult.Code + errorResult.Msg = http.StatusText(errorResult.Code) + } + + if errorResult.Code >= http.StatusInternalServerError { + logs.Error("System error. %v", err) + } else { + logs.Info("Info error. %v", err) + } + + c.Ctx.Output.SetStatus(errorResult.Code) + + body, err := json.Marshal(errorResult) + if err != nil { + logs.Error("Json Marshal error. %v", err) + c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + c.Ctx.Output.Body(body) + return errorResult.Code +} + +func (c *ResultHandlerController) errorResult(code int, msg string) []byte { + errorResult := ErrorResult{ + Code: code, + Msg: msg, + } + body, err := json.Marshal(errorResult) + if err != nil { + logs.Error("Json Marshal error. %v", err) + c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + return body +} diff --git a/src/backend/controllers/kubernetes/deployment/deployment.go b/src/backend/controllers/kubernetes/deployment/deployment.go index 4b292d7a5..88821fda3 100644 --- a/src/backend/controllers/kubernetes/deployment/deployment.go +++ b/src/backend/controllers/kubernetes/deployment/deployment.go @@ -26,7 +26,9 @@ type KubeDeploymentController struct { } func (c *KubeDeploymentController) URLMapping() { + c.Mapping("List", c.List) c.Mapping("Get", c.Get) + c.Mapping("GetDetail", c.GetDetail) c.Mapping("Offline", c.Offline) c.Mapping("Deploy", c.Deploy) } @@ -50,6 +52,35 @@ func (c *KubeDeploymentController) Prepare() { } } +// @Title List deployment +// @Description get all deployment +// @Param pageNo query int false "the page current no" +// @Param pageSize query int false "the page size" +// @Param filter query string false "column filter, ex. filter=name=test" +// @Param sortby query string false "column sorted by, ex. sortby=-id, '-' representation desc, and sortby=id representation asc" +// @Param cluster path string true "the cluster name" +// @Param namespace path string true "the namespace name" +// @Success 200 {object} common.Page success +// @router /namespaces/:namespace/clusters/:cluster [get] +func (c *KubeDeploymentController) List() { + param := c.BuildQueryParam() + cluster := c.Ctx.Input.Param(":cluster") + namespace := c.Ctx.Input.Param(":namespace") + + manager, err := client.Manager(cluster) + if err == nil { + result, err := deployment.GetDeploymentPage(manager.Client, manager.Indexer, namespace, param) + if err != nil { + logs.Error("list kubernetes deployments error.", cluster, namespace, err) + c.HandleError(err) + return + } + c.Success(result) + } else { + c.AbortBadRequestFormat("Cluster") + } +} + // @Title deploy // @Description deploy tpl // @Param body body string true "The tpl content" @@ -220,9 +251,11 @@ func getNamespace(appId int64) (*models.Namespace, error) { // @Title Get // @Description find Deployment by cluster +// @Param cluster path string true "the cluster name" +// @Param namespace path string true "the namespace name" // @Success 200 {object} models.Deployment success -// @router /:deployment/namespaces/:namespace/clusters/:cluster [get] -func (c *KubeDeploymentController) Get() { +// @router /:deployment/detail/namespaces/:namespace/clusters/:cluster [get] +func (c *KubeDeploymentController) GetDetail() { cluster := c.Ctx.Input.Param(":cluster") namespace := c.Ctx.Input.Param(":namespace") name := c.Ctx.Input.Param(":deployment") @@ -240,6 +273,30 @@ func (c *KubeDeploymentController) Get() { } } +// @Title Get +// @Description find Deployment by cluster +// @Param cluster path string true "the cluster name" +// @Param namespace path string true "the namespace name" +// @Success 200 {object} models.Deployment success +// @router /:deployment/namespaces/:namespace/clusters/:cluster [get] +func (c *KubeDeploymentController) Get() { + cluster := c.Ctx.Input.Param(":cluster") + namespace := c.Ctx.Input.Param(":namespace") + name := c.Ctx.Input.Param(":deployment") + manager, err := client.Manager(cluster) + if err == nil { + result, err := deployment.GetDeployment(manager.Client, name, namespace) + if err != nil { + logs.Error("get kubernetes deployment error.", cluster, namespace, name, err) + c.HandleError(err) + return + } + c.Success(result) + } else { + c.AbortBadRequestFormat("Cluster") + } +} + // @Title Delete // @Description delete the Deployment // @Param cluster path string true "the cluster want to delete" diff --git a/src/backend/models/auditlog.go b/src/backend/models/auditlog.go index 46ebab50f..92c5afe48 100644 --- a/src/backend/models/auditlog.go +++ b/src/backend/models/auditlog.go @@ -32,7 +32,7 @@ type AuditLog struct { SubjectId int64 `orm:"type(bigint)" json:"subjectId,omitempty"` LogType AuditLogType `orm:"index;size(128)" json:"logType,omitempty"` LogLevel AuditLogLevel `orm:"index;size(128)" json:"logLevel,omitempty"` - Action string `orm:"index;size(256)" json:"action,omitempty"` + Action string `orm:"index;size(255)" json:"action,omitempty"` Message string `orm:"type(text);null" json:"message,omitempty"` UserIp string `orm:"size(200)" json:"userIp,omitempty"` User string `orm:"index;size(128)" json:"user,omitempty"` diff --git a/src/backend/models/build_sql.go b/src/backend/models/build_sql.go index 6329fb7bb..3fa6bdba7 100644 --- a/src/backend/models/build_sql.go +++ b/src/backend/models/build_sql.go @@ -24,7 +24,7 @@ func BuildQuery(qb orm.QueryBuilder, query map[string]interface{}) (orm.QueryBui querySql := make([]string, 0) for key, value := range query { - queryCondition := strings.Split(key, "__") + queryCondition := strings.Split(key, ListFilterExprSep) // 不符合查询条件定义规则,跳过条件拼接 if len(queryCondition) > 2 || len(queryCondition) == 0 { continue @@ -63,20 +63,18 @@ func BuildGroupBy(qb orm.QueryBuilder, groupby []string) orm.QueryBuilder { return qb } -func BuildOrder(qb orm.QueryBuilder, order []string) orm.QueryBuilder { - if len(order) > 0 { - for _, sort := range order { - asc := true - if strings.Index(sort, "-") == 0 { - asc = false - sort = sort[1:] - } - qb = qb.OrderBy("T0." + sort) - if asc { - qb = qb.Asc() - } else { - qb = qb.Desc() - } +func BuildOrder(qb orm.QueryBuilder, sort string) orm.QueryBuilder { + if sort != "" { + asc := true + if strings.Index(sort, "-") == 0 { + asc = false + sort = sort[1:] + } + qb = qb.OrderBy("T0." + sort) + if asc { + qb = qb.Asc() + } else { + qb = qb.Desc() } } return qb diff --git a/src/backend/models/const.go b/src/backend/models/const.go index acaf12611..26f4c8735 100644 --- a/src/backend/models/const.go +++ b/src/backend/models/const.go @@ -25,3 +25,5 @@ var TableToKubeApiTypeMap = map[string]KubeApiType{ TableNameSecret: KubeApiTypeSecret, TableNamePersistentVolumeClaim: KubeApiTypePersistentVolumeClaim, } + +const ListFilterExprSep = "__" diff --git a/src/backend/models/util.go b/src/backend/models/util.go index 3814247a6..d145244ae 100644 --- a/src/backend/models/util.go +++ b/src/backend/models/util.go @@ -33,7 +33,9 @@ func GetAll(queryTable interface{}, list interface{}, q *common.QueryParam) erro if len(q.Groupby) != 0 { qs = qs.GroupBy(q.Groupby...) } - qs = qs.OrderBy(q.Sortby...) + if q.Sortby != "" { + qs = qs.OrderBy(q.Sortby) + } if _, err := qs.Limit(q.Limit(), q.Offset()).All(list); err != nil { return err } @@ -56,7 +58,7 @@ func BuildFilter(qs orm.QuerySeter, query map[string]interface{}) orm.QuerySeter // query k=v for k, v := range query { // rewrite dot-notation to Object__Attribute - k = strings.Replace(k, ".", "__", -1) + k = strings.Replace(k, ".", ListFilterExprSep, -1) qs = qs.Filter(k, v) } return qs diff --git a/src/backend/resources/common/util.go b/src/backend/resources/common/util.go index eeb8643c7..e0c14fbde 100644 --- a/src/backend/resources/common/util.go +++ b/src/backend/resources/common/util.go @@ -36,3 +36,16 @@ func ContainersResourceList(containers []v1.Container) *ResourceList { Memory: memoryUsage, } } + +func CompareLabels(source map[string]string, target map[string]string) bool { + if len(source) != len(target) { + return false + } + for key, value := range source { + targetValue, ok := target[key] + if !ok || value != targetValue { + return false + } + } + return true +} diff --git a/src/backend/resources/dataselector/comparabletypes.go b/src/backend/resources/dataselector/comparabletypes.go new file mode 100644 index 000000000..22e783da1 --- /dev/null +++ b/src/backend/resources/dataselector/comparabletypes.go @@ -0,0 +1,61 @@ +package dataselector + +import ( + "strings" + "time" +) + +type StdComparableInt int + +func (self StdComparableInt) Compare(otherV ComparableValue) int { + other := otherV.(StdComparableInt) + return intsCompare(int(self), int(other)) +} + +func (self StdComparableInt) Contains(otherV ComparableValue) bool { + return self.Compare(otherV) == 0 +} + +type StdComparableString string + +func (self StdComparableString) Compare(otherV ComparableValue) int { + other := otherV.(StdComparableString) + return strings.Compare(string(self), string(other)) +} + +func (self StdComparableString) Contains(otherV ComparableValue) bool { + other := otherV.(StdComparableString) + return strings.Contains(string(self), string(other)) +} + +type StdComparableTime time.Time + +func (self StdComparableTime) Compare(otherV ComparableValue) int { + other := otherV.(StdComparableTime) + return ints64Compare(time.Time(self).Unix(), time.Time(other).Unix()) +} + +func (self StdComparableTime) Contains(otherV ComparableValue) bool { + return self.Compare(otherV) == 0 +} + +// Compares self with other value. Returns 1 if other value is smaller, +// 0 if they are the same, -1 if other is larger. +func ints64Compare(a, b int64) int { + if a > b { + return 1 + } else if a == b { + return 0 + } + return -1 +} + +// Int comparison functions. Similar to strings.Compare. +func intsCompare(a, b int) int { + if a > b { + return 1 + } else if a == b { + return 0 + } + return -1 +} diff --git a/src/backend/resources/dataselector/const.go b/src/backend/resources/dataselector/const.go new file mode 100644 index 000000000..4b224334e --- /dev/null +++ b/src/backend/resources/dataselector/const.go @@ -0,0 +1,13 @@ +package dataselector + +// PropertyName is used to get the value of certain property of data cell. +// For example if we want to get the namespace of certain Deployment we can use DeploymentCell.GetProperty(NamespaceProperty) +type PropertyName string + +// List of all property names supported by the UI. +const ( + NameProperty PropertyName = "name" + CreationTimestampProperty PropertyName = "creationTimestamp" + NamespaceProperty PropertyName = "namespace" + StatusProperty PropertyName = "status" +) diff --git a/src/backend/resources/dataselector/dataselector.go b/src/backend/resources/dataselector/dataselector.go new file mode 100644 index 000000000..11b691ce7 --- /dev/null +++ b/src/backend/resources/dataselector/dataselector.go @@ -0,0 +1,123 @@ +package dataselector + +import ( + "sort" + "strings" + "time" + + "github.com/Qihoo360/wayne/src/backend/common" + "github.com/Qihoo360/wayne/src/backend/models" +) + +// GenericDataCell describes the interface of the data cell that contains all the necessary methods needed to perform +// complex data selection +// GenericDataSelect takes a list of these interfaces and performs selection operation. +// Therefore as long as the list is composed of GenericDataCells you can perform any data selection! +type DataCell interface { + // GetPropertyAtIndex returns the property of this data cell. + // Value returned has to have Compare method which is required by Sort functionality of DataSelect. + GetProperty(PropertyName) ComparableValue +} + +// ComparableValue hold any value that can be compared to its own kind. +type ComparableValue interface { + // Compares self with other value. Returns 1 if other value is smaller, 0 if they are the same, -1 if other is larger. + Compare(ComparableValue) int + // Returns true if self value contains or is equal to other value, false otherwise. + Contains(ComparableValue) bool +} + +// SelectableData contains all the required data to perform data selection. +// It implements sort.Interface so its sortable under sort.Sort +// You can use its Select method to get selected GenericDataCell list. +type DataSelector struct { + // GenericDataList hold generic data cells that are being selected. + GenericDataList []DataCell + // DataSelectQuery holds instructions for data select. + DataSelectQuery *common.QueryParam +} + +// Implementation of sort.Interface so that we can use built-in sort function (sort.Sort) for sorting SelectableData + +// Len returns the length of data inside SelectableData. +func (self DataSelector) Len() int { return len(self.GenericDataList) } + +// Swap swaps 2 indices inside SelectableData. +func (self DataSelector) Swap(i, j int) { + self.GenericDataList[i], self.GenericDataList[j] = self.GenericDataList[j], self.GenericDataList[i] +} + +// Less compares 2 indices inside SelectableData and returns true if first index is larger. +func (self DataSelector) Less(i, j int) bool { + sort := self.DataSelectQuery.Sortby + if sort != "" { + asc := true + if strings.Index(sort, "-") == 0 { + asc = false + sort = sort[1:] + } + a := self.GenericDataList[i].GetProperty(PropertyName(sort)) + b := self.GenericDataList[j].GetProperty(PropertyName(sort)) + // ignore sort completely if property name not found + if a == nil || b == nil { + return false + } + cmp := a.Compare(b) + if cmp == 0 { // values are the same. Just return + return false + } else { // values different + return (cmp == -1 && asc) || (cmp == 1 && !asc) + } + } + return false +} + +// Sort sorts the data inside as instructed by DataSelectQuery and returns itself to allow method chaining. +func (self *DataSelector) Sort() *DataSelector { + sort.Sort(*self) + return self +} + +// Filter the data inside as instructed by DataSelectQuery and returns itself to allow method chaining. +func (self *DataSelector) Filter() *DataSelector { + filteredList := []DataCell{} + + for _, c := range self.GenericDataList { + matches := true + for key, value := range self.DataSelectQuery.Query { + // TODO now string default use contains condition, may be support labels and other types? + if strings.Contains(key, models.ListFilterExprSep) { + keySplit := strings.Split(key, models.ListFilterExprSep) + key = keySplit[0] + } + v := c.GetProperty(PropertyName(key)) + if v == nil { + matches = false + continue + } + if !v.Contains(ParseToComparableValue(value)) { + matches = false + continue + } + } + if matches { + filteredList = append(filteredList, c) + } + } + + self.GenericDataList = filteredList + return self +} + +func ParseToComparableValue(value interface{}) ComparableValue { + switch value.(type) { + case string: + return StdComparableString(value.(string)) + case int: + return StdComparableInt(value.(int)) + case time.Time: + return StdComparableTime(value.(time.Time)) + default: + return nil + } +} diff --git a/src/backend/resources/dataselector/page.go b/src/backend/resources/dataselector/page.go new file mode 100644 index 000000000..034ad9142 --- /dev/null +++ b/src/backend/resources/dataselector/page.go @@ -0,0 +1,30 @@ +package dataselector + +import ( + "github.com/Qihoo360/wayne/src/backend/common" +) + +// GenericDataSelect takes a list of GenericDataCells and DataSelectQuery and returns selected data as instructed by dsQuery. +func DataSelectPage(dataList []DataCell, q *common.QueryParam) *common.Page { + SelectableData := DataSelector{ + GenericDataList: dataList, + DataSelectQuery: q, + } + // Pipeline is Filter -> Sort -> Paginate + filtered := SelectableData.Filter().Sort() + filteredTotal := len(filtered.GenericDataList) + + // slice start and end point + start := q.Offset() + end := start + q.Limit() + if start > int64(filteredTotal) { + start = int64(filteredTotal) + } + if end > int64(filteredTotal) { + end = int64(filteredTotal) + } + + pagedList := filtered.GenericDataList[start:end] + + return q.NewPage(int64(filteredTotal), pagedList) +} diff --git a/src/backend/resources/deployment/common.go b/src/backend/resources/deployment/common.go new file mode 100644 index 000000000..440d1a541 --- /dev/null +++ b/src/backend/resources/deployment/common.go @@ -0,0 +1,21 @@ +package deployment + +import ( + "github.com/Qihoo360/wayne/src/backend/resources/dataselector" +) + +type DeploymentCell Deployment + +func (cell DeploymentCell) GetProperty(name dataselector.PropertyName) dataselector.ComparableValue { + switch name { + case dataselector.NameProperty: + return dataselector.StdComparableString(cell.ObjectMeta.Name) + case dataselector.CreationTimestampProperty: + return dataselector.StdComparableTime(cell.ObjectMeta.CreationTimestamp.Time) + case dataselector.NamespaceProperty: + return dataselector.StdComparableString(cell.ObjectMeta.Namespace) + default: + // if name is not supported then just return a constant dummy value, sort will have no effect. + return nil + } +} diff --git a/src/backend/resources/deployment/deployment.go b/src/backend/resources/deployment/deployment.go index 60fc91e5a..f420c8427 100644 --- a/src/backend/resources/deployment/deployment.go +++ b/src/backend/resources/deployment/deployment.go @@ -5,17 +5,25 @@ import ( "github.com/Qihoo360/wayne/src/backend/resources/common" "github.com/Qihoo360/wayne/src/backend/resources/event" "github.com/Qihoo360/wayne/src/backend/resources/pod" - "github.com/Qihoo360/wayne/src/backend/util/maps" "k8s.io/api/apps/v1beta1" "k8s.io/apimachinery/pkg/api/errors" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" ) type Deployment struct { ObjectMeta common.ObjectMeta `json:"objectMeta"` Pods common.PodInfo `json:"pods"` + Containers []string `json:"containers"` +} + +func GetDeploymentList(cli *kubernetes.Clientset, namespace string, opts metaV1.ListOptions) ([]v1beta1.Deployment, error) { + deployments, err := cli.AppsV1beta1().Deployments(namespace).List(opts) + if err != nil { + return nil, err + } + + return deployments.Items, nil } func GetDeploymentResource(cli *kubernetes.Clientset, deployment *v1beta1.Deployment) (*common.ResourceList, error) { @@ -43,10 +51,10 @@ func CreateOrUpdateDeployment(cli *kubernetes.Clientset, deployment *v1beta1.Dep } return nil, err } - old.Labels = maps.MergeLabels(old.Labels, deployment.Labels) - oldTemplateLabels := old.Spec.Template.Labels + old.Labels = deployment.Labels + old.Annotations = deployment.Annotations old.Spec = deployment.Spec - old.Spec.Template.Labels = maps.MergeLabels(oldTemplateLabels, deployment.Spec.Template.Labels) + return cli.AppsV1beta1().Deployments(deployment.Namespace).Update(old) } func UpdateDeployment(cli *kubernetes.Clientset, deployment *v1beta1.Deployment) (*v1beta1.Deployment, error) { @@ -58,11 +66,15 @@ func GetDeployment(cli *kubernetes.Clientset, name, namespace string) (*v1beta1. } func GetDeploymentDetail(cli *kubernetes.Clientset, indexer *client.CacheIndexer, name, namespace string) (*Deployment, error) { - deployment, err := cli.ExtensionsV1beta1().Deployments(namespace).Get(name, metaV1.GetOptions{}) + deployment, err := cli.AppsV1beta1().Deployments(namespace).Get(name, metaV1.GetOptions{}) if err != nil { return nil, err } + return toDeployment(deployment, indexer), nil +} + +func toDeployment(deployment *v1beta1.Deployment, indexer *client.CacheIndexer) *Deployment { result := &Deployment{ ObjectMeta: common.NewObjectMeta(deployment.ObjectMeta), } @@ -71,17 +83,20 @@ func GetDeploymentDetail(cli *kubernetes.Clientset, indexer *client.CacheIndexer podInfo.Current = deployment.Status.AvailableReplicas podInfo.Desired = *deployment.Spec.Replicas - podSelector := labels.SelectorFromSet(deployment.Spec.Template.Labels).String() - pods, err := pod.GetPodsBySelector(cli, namespace, podSelector) - if err != nil { - return nil, err - } + pods := pod.GetPodsBySelectorFromCache(indexer, deployment.Namespace, deployment.Spec.Template.Labels) podInfo.Warnings = event.GetPodsWarningEvents(indexer, pods) result.Pods = podInfo - return result, nil + containers := make([]string, 0) + for _, container := range deployment.Spec.Template.Spec.Containers { + containers = append(containers, container.Image) + } + + result.Containers = containers + + return result } func DeleteDeployment(cli *kubernetes.Clientset, name, namespace string) error { diff --git a/src/backend/resources/deployment/list.go b/src/backend/resources/deployment/list.go new file mode 100644 index 000000000..dfaf2fb05 --- /dev/null +++ b/src/backend/resources/deployment/list.go @@ -0,0 +1,32 @@ +package deployment + +import ( + "github.com/Qihoo360/wayne/src/backend/client" + "github.com/Qihoo360/wayne/src/backend/common" + "github.com/Qihoo360/wayne/src/backend/resources/dataselector" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func GetDeploymentPage(cli *kubernetes.Clientset, indexer *client.CacheIndexer, namespace string, q *common.QueryParam) (*common.Page, error) { + kubeDeployments, err := GetDeploymentList(cli, namespace, metaV1.ListOptions{}) + if err != nil { + return nil, err + } + + deployments := make([]Deployment, 0) + + for i := 0; i < len(kubeDeployments); i++ { + deployments = append(deployments, *toDeployment(&kubeDeployments[i], indexer)) + } + + return dataselector.DataSelectPage(toCells(deployments), q), nil +} + +func toCells(deploy []Deployment) []dataselector.DataCell { + cells := make([]dataselector.DataCell, len(deploy)) + for i := range deploy { + cells[i] = DeploymentCell(deploy[i]) + } + return cells +} diff --git a/src/backend/resources/pod/pod.go b/src/backend/resources/pod/pod.go index 0e19107ed..cc6f8aebc 100644 --- a/src/backend/resources/pod/pod.go +++ b/src/backend/resources/pod/pod.go @@ -5,6 +5,7 @@ import ( "github.com/Qihoo360/wayne/src/backend/client" "github.com/Qihoo360/wayne/src/backend/models" + "github.com/Qihoo360/wayne/src/backend/resources/common" "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -48,6 +49,28 @@ func GetPodCounts(indexer *client.CacheIndexer) int { return len(pods) } +func GetPodsBySelectorFromCache(indexer *client.CacheIndexer, namespace string, labels map[string]string) []v1.Pod { + cachePods := indexer.Pod.List() + var pods []v1.Pod + for _, pod := range cachePods { + cachePod, ok := pod.(*v1.Pod) + if !ok { + continue + } + if namespace != cachePod.Namespace { + continue + } + + if !common.CompareLabels(labels, cachePod.Labels) { + continue + } + + pods = append(pods, *cachePod) + } + + return pods +} + func GetAllPodByLabelSelector(cli *kubernetes.Clientset, labelSelector string) ([]*Pod, error) { podList, err := cli.CoreV1().Pods(metaV1.NamespaceAll).List(metaV1.ListOptions{LabelSelector: labelSelector}) if err != nil { diff --git a/src/backend/routers/commentsRouter_controllers_kubernetes_deployment.go b/src/backend/routers/commentsRouter_controllers_kubernetes_deployment.go index bb59bd1d1..420b97143 100644 --- a/src/backend/routers/commentsRouter_controllers_kubernetes_deployment.go +++ b/src/backend/routers/commentsRouter_controllers_kubernetes_deployment.go @@ -7,6 +7,14 @@ import ( func init() { + beego.GlobalControllerRouter["github.com/Qihoo360/wayne/src/backend/controllers/kubernetes/deployment:KubeDeploymentController"] = append(beego.GlobalControllerRouter["github.com/Qihoo360/wayne/src/backend/controllers/kubernetes/deployment:KubeDeploymentController"], + beego.ControllerComments{ + Method: "GetDetail", + Router: `/:deployment/detail/namespaces/:namespace/clusters/:cluster`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Params: nil}) + beego.GlobalControllerRouter["github.com/Qihoo360/wayne/src/backend/controllers/kubernetes/deployment:KubeDeploymentController"] = append(beego.GlobalControllerRouter["github.com/Qihoo360/wayne/src/backend/controllers/kubernetes/deployment:KubeDeploymentController"], beego.ControllerComments{ Method: "Get", @@ -31,4 +39,12 @@ func init() { MethodParams: param.Make(), Params: nil}) + beego.GlobalControllerRouter["github.com/Qihoo360/wayne/src/backend/controllers/kubernetes/deployment:KubeDeploymentController"] = append(beego.GlobalControllerRouter["github.com/Qihoo360/wayne/src/backend/controllers/kubernetes/deployment:KubeDeploymentController"], + beego.ControllerComments{ + Method: "List", + Router: `/namespaces/:namespace/clusters/:cluster`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Params: nil}) + } diff --git a/src/frontend/lib b/src/frontend/lib index 7a9ca86f6..78e72f46e 160000 --- a/src/frontend/lib +++ b/src/frontend/lib @@ -1 +1 @@ -Subproject commit 7a9ca86f691a0b7760b61cd318906ac9519459a7 +Subproject commit 78e72f46eb82945941c1fdaf813e7b8f57a628da diff --git a/src/frontend/src/app/admin/admin-routing.module.ts b/src/frontend/src/app/admin/admin-routing.module.ts index 31156edd0..16a113fcf 100644 --- a/src/frontend/src/app/admin/admin-routing.module.ts +++ b/src/frontend/src/app/admin/admin-routing.module.ts @@ -62,6 +62,7 @@ import { IngressComponent } from './ingress/ingress.component'; import { TrashIngressComponent } from './ingress/trash-ingress/trash-ingress.component'; import { IngressTplComponent } from './ingresstpl/ingresstpl.component'; import { TrashIngressTplComponent } from './ingresstpl/trash-ingresstpl/trash-ingresstpl.component'; +import { KubeDeploymentComponent } from './kubernetes/deployment/kube-deployment.component'; const routes: Routes = [ @@ -147,6 +148,8 @@ const routes: Routes = [ {path: 'ingress/trash', component: TrashIngressComponent}, {path: 'ingress/tpl', component: IngressTplComponent}, {path: 'ingress/tpl/trash', component: TrashIngressTplComponent}, + {path: 'kubernetes/deployment', component: KubeDeploymentComponent}, + {path: 'kubernetes/deployment/:cluster', component: KubeDeploymentComponent}, ...ADMINROUTES ] } diff --git a/src/frontend/src/app/admin/admin.component.html b/src/frontend/src/app/admin/admin.component.html index fb046ff3b..0af70c848 100644 --- a/src/frontend/src/app/admin/admin.component.html +++ b/src/frontend/src/app/admin/admin.component.html @@ -12,3 +12,4 @@ + diff --git a/src/frontend/src/app/admin/admin.component.ts b/src/frontend/src/app/admin/admin.component.ts index 0d25c30bc..20d380389 100644 --- a/src/frontend/src/app/admin/admin.component.ts +++ b/src/frontend/src/app/admin/admin.component.ts @@ -184,6 +184,9 @@ export class AdminComponent implements OnInit { '/admin/kubernetes/persistentvolume': { i18nKey: 'MENU.PV', }, + '/admin/kubernetes/deployment': { + i18nKey: 'MENU.DEPLOYMENT', + }, }; constructor( diff --git a/src/frontend/src/app/admin/admin.module.ts b/src/frontend/src/app/admin/admin.module.ts index d2b8e20a9..fa6bfbb64 100644 --- a/src/frontend/src/app/admin/admin.module.ts +++ b/src/frontend/src/app/admin/admin.module.ts @@ -36,6 +36,8 @@ import { NodesModule } from './node/nodes.module'; import { LibraryAdminModule } from '../../../lib/admin/library-admin.module'; import { IngressModule } from './ingress/ingress.module'; import { IngressTplModule } from './ingresstpl/ingresstpl.module'; +import { KubeDeploymentModule } from './kubernetes/deployment/kube-deployment.module'; +import { TplDetailModule } from '../shared/tpl-detail/tpl-detail.module'; @NgModule({ imports: [ @@ -72,7 +74,9 @@ import { IngressTplModule } from './ingresstpl/ingresstpl.module'; NodesModule, LibraryAdminModule, IngressModule, - IngressTplModule + IngressTplModule, + KubeDeploymentModule, + TplDetailModule ], providers: [ AdminAuthCheckGuard, diff --git a/src/frontend/src/app/admin/deployment/deployment.component.spec.ts b/src/frontend/src/app/admin/deployment/deployment.component.spec.ts index 947742c6a..9b80d407a 100644 --- a/src/frontend/src/app/admin/deployment/deployment.component.spec.ts +++ b/src/frontend/src/app/admin/deployment/deployment.component.spec.ts @@ -2,7 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { DeploymentComponent } from './deployment.component'; -describe('DeploymentComponent', () => { +describe('KubeDeploymentComponent', () => { let component: DeploymentComponent; let fixture: ComponentFixture; diff --git a/src/frontend/src/app/admin/kubernetes/deployment/kube-deployment.component.html b/src/frontend/src/app/admin/kubernetes/deployment/kube-deployment.component.html new file mode 100644 index 000000000..ac8a9c0ad --- /dev/null +++ b/src/frontend/src/app/admin/kubernetes/deployment/kube-deployment.component.html @@ -0,0 +1,31 @@ + + + + + + + 名称 + Label + 镜像 + 容器状态 + Age + + + + + + + + + + + + + diff --git a/src/frontend/src/app/admin/kubernetes/deployment/kube-deployment.component.ts b/src/frontend/src/app/admin/kubernetes/deployment/kube-deployment.component.ts new file mode 100644 index 000000000..0976e3305 --- /dev/null +++ b/src/frontend/src/app/admin/kubernetes/deployment/kube-deployment.component.ts @@ -0,0 +1,141 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { State } from '@clr/angular'; +import { Subscription } from 'rxjs/Subscription'; +import { DeploymentClient } from '../../../shared/client/v1/kubernetes/deployment'; +import { PageState } from '../../../shared/page/page-state'; +import { BreadcrumbService } from '../../../shared/client/v1/breadcrumb.service'; +import { MessageHandlerService } from '../../../shared/message-handler/message-handler.service'; +import { KubeListDeploymentComponent } from './list/kube-list-deployment.component'; +import { ClusterService } from '../../../shared/client/v1/cluster.service'; +import { DeploymentList } from '../../../shared/model/v1/deployment-list'; +import { AdminDefaultApiId } from '../../../shared/shared.const'; +import { AceEditorMsg } from '../../../shared/ace-editor/ace-editor'; +import { AceEditorService } from '../../../shared/ace-editor/ace-editor.service'; +import { KubeMigrationDeploymentComponent } from './migration/kube-migration-deployment.component'; + +const showState = { + 'name': {hidden: false}, + 'label': {hidden: false}, + 'containers': {hidden: false}, + 'status': {hidden: false}, + 'age': {hidden: false} +}; + +@Component({ + selector: 'kubernetes-deployment', + templateUrl: './kube-deployment.component.html' +}) +export class KubeDeploymentComponent implements OnInit { + @ViewChild(KubeListDeploymentComponent) + listDeployment: KubeListDeploymentComponent; + @ViewChild(KubeMigrationDeploymentComponent) + migrationDeployment: KubeMigrationDeploymentComponent; + + namespace = 'default'; + cluster: string; + clusters: Array; + changedDeployments: DeploymentList[]; + pageState: PageState = new PageState(); + subscription: Subscription; + + showList: any[] = Array(); + showState: object = showState; + + constructor(private breadcrumbService: BreadcrumbService, + private deploymentClient: DeploymentClient, + private clusterService: ClusterService, + private route: ActivatedRoute, + private router: Router, + private aceEditorService: AceEditorService, + private messageHandlerService: MessageHandlerService) { + } + + initShow() { + this.showList = []; + Object.keys(this.showState).forEach(key => { + if (!this.showState[key].hidden) { + this.showList.push(key); + } + }); + } + + confirmEvent() { + Object.keys(this.showState).forEach(key => { + if (this.showList.indexOf(key) > -1) { + this.showState[key] = {hidden: false}; + } else { + this.showState[key] = {hidden: true}; + } + }); + } + + cancelEvent() { + this.initShow(); + } + + ngOnInit() { + this.initShow(); + let cluster = this.route.snapshot.params['cluster']; + this.clusterService.getNames().subscribe( + response => { + const data = response.data; + if (data) { + this.clusters = data.map(item => item.name); + if (data.length > 0 && !this.cluster || this.clusters.indexOf(this.cluster) === -1) { + cluster = cluster ? cluster : data[0].name; + } + this.jumpTo(cluster); + } + }, + error => this.messageHandlerService.handleError(error) + ); + } + + jumpTo(cluster: string) { + this.cluster = cluster; + this.router.navigateByUrl(`admin/kubernetes/deployment/${cluster}`); + this.retrieve(); + } + + detail(deploymentList: DeploymentList) { + this.deploymentClient.get(AdminDefaultApiId, this.cluster, deploymentList.objectMeta.namespace, deploymentList.objectMeta.name) + .subscribe( + response => { + const data = response.data; + this.aceEditorService.announceMessage(AceEditorMsg.Instance(data, false, '详情')); + }, + error => this.messageHandlerService.handleError(error) + ); + } + + migration(deploymentList: DeploymentList) { + this.deploymentClient.get(AdminDefaultApiId, this.cluster, deploymentList.objectMeta.namespace, deploymentList.objectMeta.name) + .subscribe( + response => { + const data = response.data; + this.migrationDeployment.openModal(this.cluster, data); + }, + error => this.messageHandlerService.handleError(error) + ); + } + + retrieve(state?: State): void { + if (state) { + this.pageState = PageState.fromState(state, {totalPage: this.pageState.page.totalPage, totalCount: this.pageState.page.totalCount}); + } + if (this.cluster) { + this.deploymentClient.listPage(this.pageState, this.cluster, this.namespace) + .subscribe( + response => { + const data = response.data; + this.pageState.page.totalPage = data.totalPage; + this.pageState.page.totalCount = data.totalCount; + this.changedDeployments = data.list; + }, + error => this.messageHandlerService.handleError(error) + ); + } + } + +} diff --git a/src/frontend/src/app/admin/kubernetes/deployment/kube-deployment.module.ts b/src/frontend/src/app/admin/kubernetes/deployment/kube-deployment.module.ts new file mode 100644 index 000000000..aac6fbc06 --- /dev/null +++ b/src/frontend/src/app/admin/kubernetes/deployment/kube-deployment.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '../../../shared/shared.module'; +import { KubeDeploymentComponent } from './kube-deployment.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { DeploymentClient } from '../../../shared/client/v1/kubernetes/deployment'; +import { KubeListDeploymentComponent } from './list/kube-list-deployment.component'; +import { KubeMigrationDeploymentComponent } from './migration/kube-migration-deployment.component'; + +@NgModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + ], + providers: [ + DeploymentClient + ], + exports: [ + KubeDeploymentComponent, + KubeListDeploymentComponent, + KubeMigrationDeploymentComponent + ], + declarations: [ + KubeDeploymentComponent, + KubeListDeploymentComponent, + KubeMigrationDeploymentComponent + ] +}) + +export class KubeDeploymentModule { +} diff --git a/src/frontend/src/app/admin/kubernetes/deployment/list/kube-list-deployment.component.html b/src/frontend/src/app/admin/kubernetes/deployment/list/kube-list-deployment.component.html new file mode 100644 index 000000000..a9c9eaae7 --- /dev/null +++ b/src/frontend/src/app/admin/kubernetes/deployment/list/kube-list-deployment.component.html @@ -0,0 +1,61 @@ + + + + 名称 + + + + + Label + + + + + 镜像 + + + + + 容器状态 + + + + + Age + + + + + + + 详情 + 迁移 + + {{deployment.objectMeta.name}} + + + {{label.key}}: {{label.value}} + + + + + {{container}} + + + + {{deployment.pods.current}}/{{deployment.pods.desired}} + + + + {{deployment.objectMeta.creationTimestamp | relativeTime}} + + + + + diff --git a/src/frontend/src/app/admin/kubernetes/deployment/list/kube-list-deployment.component.ts b/src/frontend/src/app/admin/kubernetes/deployment/list/kube-list-deployment.component.ts new file mode 100644 index 000000000..c1823c407 --- /dev/null +++ b/src/frontend/src/app/admin/kubernetes/deployment/list/kube-list-deployment.component.ts @@ -0,0 +1,57 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Router } from '@angular/router'; +import { State } from '@clr/angular'; +import { Page } from '../../../../shared/page/page-state'; +import { BreadcrumbService } from '../../../../shared/client/v1/breadcrumb.service'; +import { DeploymentList } from '../../../../shared/model/v1/deployment-list'; +import { TplDetailService } from '../../../../shared/tpl-detail/tpl-detail.service'; + +@Component({ + selector: 'kube-list-deployment', + templateUrl: 'kube-list-deployment.component.html' +}) +export class KubeListDeploymentComponent implements OnInit { + + @Input() deployments: DeploymentList[]; + @Input() page: Page; + @Input() showState: object; + currentPage = 1; + state: State; + + @Output() paginate = new EventEmitter(); + @Output() detail = new EventEmitter(); + @Output() migration = new EventEmitter(); + + constructor( + private breadcrumbService: BreadcrumbService, + private router: Router, + private tplDetailService: TplDetailService) { + } + + ngOnInit(): void { + } + + pageSizeChange(pageSize: number) { + this.state.page.to = pageSize - 1; + this.state.page.size = pageSize; + this.currentPage = 1; + this.paginate.emit(this.state); + } + + refresh(state: State) { + this.state = state; + this.paginate.emit(state); + } + + versionDetail(version: string) { + this.tplDetailService.openModal(version, '版本'); + } + + detailDeployment(obj: DeploymentList) { + this.detail.emit(obj); + } + + migrationDeployment(obj: DeploymentList) { + this.migration.emit(obj); + } +} diff --git a/src/frontend/src/app/admin/kubernetes/deployment/migration/kube-migration-deployment.component.html b/src/frontend/src/app/admin/kubernetes/deployment/migration/kube-migration-deployment.component.html new file mode 100644 index 000000000..1a5ec19b8 --- /dev/null +++ b/src/frontend/src/app/admin/kubernetes/deployment/migration/kube-migration-deployment.component.html @@ -0,0 +1,45 @@ + + 迁移部署到机房: [{{cluster}}] + + + + + + + + + + + {{warningMsg}} + + + + + + + + 应用 + + + {{app.name}} + + + + + + 部署模版 + + + + + + + + diff --git a/src/frontend/src/app/admin/kubernetes/deployment/migration/kube-migration-deployment.component.ts b/src/frontend/src/app/admin/kubernetes/deployment/migration/kube-migration-deployment.component.ts new file mode 100644 index 000000000..5ff9bd39d --- /dev/null +++ b/src/frontend/src/app/admin/kubernetes/deployment/migration/kube-migration-deployment.component.ts @@ -0,0 +1,138 @@ +import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'; +import { NgForm } from '@angular/forms'; +import { AceEditorBoxComponent } from '../../../../shared/ace-editor/ace-editor-box/ace-editor-box.component'; +import { App } from '../../../../shared/model/v1/app'; +import { DeploymentService } from '../../../../shared/client/v1/deployment.service'; +import { AppService } from '../../../../shared/client/v1/app.service'; +import { AceEditorService } from '../../../../shared/ace-editor/ace-editor.service'; +import { MessageHandlerService } from '../../../../shared/message-handler/message-handler.service'; +import { AceEditorMsg } from '../../../../shared/ace-editor/ace-editor'; +import { isUndefined } from 'util'; +import { KubeDeployment } from '../../../../shared/model/v1/kubernetes/deployment'; +import { defaultDeployment } from '../../../../shared/default-models/deployment.const'; +import { AuthService } from '../../../../shared/auth/auth.service'; +import { Deployment, DeploymentMetaData } from '../../../../shared/model/v1/deployment'; +import { DeploymentTplService } from '../../../../shared/client/v1/deploymenttpl.service'; +import { DeploymentTpl } from '../../../../shared/model/v1/deploymenttpl'; + +@Component({ + selector: 'kube-migration-deployment', + templateUrl: 'kube-migration-deployment.component.html' +}) +export class KubeMigrationDeploymentComponent implements OnInit { + + @Output() create = new EventEmitter(); + modalOpened: boolean; + + @ViewChild('ngForm') + currentForm: NgForm; + + @ViewChild(AceEditorBoxComponent) + aceBox: any; + + deployment: KubeDeployment; + isSubmitOnGoing = false; + + warningMsg: string; + cluster: string; + + apps: App[]; + selectedApp: App; + + constructor(private deploymentService: DeploymentService, + private deploymentTplService: DeploymentTplService, + private appService: AppService, + public authService: AuthService, + private aceEditorService: AceEditorService, + private messageHandlerService: MessageHandlerService) { + + } + + ngOnInit(): void { + this.appService + .getNames() + .subscribe( + response => { + this.apps = response.data; + }, + error => this.messageHandlerService.handleError(error) + ); + + } + + openModal(cluster: string, deployment: KubeDeployment) { + this.modalOpened = true; + this.isSubmitOnGoing = false; + this.warningMsg = ''; + this.cluster = cluster; + + this.deployment = JSON.parse(defaultDeployment); + this.deployment.metadata.name = deployment.metadata.name; + this.deployment.metadata.labels = deployment.metadata.labels; + this.deployment.metadata.annotations = deployment.metadata.annotations; + this.deployment.spec = deployment.spec; + this.validLabel(deployment); + this.initJsonEditor(); + } + + initJsonEditor(): void { + this.aceEditorService.announceMessage(AceEditorMsg.Instance(this.deployment)); + } + + validLabel(deployment: KubeDeployment) { + const app = deployment.spec.selector.matchLabels['app']; + if (!app) { + this.warningMsg = '.spec.selector.matchLabels 没有app标签,直接发布可能会导致游离的rs!'; + } + if (app !== deployment.metadata.name) { + this.warningMsg = '.spec.selector.matchLabels app标签和部署名称不一致,直接发布可能会导致游离的rs!'; + } + } + + onCancel() { + this.modalOpened = false; + this.currentForm.reset(); + } + + onSubmit() { + if (this.isSubmitOnGoing) { + return; + } + this.isSubmitOnGoing = true; + const deployment = new Deployment(); + deployment.name = this.deployment.metadata.name; + deployment.appId = this.selectedApp.id; + const metaData = new DeploymentMetaData(); + metaData.replicas = {[this.cluster]: this.deployment.spec.replicas}; + deployment.metaData = JSON.stringify(metaData); + this.deploymentService.create(deployment).subscribe( + resp => { + const data = resp.data; + const deploymentTpl = new DeploymentTpl(); + deploymentTpl.name = this.deployment.metadata.name; + deploymentTpl.deploymentId = data.id; + deploymentTpl.template = JSON.stringify(this.deployment); + deploymentTpl.description = 'migration from kubernetes. '; + this.deploymentTplService.create(deploymentTpl, this.selectedApp.id).subscribe( + () => { + this.messageHandlerService.showSuccess('创建部署和部署模版成功!请前往前台手动发布部署到相应机房!'); + }, + error => { + this.messageHandlerService.handleError(error); + }); + }, + error => { + this.messageHandlerService.handleError(error); + } + ); + this.modalOpened = false; + } + + public get isValid(): boolean { + return this.currentForm && + this.currentForm.valid && + !this.isSubmitOnGoing && + !isUndefined(this.selectedApp); + } + +} diff --git a/src/frontend/src/app/portal/base/base-app.module.ts b/src/frontend/src/app/portal/base/base-app.module.ts index 5bda1eb24..539d3f0f6 100644 --- a/src/frontend/src/app/portal/base/base-app.module.ts +++ b/src/frontend/src/app/portal/base/base-app.module.ts @@ -5,7 +5,7 @@ import { FooterModule } from '../../shared/footer/footer.module'; import { SidenavModule } from '../sidenav/sidenav.module'; import { BaseComponent } from './base.component'; import { PublishHistoryModule } from '../common/publish-history/publish-history.module'; -import { TplDetailModule } from '../common/tpl-detail/tpl-detail.module'; +import { TplDetailModule } from '../../shared/tpl-detail/tpl-detail.module'; @NgModule({ imports: [ diff --git a/src/frontend/src/app/portal/configmap/list-configmap/list-configmap.component.ts b/src/frontend/src/app/portal/configmap/list-configmap/list-configmap.component.ts index 05fc840a5..fed42f0ea 100644 --- a/src/frontend/src/app/portal/configmap/list-configmap/list-configmap.component.ts +++ b/src/frontend/src/app/portal/configmap/list-configmap/list-configmap.component.ts @@ -9,7 +9,7 @@ import { PublishConfigMapTplComponent } from '../publish-tpl/publish-tpl.compone import { ConfigMap } from '../../../shared/model/v1/configmap'; import { ConfigMapTpl } from '../../../shared/model/v1/configmaptpl'; import { ConfigMapTplService } from '../../../shared/client/v1/configmaptpl.service'; -import { TplDetailService } from '../../common/tpl-detail/tpl-detail.service'; +import { TplDetailService } from '../../../shared/tpl-detail/tpl-detail.service'; import { AuthService } from '../../../shared/auth/auth.service'; import { App } from '../../../shared/model/v1/app'; import { Page } from '../../../shared/page/page-state'; diff --git a/src/frontend/src/app/portal/cronjob/list-cronjob/list-cronjob.component.ts b/src/frontend/src/app/portal/cronjob/list-cronjob/list-cronjob.component.ts index 59f1bab19..1779c6f3d 100644 --- a/src/frontend/src/app/portal/cronjob/list-cronjob/list-cronjob.component.ts +++ b/src/frontend/src/app/portal/cronjob/list-cronjob/list-cronjob.component.ts @@ -9,7 +9,7 @@ import { PublishCronjobTplComponent } from '../publish-tpl/publish-tpl.component import { CronjobTpl } from '../../../shared/model/v1/cronjobtpl'; import { CronjobService } from '../../../shared/client/v1/cronjob.service'; import { CronjobTplService } from '../../../shared/client/v1/cronjobtpl.service'; -import { TplDetailService } from '../../common/tpl-detail/tpl-detail.service'; +import { TplDetailService } from '../../../shared/tpl-detail/tpl-detail.service'; import { AuthService } from '../../../shared/auth/auth.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Page } from '../../../shared/page/page-state'; diff --git a/src/frontend/src/app/portal/daemonset/list-daemonset/list-daemonset.component.ts b/src/frontend/src/app/portal/daemonset/list-daemonset/list-daemonset.component.ts index 70d550104..197fe5e4d 100644 --- a/src/frontend/src/app/portal/daemonset/list-daemonset/list-daemonset.component.ts +++ b/src/frontend/src/app/portal/daemonset/list-daemonset/list-daemonset.component.ts @@ -14,7 +14,7 @@ import { Subscription } from 'rxjs/Subscription'; import { PublishDaemonSetTplComponent } from '../publish-tpl/publish-tpl.component'; import { ListEventComponent } from '../list-event/list-event.component'; import { ListPodComponent } from '../list-pod/list-pod.component'; -import { TplDetailService } from '../../common/tpl-detail/tpl-detail.service'; +import { TplDetailService } from '../../../shared/tpl-detail/tpl-detail.service'; import { AuthService } from '../../../shared/auth/auth.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Page } from '../../../shared/page/page-state'; diff --git a/src/frontend/src/app/portal/deployment/create-edit-deployment/create-edit-deployment.component.ts b/src/frontend/src/app/portal/deployment/create-edit-deployment/create-edit-deployment.component.ts index 3cc1ecd5c..2bf3e0c24 100644 --- a/src/frontend/src/app/portal/deployment/create-edit-deployment/create-edit-deployment.component.ts +++ b/src/frontend/src/app/portal/deployment/create-edit-deployment/create-edit-deployment.component.ts @@ -21,7 +21,6 @@ import { TranslateService } from '@ngx-translate/core'; }) export class CreateEditDeploymentComponent implements OnInit { - deploymentForm: NgForm; @ViewChild('deploymentForm') currentForm: NgForm; clusters: Cluster[]; @@ -29,13 +28,13 @@ export class CreateEditDeploymentComponent implements OnInit { resourcesMetas = new Resources(); title: string; deployment: Deployment = new Deployment(); - checkOnGoing: boolean = false; - isSubmitOnGoing: boolean = false; - isNameValid: boolean = true; + checkOnGoing = false; + isSubmitOnGoing = false; + isNameValid = true; actionType: ActionType; modalOpened: boolean; app: App; - isMaster: boolean = false; + isMaster = false; @Output() create = new EventEmitter(); @@ -52,7 +51,7 @@ export class CreateEditDeploymentComponent implements OnInit { this.clusters = clusters; this.clusterMetas = {}; if (this.clusters && this.clusters.length > 0) { - for (let clu of this.clusters) { + for (const clu of this.clusters) { this.clusterMetas[clu.name] = new ClusterMeta(false); } } @@ -62,11 +61,11 @@ export class CreateEditDeploymentComponent implements OnInit { this.deploymentService.getById(id, app.id).subscribe( status => { this.deployment = status.data; - let metaData = JSON.parse(this.deployment.metaData); + const metaData = JSON.parse(this.deployment.metaData ? this.deployment.metaData : '{}'); if (this.clusters && this.clusters.length > 0) { - let replicas = metaData['replicas']; - for (let clu of this.clusters) { - let culsterMeta = new ClusterMeta(false); + const replicas = metaData['replicas']; + for (const clu of this.clusters) { + const culsterMeta = new ClusterMeta(false); if (replicas && replicas[clu.name]) { culsterMeta.checked = true; culsterMeta.value = replicas[clu.name]; @@ -75,8 +74,9 @@ export class CreateEditDeploymentComponent implements OnInit { } } if ('resources' in metaData) { - for (let limit in metaData.resources) { - metaData.resources[limit] = /Percent$/.test(limit) ? parseFloat(metaData.resources[limit].replace(/%$/, '')) : parseFloat(metaData.resources[limit]); + for (const limit in metaData.resources) { + metaData.resources[limit] = /Percent$/.test(limit) ? + parseFloat(metaData.resources[limit].replace(/%$/, '')) : parseFloat(metaData.resources[limit]); } this.resourcesMetas = metaData.resources; } @@ -96,7 +96,7 @@ export class CreateEditDeploymentComponent implements OnInit { get replicaLimit(): number { let replicaLimit = defaultResources.replicaLimit; if (this.deployment && this.deployment.metaData) { - let metaData = JSON.parse(this.deployment.metaData); + const metaData = JSON.parse(this.deployment.metaData); if (metaData.resources && metaData.resources.replicaLimit) { replicaLimit = parseInt(metaData.resources.replicaLimit); @@ -109,7 +109,7 @@ export class CreateEditDeploymentComponent implements OnInit { } replicaValidation(cluster: string): boolean { - let clusterMeta = this.clusterMetas[cluster]; + const clusterMeta = this.clusterMetas[cluster]; if (this.deployment && this.deployment.metaData && clusterMeta) { if (!clusterMeta.checked) { return true; @@ -120,7 +120,7 @@ export class CreateEditDeploymentComponent implements OnInit { } resourcesValidation(resource: string): boolean { - let value = this.resourcesMetas[resource]; + const value = this.resourcesMetas[resource]; if (/Percent$/.test(resource) && value !== null) { if (value <= 0 || value > 100) { return false; @@ -143,21 +143,26 @@ export class CreateEditDeploymentComponent implements OnInit { if (!this.deployment.metaData) { this.deployment.metaData = '{}'; } - let metaData = JSON.parse(this.deployment.metaData); - let replicas = {}; - let resources = {}; - for (let clu of this.clusters) { - let clusterMeta = this.clusterMetas[clu.name]; + const metaData = JSON.parse(this.deployment.metaData); + const replicas = {}; + const resources = {}; + for (const clu of this.clusters) { + const clusterMeta = this.clusterMetas[clu.name]; if (clusterMeta && clusterMeta.checked && clusterMeta.value) { replicas[clu.name] = clusterMeta.value; } } - for (let resource in this.resourcesMetas) { - if (this.resourcesMetas[resource] !== null) resources[resource] = /Percent$/.test(resource) ? this.resourcesMetas[resource] + '%' : this.resourcesMetas[resource].toString(); + for (const resource in this.resourcesMetas) { + if (this.resourcesMetas[resource] !== null) { + resources[resource] = /Percent$/.test(resource) ? this.resourcesMetas[resource] + '%' : this.resourcesMetas[resource].toString(); + } } metaData.replicas = replicas; - if (Object.keys(resources).length) metaData.resources = resources; - else delete metaData.resources; + if (Object.keys(resources).length) { + metaData.resources = resources; + } else { + delete metaData.resources; + } return JSON.stringify(metaData); } @@ -218,8 +223,8 @@ export class CreateEditDeploymentComponent implements OnInit { } isResourcesValid(): boolean { - for (let resource in this.resourcesMetas) { - let value = this.resourcesMetas[resource]; + for (const resource in this.resourcesMetas) { + const value = this.resourcesMetas[resource]; if (/Percent$/.test(resource) && value !== null) { if (value <= 0 || value > 100) { return false; @@ -231,8 +236,8 @@ export class CreateEditDeploymentComponent implements OnInit { isClusterValid(): boolean { if (this.clusters) { - for (let clu of this.clusters) { - let clusterMeta = this.clusterMetas[clu.name]; + for (const clu of this.clusters) { + const clusterMeta = this.clusterMetas[clu.name]; if (clusterMeta && clusterMeta.checked && clusterMeta.value) { return true; } @@ -243,7 +248,7 @@ export class CreateEditDeploymentComponent implements OnInit { isClusterReplicaValid(): boolean { if (this.clusters) { - for (let clu of this.clusters) { + for (const clu of this.clusters) { if (!this.replicaValidation(clu.name)) { return false; } @@ -256,7 +261,7 @@ export class CreateEditDeploymentComponent implements OnInit { //Handle the form validation handleValidation(): void { - let cont = this.currentForm.controls['deployment_name']; + const cont = this.currentForm.controls['deployment_name']; if (cont) { this.isNameValid = cont.valid; } diff --git a/src/frontend/src/app/portal/deployment/create-edit-deploymenttpl/create-edit-deploymenttpl.component.ts b/src/frontend/src/app/portal/deployment/create-edit-deploymenttpl/create-edit-deploymenttpl.component.ts index eb657df0c..57a944720 100644 --- a/src/frontend/src/app/portal/deployment/create-edit-deploymenttpl/create-edit-deploymenttpl.component.ts +++ b/src/frontend/src/app/portal/deployment/create-edit-deploymenttpl/create-edit-deploymenttpl.component.ts @@ -90,7 +90,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, actionType: ActionType; deploymentTpl: DeploymentTpl = new DeploymentTpl(); - isSubmitOnGoing: boolean = false; + isSubmitOnGoing = false; app: App; deployment: Deployment; kubeDeployment: KubeDeployment = new KubeDeployment(); @@ -176,7 +176,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, get memoryLimit(): number { let memoryLimit = defaultResources.memoryLimit; if (this.deployment && this.deployment.metaData) { - let metaData = JSON.parse(this.deployment.metaData); + const metaData = JSON.parse(this.deployment.metaData); if (metaData.resources && metaData.resources.memoryLimit) { memoryLimit = parseInt(metaData.resources.memoryLimit); @@ -188,7 +188,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, get cpuLimit(): number { let cpuLimit = defaultResources.cpuLimit; if (this.deployment && this.deployment.metaData) { - let metaData = JSON.parse(this.deployment.metaData); + const metaData = JSON.parse(this.deployment.metaData); if (metaData.resources && metaData.resources.cpuLimit) { cpuLimit = parseInt(metaData.resources.cpuLimit); @@ -211,7 +211,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, } defaultContainer(): Container { - let container = new Container(); + const container = new Container(); container.resources = new ResourceRequirements(); container.resources.limits = {'memory': '', 'cpu': ''}; container.env = []; @@ -222,7 +222,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, // 初始化navigation数据 setContainDom(i) { - let dom = JSON.parse(JSON.stringify(containerDom)); + const dom = JSON.parse(JSON.stringify(containerDom)); dom.id += i ? i : ''; dom.child.forEach(item => { item.id += i ? i : ''; @@ -232,7 +232,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, initNavList() { this.naviList = null; - let naviList = JSON.parse(JSON.stringify(templateDom)); + const naviList = JSON.parse(JSON.stringify(templateDom)); for (let key = 0; key < this.containersLength; key++) { naviList[0].child.push(this.setContainDom(key)); } @@ -241,11 +241,11 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, ngOnInit(): void { this.initDefault(); - let appId = parseInt(this.route.parent.snapshot.params['id']); - let namespaceId = this.cacheService.namespaceId; - let deploymentId = parseInt(this.route.snapshot.params['deploymentId']); - let tplId = parseInt(this.route.snapshot.params['tplId']); - let observables = Array( + const appId = parseInt(this.route.parent.snapshot.params['id']); + const namespaceId = this.cacheService.namespaceId; + const deploymentId = parseInt(this.route.snapshot.params['deploymentId']); + const tplId = parseInt(this.route.snapshot.params['tplId']); + const observables = Array( this.appService.getById(appId, namespaceId), this.deploymentService.getById(deploymentId, appId) ); @@ -259,7 +259,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, response => { this.app = response[0].data; this.deployment = response[1].data; - let tpl = response[2]; + const tpl = response[2]; if (tpl) { this.deploymentTpl = tpl.data; @@ -284,21 +284,16 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, return labels; } - // 兼容旧的deployment - buildSelectorLabels(labels: {}) { - if (!labels) { - labels = {}; - } - labels[this.authService.config[appLabelKey]] = this.app.name; - labels['app'] = this.deployment.name; - delete labels[this.authService.config[namespaceLabelKey]]; - return labels; + buildSelectorLabels() { + const result = {}; + result['app'] = this.deployment.name; + return result; } fillDeploymentLabel(kubeDeployment: KubeDeployment): KubeDeployment { kubeDeployment.metadata.name = this.deployment.name; kubeDeployment.metadata.labels = this.buildLabels(this.kubeDeployment.metadata.labels); - kubeDeployment.spec.selector.matchLabels = this.buildSelectorLabels(this.kubeDeployment.spec.selector.matchLabels); + kubeDeployment.spec.selector.matchLabels = this.buildSelectorLabels(); kubeDeployment.spec.template.metadata.labels = this.buildLabels(this.kubeDeployment.spec.template.metadata.labels); return kubeDeployment; } @@ -464,14 +459,14 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, } normalPreStopExecSelected(i: number): boolean { - let preStop = this.kubeDeployment.spec.template.spec.containers[i].lifecycle.preStop; + const preStop = this.kubeDeployment.spec.template.spec.containers[i].lifecycle.preStop; return preStop && preStop.exec && preStop.exec.command && preStop.exec.command.length > 0 && preStop.exec.command[0] != this.defaultSafeExecCommand; } safeExitSelected(i: number): boolean { - let preStop = this.kubeDeployment.spec.template.spec.containers[i].lifecycle.preStop; + const preStop = this.kubeDeployment.spec.template.spec.containers[i].lifecycle.preStop; return preStop && preStop.exec && preStop.exec.command && preStop.exec.command.length > 0 && preStop.exec.command[0] == this.defaultSafeExecCommand; @@ -482,7 +477,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, } defaultEnv(type: number): EnvVar { - let env = new EnvVar(); + const env = new EnvVar(); switch (parseInt(type.toString())) { case 0: env.value = ''; @@ -502,7 +497,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, } defaultEnvFrom(type: number): EnvFromSource { - let envFrom = new EnvFromSource(); + const envFrom = new EnvFromSource(); switch (parseInt(type.toString())) { case 1: envFrom.configMapRef = new ConfigMapEnvSource(); @@ -568,11 +563,13 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, convertProbeCommandToArray(kubeDeployment: KubeDeployment): KubeDeployment { if (kubeDeployment.spec.template.spec.containers && kubeDeployment.spec.template.spec.containers.length > 0) { - for (let container of kubeDeployment.spec.template.spec.containers) { - if (container.livenessProbe && container.livenessProbe.exec && container.livenessProbe.exec.command && container.livenessProbe.exec.command.length > 0) { + for (const container of kubeDeployment.spec.template.spec.containers) { + if (container.livenessProbe && container.livenessProbe.exec && container.livenessProbe.exec.command + && container.livenessProbe.exec.command.length > 0) { container.livenessProbe.exec.command = container.livenessProbe.exec.command[0].split('\n'); } - if (container.readinessProbe && container.readinessProbe.exec && container.readinessProbe.exec.command && container.readinessProbe.exec.command.length > 0) { + if (container.readinessProbe && container.readinessProbe.exec && container.readinessProbe.exec.command + && container.readinessProbe.exec.command.length > 0) { container.readinessProbe.exec.command = container.readinessProbe.exec.command[0].split('\n'); } if (container.lifecycle) { @@ -612,7 +609,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, let cpuRequestLimitPercent = 0.5; let memoryRequestLimitPercent = 1; if (this.deployment.metaData) { - let metaData = JSON.parse(this.deployment.metaData); + const metaData = JSON.parse(this.deployment.metaData); if (metaData.resources && metaData.resources.cpuRequestLimitPercent) { if (metaData.resources.cpuRequestLimitPercent.indexOf('%') > -1) { cpuRequestLimitPercent = parseFloat(metaData.resources.cpuRequestLimitPercent.replace('%', '')) / 100; @@ -629,9 +626,9 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, } } - for (let container of kubeDeployment.spec.template.spec.containers) { - let memoryLimit = container.resources.limits['memory']; - let cpuLimit = container.resources.limits['cpu']; + for (const container of kubeDeployment.spec.template.spec.containers) { + const memoryLimit = container.resources.limits['memory']; + const cpuLimit = container.resources.limits['cpu']; if (!container.resources.requests) { container.resources.requests = {}; } @@ -650,10 +647,10 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, get totalFee() { let fee = 0; if (this.kubeDeployment.spec.template.spec.containers) { - for (let container of this.kubeDeployment.spec.template.spec.containers) { - let limit = container.resources.limits; - let cpu = limit['cpu']; - let memory = limit['memory']; + for (const container of this.kubeDeployment.spec.template.spec.containers) { + const limit = container.resources.limits; + const cpu = limit['cpu']; + const memory = limit['memory']; if (cpu) { fee += parseFloat(cpu) * this.cpuUnitPrice; } @@ -676,14 +673,16 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, convertProbeCommandToText(kubeDeployment: KubeDeployment) { if (kubeDeployment.spec.template.spec.containers && kubeDeployment.spec.template.spec.containers.length > 0) { - for (let container of kubeDeployment.spec.template.spec.containers) { - if (container.livenessProbe && container.livenessProbe.exec && container.livenessProbe.exec.command && container.livenessProbe.exec.command.length > 0) { - let commands = container.livenessProbe.exec.command; + for (const container of kubeDeployment.spec.template.spec.containers) { + if (container.livenessProbe && container.livenessProbe.exec && container.livenessProbe.exec.command + && container.livenessProbe.exec.command.length > 0) { + const commands = container.livenessProbe.exec.command; container.livenessProbe.exec.command = Array(); container.livenessProbe.exec.command.push(commands.join('\n')); } - if (container.readinessProbe && container.readinessProbe.exec && container.readinessProbe.exec.command && container.readinessProbe.exec.command.length > 0) { - let commands = container.readinessProbe.exec.command; + if (container.readinessProbe && container.readinessProbe.exec && container.readinessProbe.exec.command + && container.readinessProbe.exec.command.length > 0) { + const commands = container.readinessProbe.exec.command; container.readinessProbe.exec.command = Array(); container.readinessProbe.exec.command.push(commands.join('\n')); } @@ -692,7 +691,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, container.lifecycle.postStart.exec.command && container.lifecycle.postStart.exec.command.length > 0) { - let commands = container.lifecycle.postStart.exec.command; + const commands = container.lifecycle.postStart.exec.command; container.lifecycle.postStart.exec.command = Array(); container.lifecycle.postStart.exec.command.push(commands.join('\n')); @@ -703,7 +702,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, container.lifecycle.preStop.exec.command && container.lifecycle.preStop.exec.command.length > 0) { - let commands = container.lifecycle.preStop.exec.command; + const commands = container.lifecycle.preStop.exec.command; container.lifecycle.preStop.exec.command = Array(); container.lifecycle.preStop.exec.command.push(commands.join('\n')); @@ -725,7 +724,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, kubeDeployment.spec.strategy.rollingUpdate.maxUnavailable = 1; } if (kubeDeployment.spec.template.spec.containers && kubeDeployment.spec.template.spec.containers.length > 0) { - for (let container of kubeDeployment.spec.template.spec.containers) { + for (const container of kubeDeployment.spec.template.spec.containers) { if (!container.resources) { container.resources = ResourceRequirements.emptyObject(); } @@ -758,7 +757,7 @@ export class CreateEditDeploymentTplComponent implements OnInit, AfterViewInit, } getImagePrefixReg() { - let imagePrefix = this.authService.config['system.image-prefix']; + const imagePrefix = this.authService.config['system.image-prefix']; return imagePrefix; } diff --git a/src/frontend/src/app/portal/deployment/deployment.component.ts b/src/frontend/src/app/portal/deployment/deployment.component.ts index 3df870160..d2d122fad 100644 --- a/src/frontend/src/app/portal/deployment/deployment.component.ts +++ b/src/frontend/src/app/portal/deployment/deployment.component.ts @@ -178,7 +178,7 @@ export class DeploymentComponent implements OnInit, OnDestroy { let status = tpl.status[j]; // 错误超过俩次时候停止请求 if (status.errNum > 2) continue; - this.deploymentClient.get(this.appId, status.cluster, this.cacheService.kubeNamespace, tpl.name).subscribe( + this.deploymentClient.getDetail(this.appId, status.cluster, this.cacheService.kubeNamespace, tpl.name).subscribe( response => { let code = response.statusCode | response.status; if (code === httpStatusCode.NoContent) { diff --git a/src/frontend/src/app/portal/deployment/list-deployment/list-deployment.component.ts b/src/frontend/src/app/portal/deployment/list-deployment/list-deployment.component.ts index f5c3ffb4b..68bbebb2b 100644 --- a/src/frontend/src/app/portal/deployment/list-deployment/list-deployment.component.ts +++ b/src/frontend/src/app/portal/deployment/list-deployment/list-deployment.component.ts @@ -17,7 +17,7 @@ import { ListPodComponent } from '../list-pod/list-pod.component'; import { DeploymentStatus, DeploymentTpl, Event } from '../../../shared/model/v1/deploymenttpl'; import { DeploymentService } from '../../../shared/client/v1/deployment.service'; import { DeploymentTplService } from '../../../shared/client/v1/deploymenttpl.service'; -import { TplDetailService } from '../../common/tpl-detail/tpl-detail.service'; +import { TplDetailService } from '../../../shared/tpl-detail/tpl-detail.service'; import { AuthService } from '../../../shared/auth/auth.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Page } from '../../../shared/page/page-state'; diff --git a/src/frontend/src/app/portal/ingress/list-ingress/list-ingress.component.ts b/src/frontend/src/app/portal/ingress/list-ingress/list-ingress.component.ts index 50bffc5ed..8ee050faf 100644 --- a/src/frontend/src/app/portal/ingress/list-ingress/list-ingress.component.ts +++ b/src/frontend/src/app/portal/ingress/list-ingress/list-ingress.component.ts @@ -8,7 +8,6 @@ import { EventEmitter } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; import { IngressService } from '../../../shared/client/v1/ingress.service'; import { IngressTplService } from '../../../shared/client/v1/ingresstpl.service'; -import { TplDetailService } from '../../common/tpl-detail/tpl-detail.service'; import { MessageHandlerService } from '../../../shared/message-handler/message-handler.service'; import { AceEditorService } from '../../../shared/ace-editor/ace-editor.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -26,6 +25,7 @@ import { ResourcesActionType, TemplateState } from '../../../shared/shared.const'; +import { TplDetailService } from '../../../shared/tpl-detail/tpl-detail.service'; @Component({ @@ -112,7 +112,7 @@ export class ListIngressComponent implements OnInit, OnDestroy { this.ingressService.getById(tpl.ingressId, this.appId).subscribe( response => { const ingress = response.data; - this.publishTpl.newPublishTpl(ingress, tpl, ResourcesActionType.PUBLISH) + this.publishTpl.newPublishTpl(ingress, tpl, ResourcesActionType.PUBLISH); }, error => { this.messageHandlerService.handleError(error); @@ -123,7 +123,7 @@ export class ListIngressComponent implements OnInit, OnDestroy { ingressState(status: PublishStatus, tpl: IngressTpl) { if (status.cluster && status.state !== TemplateState.NOT_FOUND) { - this.ingressStatus.newIngressStatus(status.cluster, tpl) + this.ingressStatus.newIngressStatus(status.cluster, tpl); } } diff --git a/src/frontend/src/app/portal/persistentvolumeclaim/list-persistentvolumeclaim/list-persistentvolumeclaim.component.ts b/src/frontend/src/app/portal/persistentvolumeclaim/list-persistentvolumeclaim/list-persistentvolumeclaim.component.ts index b370c6ca2..61330b131 100644 --- a/src/frontend/src/app/portal/persistentvolumeclaim/list-persistentvolumeclaim/list-persistentvolumeclaim.component.ts +++ b/src/frontend/src/app/portal/persistentvolumeclaim/list-persistentvolumeclaim/list-persistentvolumeclaim.component.ts @@ -14,7 +14,7 @@ import { import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { Subscription } from 'rxjs/Subscription'; import { MessageHandlerService } from '../../../shared/message-handler/message-handler.service'; -import { TplDetailService } from '../../common/tpl-detail/tpl-detail.service'; +import { TplDetailService } from '../../../shared/tpl-detail/tpl-detail.service'; import { AuthService } from '../../../shared/auth/auth.service'; import { PersistentVolumeClaimTplService } from '../../../shared/client/v1/persistentvolumeclaimtpl.service'; import { PersistentVolumeClaimTpl } from '../../../shared/model/v1/persistentvolumeclaimtpl'; @@ -48,9 +48,9 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { appId: number; pvcId: number; state: State; - currentPage: number = 1; + currentPage = 1; pageState: PageState = new PageState(); - isOnline: boolean = false; + isOnline = false; loading: boolean; pvcTpls: PersistentVolumeClaimTpl[]; publishStatus: PublishStatus[]; @@ -61,7 +61,7 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { subscription: Subscription; isOnlineObservable: Subscription; - componentName: string = 'PVC'; + componentName = 'PVC'; constructor(private pvcTplService: PersistentVolumeClaimTplService, private tplDetailService: TplDetailService, @@ -85,7 +85,7 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { if (message && message.state === ConfirmationState.CONFIRMED && message.source === ConfirmationTargets.PERSISTENT_VOLUME_CLAIM_TPL) { - let tplId = message.data; + const tplId = message.data; this.pvcTplService.deleteById(tplId, this.appId) .subscribe( response => { @@ -114,19 +114,19 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { syncStatus(): void { if (this.pvcTpls && this.pvcTpls.length > 0) { for (let i = 0; i < this.pvcTpls.length; i++) { - let tpl = this.pvcTpls[i]; + const tpl = this.pvcTpls[i]; if (tpl.status && tpl.status.length > 0) { for (let j = 0; j < tpl.status.length; j++) { - let status = tpl.status[j]; + const status = tpl.status[j]; this.pvcClient.get(this.appId, status.cluster, this.cacheService.kubeNamespace, tpl.name).subscribe( response => { - let code = response.statusCode | response.status; + const code = response.statusCode | response.status; if (code === httpStatusCode.NoContent) { this.pvcTpls[i].status[j].state = TemplateState.NOT_FOUND; return; } - let pvc = response.data; + const pvc = response.data; this.pvcTpls[i].status[j].pvc = pvc; if (response.data && this.pvcTpls && @@ -154,7 +154,7 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { pvcFileSystemStatus(publishStatus: PublishStatus) { - if (publishStatus.state == TemplateState.SUCCESS) { + if (publishStatus.state === TemplateState.SUCCESS) { this.persistentVolumeClaimRobinClient.getStatus(this.appId, publishStatus.cluster, publishStatus.pvc.metadata.namespace, publishStatus.pvc.metadata.name).subscribe( response => { publishStatus.fileSystemStatus = response.data; @@ -168,23 +168,23 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { } fileSystemState(fileSystemStatus: PersistentVolumeClaimFileSystemStatus) { - let status = Array(); + const status = Array(); if (fileSystemStatus) { if (isArrayNotEmpty(fileSystemStatus.status)) { - for (let state of fileSystemStatus.status) { - if (state == 'Mount') { + for (const state of fileSystemStatus.status) { + if (state === 'Mount') { status.push('已激活'); } - if (state == 'LoginForbidden') { + if (state === 'LoginForbidden') { status.push('禁止登录'); } - if (state == 'Verifying') { + if (state === 'Verifying') { status.push('校验中'); } - if (state == 'VerifyOk') { + if (state === 'VerifyOk') { status.push('校验成功'); } - if (state == 'VerifyFailed') { + if (state === 'VerifyFailed') { status.push('校验失败'); } } @@ -200,8 +200,8 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { activedFileSystem(fileSystemStatus: PersistentVolumeClaimFileSystemStatus) { if (fileSystemStatus) { if (isArrayNotEmpty(fileSystemStatus.status)) { - for (let state of fileSystemStatus.status) { - if (state == 'Mount') { + for (const state of fileSystemStatus.status) { + if (state === 'Mount') { return true; } } @@ -213,8 +213,8 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { containState(fileSystemStatus: PersistentVolumeClaimFileSystemStatus, state: string) { if (fileSystemStatus) { if (isArrayNotEmpty(fileSystemStatus.status)) { - for (let fstate of fileSystemStatus.status) { - if (fstate == state) { + for (const fstate of fileSystemStatus.status) { + if (fstate === state) { return true; } } @@ -225,7 +225,8 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { activePv(status: PublishStatus) { this.loading = true; - this.persistentVolumeClaimRobinClient.activeRbdImage(this.appId, status.cluster, status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( + this.persistentVolumeClaimRobinClient.activeRbdImage(this.appId, status.cluster, + status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( response => { this.syncStatus(); this.messageHandlerService.showSuccess('激活成功!'); @@ -239,7 +240,8 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { inActivePv(status: PublishStatus) { this.loading = true; - this.persistentVolumeClaimRobinClient.inActiveRbdImage(this.appId, status.cluster, status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( + this.persistentVolumeClaimRobinClient.inActiveRbdImage(this.appId, status.cluster, + status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( response => { this.syncStatus(); this.messageHandlerService.showSuccess('取消激活成功!'); @@ -253,7 +255,8 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { offlineImageUser(status: PublishStatus) { this.loading = true; - this.persistentVolumeClaimRobinClient.offlineRbdImageUser(this.appId, status.cluster, status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( + this.persistentVolumeClaimRobinClient.offlineRbdImageUser(this.appId, status.cluster, + status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( response => { this.syncStatus(); this.messageHandlerService.showSuccess('下线所有用户成功!'); @@ -267,7 +270,8 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { loginInfo(status: PublishStatus) { - this.persistentVolumeClaimRobinClient.loginInfo(this.appId, status.cluster, status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( + this.persistentVolumeClaimRobinClient.loginInfo(this.appId, status.cluster, + status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( response => { this.userInfoComponent.openModal(response.data); }, @@ -280,7 +284,8 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { verifyFileSystem(status: PublishStatus) { this.loading = true; - this.persistentVolumeClaimRobinClient.verify(this.appId, status.cluster, status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( + this.persistentVolumeClaimRobinClient.verify(this.appId, status.cluster, + status.pvc.metadata.namespace, status.pvc.metadata.name).subscribe( response => { this.messageHandlerService.showSuccess('发送校验请求成功!'); this.loading = false; @@ -322,7 +327,8 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { } clonePvcTpl(tpl: PersistentVolumeClaimTpl) { - this.router.navigate([`portal/namespace/${this.cacheService.namespaceId}/app/${this.appId}/persistentvolumeclaim/${this.pvcId}/tpl/${tpl.id}`]); + this.router.navigate([ + `portal/namespace/${this.cacheService.namespaceId}/app/${this.appId}/persistentvolumeclaim/${this.pvcId}/tpl/${tpl.id}`]); } detailPvcTpl(tpl: PersistentVolumeClaimTpl) { @@ -338,7 +344,7 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { } deletePvcTpl(tpl: PersistentVolumeClaimTpl): void { - let deletionMessage = new ConfirmationMessage( + const deletionMessage = new ConfirmationMessage( '删除' + this.componentName + '模版确认', `你确认删除` + this.componentName + `${tpl.name}?`, tpl.id, @@ -363,11 +369,11 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { this.publishService.listStatus(PublishType.PERSISTENT_VOLUME_CLAIM, this.pvcId) ).subscribe( response => { - let status = response[1].data; + const status = response[1].data; this.publishStatus = status; - let tplStatusMap = {}; + const tplStatusMap = {}; if (status && status.length > 0) { - for (let state of status) { + for (const state of status) { if (!tplStatusMap[state.templateId]) { tplStatusMap[state.templateId] = Array(); } @@ -376,7 +382,7 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { } this.tplStatusMap = tplStatusMap; - let tpls = response[0].data; + const tpls = response[0].data; this.buildTplList(tpls.list); this.pvcTpls = tpls.list; this.pageState.page.totalPage = tpls.totalPage; @@ -389,11 +395,11 @@ export class ListPersistentVolumeClaimComponent implements OnInit, OnDestroy { buildTplList(pvcTpls: PersistentVolumeClaimTpl[]) { if (pvcTpls) { - for (let tpl of pvcTpls) { - let metaData = tpl.metaData ? tpl.metaData : '{}'; + for (const tpl of pvcTpls) { + const metaData = tpl.metaData ? tpl.metaData : '{}'; tpl.clusters = JSON.parse(metaData).clusters; - let publishStatus = this.tplStatusMap[tpl.id]; + const publishStatus = this.tplStatusMap[tpl.id]; if (publishStatus && publishStatus.length > 0) { tpl.status = publishStatus; } diff --git a/src/frontend/src/app/portal/portal.module.ts b/src/frontend/src/app/portal/portal.module.ts index f5dedd572..a6bae6186 100644 --- a/src/frontend/src/app/portal/portal.module.ts +++ b/src/frontend/src/app/portal/portal.module.ts @@ -15,7 +15,7 @@ import { CacheService } from '../shared/auth/cache.service'; import { PublishHistoryService } from './common/publish-history/publish-history.service'; import { AppUserModule } from './app-user/app-user.module'; import { NamespaceUserModule } from './namespace-user/namespace-user.module'; -import { TplDetailService } from './common/tpl-detail/tpl-detail.service'; +import { TplDetailService } from '../shared/tpl-detail/tpl-detail.service'; import { PersistentVolumeClaimModule } from './persistentvolumeclaim/persistentvolumeclaim.module'; import { NamespaceApiKeyModule } from './namespace-apikey/apikey.module'; import { AppApiKeyModule } from './app-apikey/apikey.module'; @@ -28,7 +28,7 @@ import { PodLoggingComponent } from './pod-logging/pod-logging.component'; import { NamespaceReportModule } from './namespace-report/namespace-report.module'; import { BaseAppModule } from './base/base-app.module'; import { PublishHistoryModule } from './common/publish-history/publish-history.module'; -import { TplDetailModule } from './common/tpl-detail/tpl-detail.module'; +import { TplDetailModule } from '../shared/tpl-detail/tpl-detail.module'; import { MarkdownModule } from 'ngx-markdown'; import { LibraryPortalModule } from '../../../lib/portal/library-portal.module'; diff --git a/src/frontend/src/app/portal/secret/list-secret/list-secret.component.ts b/src/frontend/src/app/portal/secret/list-secret/list-secret.component.ts index 91939afcd..f7534cb0c 100644 --- a/src/frontend/src/app/portal/secret/list-secret/list-secret.component.ts +++ b/src/frontend/src/app/portal/secret/list-secret/list-secret.component.ts @@ -9,7 +9,7 @@ import { PublishSecretTplComponent } from '../publish-tpl/publish-tpl.component' import { Secret } from '../../../shared/model/v1/secret'; import { SecretTpl } from '../../../shared/model/v1/secrettpl'; import { SecretTplService } from '../../../shared/client/v1/secrettpl.service'; -import { TplDetailService } from '../../common/tpl-detail/tpl-detail.service'; +import { TplDetailService } from '../../../shared/tpl-detail/tpl-detail.service'; import { AuthService } from '../../../shared/auth/auth.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Page } from '../../../shared/page/page-state'; @@ -31,14 +31,14 @@ export class ListSecretComponent implements OnInit, OnDestroy { @Input() page: Page; @Input() appId: number; state: State; - currentPage: number = 1; + currentPage = 1; @Output() paginate = new EventEmitter(); @Output() secretTab = new EventEmitter(); @Output() cloneTpl = new EventEmitter(); subscription: Subscription; - componentName: string = '加密字典'; + componentName = '加密字典'; constructor(private secretTplService: SecretTplService, private tplDetailService: TplDetailService, @@ -52,7 +52,7 @@ export class ListSecretComponent implements OnInit, OnDestroy { if (message && message.state === ConfirmationState.CONFIRMED && message.source === ConfirmationTargets.SECRET_TPL) { - let tplId = message.data; + const tplId = message.data; this.secretTplService.deleteById(tplId, this.appId) .subscribe( response => { @@ -104,7 +104,7 @@ export class ListSecretComponent implements OnInit, OnDestroy { } deleteSecretTpl(tpl: SecretTpl): void { - let deletionMessage = new ConfirmationMessage( + const deletionMessage = new ConfirmationMessage( '删除' + this.componentName + '模版确认', `你确认删除` + this.componentName + `${tpl.name}?`, tpl.id, diff --git a/src/frontend/src/app/portal/statefulset/list-statefulset/list-statefulset.component.ts b/src/frontend/src/app/portal/statefulset/list-statefulset/list-statefulset.component.ts index 69451c947..a98b48c7c 100644 --- a/src/frontend/src/app/portal/statefulset/list-statefulset/list-statefulset.component.ts +++ b/src/frontend/src/app/portal/statefulset/list-statefulset/list-statefulset.component.ts @@ -14,7 +14,7 @@ import { Subscription } from 'rxjs/Subscription'; import { PublishStatefulsetTplComponent } from '../publish-tpl/publish-tpl.component'; import { ListEventComponent } from '../list-event/list-event.component'; import { ListPodComponent } from '../list-pod/list-pod.component'; -import { TplDetailService } from '../../common/tpl-detail/tpl-detail.service'; +import { TplDetailService } from '../../../shared/tpl-detail/tpl-detail.service'; import { AuthService } from '../../../shared/auth/auth.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Page } from '../../../shared/page/page-state'; diff --git a/src/frontend/src/app/shared/client/v1/kubernetes/base-client.ts b/src/frontend/src/app/shared/client/v1/kubernetes/base-client.ts new file mode 100644 index 000000000..b28397cca --- /dev/null +++ b/src/frontend/src/app/shared/client/v1/kubernetes/base-client.ts @@ -0,0 +1,36 @@ +import { PageState } from '../../../page/page-state'; +import { HttpParams } from '@angular/common/http'; +import { isNotEmpty } from '../../../utils'; + +export class BaseClient { + static buildParam(pageState: PageState): HttpParams { + let params = new HttpParams(); + params = params.set('pageNo', pageState.page.pageNo + ''); + params = params.set('pageSize', pageState.page.pageSize + ''); + // query param + Object.getOwnPropertyNames(pageState.params).map(key => { + const value = pageState.params[key]; + if (isNotEmpty(value)) { + params = params.set(key, value); + } + }); + + const filterList: Array = []; + Object.getOwnPropertyNames(pageState.filters).map(key => { + const value = pageState.filters[key]; + if (isNotEmpty(value)) { + filterList.push(`${key}=${value}`); + } + }); + if (filterList.length) { + params = params.set('filter', filterList.join(',')); + } + // sort param + if (Object.keys(pageState.sort).length !== 0) { + const sortType: any = pageState.sort.reverse ? `-${pageState.sort.by}` : pageState.sort.by; + params = params.set('sortby', sortType); + } + + return params; + } +} diff --git a/src/frontend/src/app/shared/client/v1/kubernetes/deployment.ts b/src/frontend/src/app/shared/client/v1/kubernetes/deployment.ts index 12c6c747b..c80d235eb 100644 --- a/src/frontend/src/app/shared/client/v1/kubernetes/deployment.ts +++ b/src/frontend/src/app/shared/client/v1/kubernetes/deployment.ts @@ -1,18 +1,38 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { HttpClient } from '@angular/common/http'; +import { PageState } from '../../../page/page-state'; +import { BaseClient } from './base-client'; @Injectable() export class DeploymentClient { constructor(private http: HttpClient) { } + listPage(pageState: PageState, cluster: string, namespace: string, appId?: string): Observable { + const params = BaseClient.buildParam(pageState); + + if ((typeof (appId) === 'undefined') || (!appId)) { + appId = '0'; + } + + return this.http + .get(`/api/v1/kubernetes/apps/${appId}/deployments/namespaces/${namespace}/clusters/${cluster}`, {params: params}) + .catch(error => Observable.throw(error)); + } + deploy(appId: number, cluster: string, resourceId: number, tplId: number, template: any): Observable { return this.http .post(`/api/v1/kubernetes/apps/${appId}/deployments/${resourceId}/tpls/${tplId}/clusters/${cluster}`, template) .catch(error => Observable.throw(error)); } + getDetail(appId: number, cluster: string, namespace: string, name: string): Observable { + return this.http + .get(`/api/v1/kubernetes/apps/${appId}/deployments/${name}/detail/namespaces/${namespace}/clusters/${cluster}`) + .catch(error => Observable.throw(error)); + } + get(appId: number, cluster: string, namespace: string, name: string): Observable { return this.http .get(`/api/v1/kubernetes/apps/${appId}/deployments/${name}/namespaces/${namespace}/clusters/${cluster}`) diff --git a/src/frontend/src/app/shared/model/v1/deployment-list.ts b/src/frontend/src/app/shared/model/v1/deployment-list.ts new file mode 100644 index 000000000..a7bd71489 --- /dev/null +++ b/src/frontend/src/app/shared/model/v1/deployment-list.ts @@ -0,0 +1,154 @@ +/* Do not change, this code is generated from Golang structs */ + + +export class TypeMeta { + kind: string; + + constructor(init?: TypeMeta) { + if (!init) { return; } + if (init.kind) { this.kind = init.kind; } + } + + + static emptyObject(): TypeMeta { + const result = new TypeMeta(); + return result; + } + +} + +export class Event { + objectMeta: ObjectMeta; + typeMeta: TypeMeta; + message: string; + sourceComponent: string; + name: string; + object: string; + count: number; + firstSeen: Time; + lastSeen: Time; + reason: string; + type: string; + + constructor(init?: Event) { + if (!init) { return; } + if (init.objectMeta) { this.objectMeta = init.objectMeta; } + if (init.typeMeta) { this.typeMeta = init.typeMeta; } + if (init.message) { this.message = init.message; } + if (init.sourceComponent) { this.sourceComponent = init.sourceComponent; } + if (init.name) { this.name = init.name; } + if (init.object) { this.object = init.object; } + if (init.count) { this.count = init.count; } + if (init.firstSeen) { this.firstSeen = init.firstSeen; } + if (init.lastSeen) { this.lastSeen = init.lastSeen; } + if (init.reason) { this.reason = init.reason; } + if (init.type) { this.type = init.type; } + } + + + static emptyObject(): Event { + const result = new Event(); + result.objectMeta = ObjectMeta.emptyObject(); + result.typeMeta = TypeMeta.emptyObject(); + result.firstSeen = Time.emptyObject(); + result.lastSeen = Time.emptyObject(); + return result; + } + +} + +export class PodInfo { + current: number; + desired: number; + running: number; + pending: number; + failed: number; + succeeded: number; + warnings: Event[]; + + constructor(init?: PodInfo) { + if (!init) { return; } + if (init.current) { this.current = init.current; } + if (init.desired) { this.desired = init.desired; } + if (init.running) { this.running = init.running; } + if (init.pending) { this.pending = init.pending; } + if (init.failed) { this.failed = init.failed; } + if (init.succeeded) { this.succeeded = init.succeeded; } + if (init.warnings) { this.warnings = init.warnings; } + } + + + static emptyObject(): PodInfo { + const result = new PodInfo(); + result.warnings = []; + return result; + } + +} + +export class Time { + Time: Date; + + constructor(init?: Time) { + if (!init) { return; } + if (init.Time) { this.Time = new Date(init.Time as any); } + } + + + static emptyObject(): Time { + const result = new Time(); + result.Time = null; + return result; + } + +} + +export class ObjectMeta { + name: string; + namespace: string; + labels?: { [key: string]: string }; + annotations?: { [key: string]: string }; + creationTimestamp: Time; + + constructor(init?: ObjectMeta) { + if (!init) { return; } + if (init.name) { this.name = init.name; } + if (init.namespace) { this.namespace = init.namespace; } + if (init.labels) { this.labels = init.labels; } + if (init.annotations) { this.annotations = init.annotations; } + if (init.creationTimestamp) { this.creationTimestamp = init.creationTimestamp; } + } + + + static emptyObject(): ObjectMeta { + const result = new ObjectMeta(); + result.labels = null; + result.annotations = null; + result.creationTimestamp = Time.emptyObject(); + return result; + } + +} + +export class DeploymentList { + objectMeta: ObjectMeta; + pods: PodInfo; + containers: string[]; + + constructor(init?: DeploymentList) { + if (!init) { return; } + if (init.objectMeta) { this.objectMeta = init.objectMeta; } + if (init.pods) { this.pods = init.pods; } + if (init.containers) { this.containers = init.containers; } + } + + + static emptyObject(): DeploymentList { + const result = new DeploymentList(); + result.objectMeta = ObjectMeta.emptyObject(); + result.pods = PodInfo.emptyObject(); + result.containers = []; + return result; + } + +} diff --git a/src/frontend/src/app/shared/shared.const.ts b/src/frontend/src/app/shared/shared.const.ts index db927c6da..85196dc5e 100644 --- a/src/frontend/src/app/shared/shared.const.ts +++ b/src/frontend/src/app/shared/shared.const.ts @@ -10,6 +10,8 @@ export const KubeApiTypePersistentVolumeClaim = 'PersistentVolumeClaim'; export const LoginTokenKey = 'wayne_token'; +export const AdminDefaultApiId = 0; + export const enum AlertType { DANGER, WARNING, INFO, SUCCESS } diff --git a/src/frontend/src/app/portal/common/tpl-detail/tpl-detail.component.html b/src/frontend/src/app/shared/tpl-detail/tpl-detail.component.html similarity index 100% rename from src/frontend/src/app/portal/common/tpl-detail/tpl-detail.component.html rename to src/frontend/src/app/shared/tpl-detail/tpl-detail.component.html diff --git a/src/frontend/src/app/portal/common/tpl-detail/tpl-detail.component.ts b/src/frontend/src/app/shared/tpl-detail/tpl-detail.component.ts similarity index 90% rename from src/frontend/src/app/portal/common/tpl-detail/tpl-detail.component.ts rename to src/frontend/src/app/shared/tpl-detail/tpl-detail.component.ts index 1993aa229..7abcf8616 100644 --- a/src/frontend/src/app/portal/common/tpl-detail/tpl-detail.component.ts +++ b/src/frontend/src/app/shared/tpl-detail/tpl-detail.component.ts @@ -11,7 +11,7 @@ import { Subscription } from 'rxjs/Subscription'; export class TplDetailComponent implements OnInit, OnDestroy { modalOpened: boolean; text: string; - title: string = 'release_explain'; + title = 'release_explain'; textSub: Subscription; constructor(private tplDetailService: TplDetailService) { @@ -28,7 +28,7 @@ export class TplDetailComponent implements OnInit, OnDestroy { msg => { this.modalOpened = true; this.text = msg.text; - if (msg.title) this.title = msg.title; + if (msg.title) { this.title = msg.title; } } ); } diff --git a/src/frontend/src/app/portal/common/tpl-detail/tpl-detail.module.ts b/src/frontend/src/app/shared/tpl-detail/tpl-detail.module.ts similarity index 81% rename from src/frontend/src/app/portal/common/tpl-detail/tpl-detail.module.ts rename to src/frontend/src/app/shared/tpl-detail/tpl-detail.module.ts index 4524763a3..0331dcb08 100644 --- a/src/frontend/src/app/portal/common/tpl-detail/tpl-detail.module.ts +++ b/src/frontend/src/app/shared/tpl-detail/tpl-detail.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { TplDetailComponent } from './tpl-detail.component'; -import { SharedModule } from '../../../shared/shared.module'; +import { SharedModule } from '../shared.module'; @NgModule({ imports: [ diff --git a/src/frontend/src/app/portal/common/tpl-detail/tpl-detail.scss b/src/frontend/src/app/shared/tpl-detail/tpl-detail.scss similarity index 100% rename from src/frontend/src/app/portal/common/tpl-detail/tpl-detail.scss rename to src/frontend/src/app/shared/tpl-detail/tpl-detail.scss diff --git a/src/frontend/src/app/portal/common/tpl-detail/tpl-detail.service.ts b/src/frontend/src/app/shared/tpl-detail/tpl-detail.service.ts similarity index 83% rename from src/frontend/src/app/portal/common/tpl-detail/tpl-detail.service.ts rename to src/frontend/src/app/shared/tpl-detail/tpl-detail.service.ts index 1e4d1f981..c9c05102e 100644 --- a/src/frontend/src/app/portal/common/tpl-detail/tpl-detail.service.ts +++ b/src/frontend/src/app/shared/tpl-detail/tpl-detail.service.ts @@ -15,9 +15,9 @@ export class TplDetailService { text$ = this.text.asObservable(); openModal(text: string, title?: string) { - let msg = new Message(); + const msg = new Message(); msg.text = text; - if (title) msg.title = title; + if (title) { msg.title = title; } this.text.next(msg); }