U refactor

This commit is contained in:
zhenorzz 2022-11-23 16:49:55 +08:00
parent 2dcc8d68d1
commit 05c15c06ab
38 changed files with 6536 additions and 205 deletions

View File

@ -356,7 +356,7 @@ remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
for which you have or can give appropriate copyright router.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of

154
cmd/server/api/agent.go Normal file
View File

@ -0,0 +1,154 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"github.com/zhenorzz/goploy/cmd/server/api/middleware"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"net/http"
)
type Agent API
func (a Agent) Handler() []server.Route {
return []server.Route{
server.NewWhiteRoute("/agent/report", http.MethodPost, a.Report).Middleware(middleware.CheckSign),
server.NewWhiteRoute("/agent/getServerID", http.MethodPost, a.GetServerID).Middleware(middleware.CheckSign),
server.NewWhiteRoute("/agent/getCronList", http.MethodPost, a.GetCronList).Middleware(middleware.CheckSign),
server.NewWhiteRoute("/agent/getCronLogs", http.MethodPost, a.GetCronLogs).Middleware(middleware.CheckSign),
server.NewWhiteRoute("/agent/cronReport", http.MethodPost, a.CronReport).Middleware(middleware.CheckSign),
}
}
func (Agent) GetServerID(gp *server.Goploy) server.Response {
type ReqData struct {
Name string `json:"name"`
IP string `json:"ip"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
s, err := model.Server{
Name: reqData.Name,
IP: reqData.IP,
}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: s.ID},
}
}
func (Agent) GetCronList(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `json:"serverId" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
crons, err := model.Cron{ServerID: reqData.ServerID}.GetList()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.Crons `json:"list"`
}{List: crons},
}
}
func (Agent) GetCronLogs(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `json:"serverId" validate:"gt=0"`
CronID int64 `json:"cronId" validate:"gt=0"`
Page uint64 `json:"page" validate:"gt=0"`
Rows uint64 `json:"rows" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
crons, err := model.CronLog{ServerID: reqData.ServerID, CronID: reqData.CronID}.GetList(reqData.Page, reqData.Rows)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.CronLogs `json:"list"`
}{List: crons},
}
}
func (Agent) CronReport(gp *server.Goploy) server.Response {
type ReqData struct {
ServerId int64 `json:"serverId" validate:"gt=0"`
CronId int64 `json:"cronId" validate:"gt=0"`
ExecCode int `json:"execCode"`
Message string `json:"message" validate:"required"`
ReportTime string `json:"reportTime" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err := model.CronLog{
ServerID: reqData.ServerId,
CronID: reqData.CronId,
ExecCode: reqData.ExecCode,
Message: reqData.Message,
ReportTime: reqData.ReportTime,
}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Agent) Report(gp *server.Goploy) server.Response {
type ReqData struct {
ServerId int64 `json:"serverId" validate:"gt=0"`
Type int `json:"type" validate:"gt=0"`
Item string `json:"item" validate:"required"`
Value string `json:"value" validate:"required"`
ReportTime string `json:"reportTime" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err := model.ServerAgentLog{
ServerID: reqData.ServerId,
Type: reqData.Type,
Item: reqData.Item,
Value: reqData.Value,
ReportTime: reqData.ReportTime,
}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}

110
cmd/server/api/api.go Normal file
View File

@ -0,0 +1,110 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"encoding/json"
"errors"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
"github.com/gorilla/schema"
"gopkg.in/go-playground/validator.v9"
enTranslations "gopkg.in/go-playground/validator.v9/translations/en"
"reflect"
"strings"
"unicode"
)
type API struct{}
// Validate use a single instance of Validate, it caches struct info
var Validate *validator.Validate
// Trans Translator
var Trans ut.Translator
func init() {
english := en.New()
uni := ut.New(english, english)
Trans, _ = uni.GetTranslator("english")
Validate = validator.New()
enTranslations.RegisterDefaultTranslations(Validate, Trans)
Validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
Validate.RegisterValidation("password", func(fl validator.FieldLevel) bool {
password := fl.Field().String()
if len(password) < 8 || len(password) > 16 {
return false
}
var (
hasLetter = false
hasNumber = false
hasSpecial = false
)
for _, char := range password {
switch {
case unicode.IsLetter(char):
hasLetter = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if hasLetter && hasNumber {
return true
} else if hasLetter && hasSpecial {
return true
} else if hasNumber && hasSpecial {
return true
} else {
return false
}
})
_ = Validate.RegisterTranslation("password", Trans, func(ut ut.Translator) error {
return ut.Add("password", "{0} policy is min:8, max:16 and at least one alpha and at least one special char!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("password", fe.Field())
return t
})
}
var decoder = schema.NewDecoder()
func decodeJson(data []byte, v interface{}) error {
err := json.Unmarshal(data, v)
if err != nil {
return err
}
if err := Validate.Struct(v); err != nil {
for _, err := range err.(validator.ValidationErrors) {
return errors.New(err.Translate(Trans))
}
}
return nil
}
func decodeQuery(data map[string][]string, v interface{}) error {
decoder.IgnoreUnknownKeys(true)
err := decoder.Decode(v, data)
if err != nil {
return err
}
if err := Validate.Struct(v); err != nil {
for _, err := range err.(validator.ValidationErrors) {
return errors.New(err.Translate(Trans))
}
}
return nil
}

155
cmd/server/api/cron.go Normal file
View File

@ -0,0 +1,155 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"github.com/zhenorzz/goploy/cmd/server/api/middleware"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"net/http"
)
// Cron struct
type Cron API
func (c Cron) Handler() []server.Route {
return []server.Route{
server.NewRoute("/cron/getList", http.MethodPost, c.GetList).Permissions(config.ShowCronPage),
server.NewRoute("/cron/getLogs", http.MethodPost, c.GetLogs).Permissions(config.ShowCronPage),
server.NewRoute("/cron/add", http.MethodPost, c.Add).Permissions(config.AddCron).LogFunc(middleware.AddOPLog),
server.NewRoute("/cron/edit", http.MethodPut, c.Edit).Permissions(config.EditCron).LogFunc(middleware.AddOPLog),
server.NewRoute("/cron/remove", http.MethodDelete, c.Remove).Permissions(config.DeleteCron).LogFunc(middleware.AddOPLog),
}
}
func (Cron) GetList(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `json:"serverId" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
crons, err := model.Cron{ServerID: reqData.ServerID}.GetList()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.Crons `json:"list"`
}{List: crons},
}
}
func (Cron) GetLogs(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `schema:"serverId" validate:"gt=0"`
CronID int64 `schema:"cronId" validate:"gt=0"`
Page uint64 `schema:"page" validate:"gt=0"`
Rows uint64 `schema:"rows" validate:"gt=0"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
crons, err := model.CronLog{ServerID: reqData.ServerID, CronID: reqData.CronID}.GetList(reqData.Page, reqData.Rows)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.CronLogs `json:"list"`
}{List: crons},
}
}
func (Cron) Add(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `json:"serverId" validate:"gt=0"`
Expression string `json:"expression" validate:"required"`
Command string `json:"command" validate:"required"`
SingleMode uint8 `json:"singleMode" validate:"gte=0"`
LogLevel uint8 `json:"logLevel" validate:"gte=0"`
Description string `json:"description" validate:"max=255"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
id, err := model.Cron{
ServerID: reqData.ServerID,
Expression: reqData.Expression,
Command: reqData.Command,
SingleMode: reqData.SingleMode,
LogLevel: reqData.LogLevel,
Description: reqData.Description,
Creator: gp.UserInfo.Name,
}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: id},
}
}
func (Cron) Edit(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
Expression string `json:"expression" validate:"required"`
Command string `json:"command" validate:"required"`
SingleMode uint8 `json:"singleMode" validate:"gte=0"`
LogLevel uint8 `json:"logLevel" validate:"gte=0"`
Description string `json:"description" validate:"max=255"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err := model.Cron{
ID: reqData.ID,
Expression: reqData.Expression,
Command: reqData.Command,
SingleMode: reqData.SingleMode,
LogLevel: reqData.LogLevel,
Description: reqData.Description,
Editor: gp.UserInfo.Name,
}.EditRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Cron) Remove(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.Cron{ID: reqData.ID}).RemoveRow(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}

821
cmd/server/api/deploy.go Normal file
View File

@ -0,0 +1,821 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"bytes"
"crypto/md5"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/pkg/sftp"
"github.com/zhenorzz/goploy/cmd/server/api/middleware"
"github.com/zhenorzz/goploy/cmd/server/task"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/internal/pkg/cmd"
"github.com/zhenorzz/goploy/internal/repo"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
)
type Deploy API
func (d Deploy) Handler() []server.Route {
return []server.Route{
server.NewRoute("/deploy/getList", http.MethodGet, d.GetList).Permissions(config.ShowDeployPage),
server.NewRoute("/deploy/getPublishTrace", http.MethodGet, d.GetPublishTrace).Permissions(config.DeployDetail),
server.NewRoute("/deploy/getPublishTraceDetail", http.MethodGet, d.GetPublishTraceDetail).Permissions(config.DeployDetail),
server.NewRoute("/deploy/getPreview", http.MethodGet, d.GetPreview).Permissions(config.DeployDetail),
server.NewRoute("/deploy/review", http.MethodPut, d.Review).Permissions(config.DeployReview).LogFunc(middleware.AddOPLog),
server.NewRoute("/deploy/resetState", http.MethodPut, d.ResetState).Permissions(config.DeployResetState).LogFunc(middleware.AddOPLog),
server.NewRoute("/deploy/publish", http.MethodPost, d.Publish).Permissions(config.DeployProject).Middleware(middleware.HasProjectPermission).LogFunc(middleware.AddOPLog),
server.NewRoute("/deploy/rebuild", http.MethodPost, d.Rebuild).Permissions(config.DeployRollback).Middleware(middleware.HasProjectPermission).LogFunc(middleware.AddOPLog),
server.NewRoute("/deploy/greyPublish", http.MethodPost, d.GreyPublish).Permissions(config.GreyDeploy).Middleware(middleware.HasProjectPermission).LogFunc(middleware.AddOPLog),
server.NewWhiteRoute("/deploy/webhook", http.MethodPost, d.Webhook).Middleware(middleware.FilterEvent),
server.NewWhiteRoute("/deploy/callback", http.MethodGet, d.Callback),
server.NewRoute("/deploy/fileCompare", http.MethodPost, d.FileCompare).Permissions(config.FileCompare),
server.NewRoute("/deploy/fileDiff", http.MethodPost, d.FileDiff).Permissions(config.FileCompare),
server.NewRoute("/deploy/manageProcess", http.MethodPost, d.ManageProcess).Permissions(config.ProcessManager).LogFunc(middleware.AddOPLog),
}
}
func (Deploy) GetList(gp *server.Goploy) server.Response {
var projects model.Projects
var err error
if _, ok := gp.Namespace.PermissionIDs[config.GetAllDeployList]; ok {
projects, err = model.Project{NamespaceID: gp.Namespace.ID}.GetDeployList()
} else {
projects, err = model.Project{NamespaceID: gp.Namespace.ID, UserID: gp.UserInfo.ID}.GetDeployList()
}
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Project model.Projects `json:"list"`
}{Project: projects},
}
}
func (Deploy) GetPreview(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `schema:"projectId" validate:"gt=0"`
UserID int64 `schema:"userId"`
State int `schema:"state"`
CommitDate string `schema:"commitDate"`
DeployDate string `schema:"deployDate"`
Branch string `schema:"branch"`
Commit string `schema:"commit"`
Filename string `schema:"filename"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
pagination, err := model.PaginationFrom(gp.URLQuery)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
commitDate := strings.Split(reqData.CommitDate, ",")
for i, date := range commitDate {
tm2, _ := time.Parse("2006-01-02 15:04:05", date)
commitDate[i] = strconv.FormatInt(tm2.Unix(), 10)
}
gitTraceList, pagination, err := model.PublishTrace{
ProjectID: reqData.ProjectID,
PublisherID: reqData.UserID,
State: reqData.State,
}.GetPreview(
reqData.Branch,
reqData.Commit,
reqData.Filename,
commitDate,
strings.Split(reqData.DeployDate, ","),
pagination,
)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
GitTraceList model.PublishTraces `json:"list"`
Pagination model.Pagination `json:"pagination"`
}{GitTraceList: gitTraceList, Pagination: pagination},
}
}
func (Deploy) GetPublishTrace(gp *server.Goploy) server.Response {
lastPublishToken := gp.URLQuery.Get("lastPublishToken")
publishTraceList, err := model.PublishTrace{Token: lastPublishToken}.GetListByToken()
if err == sql.ErrNoRows {
return response.JSON{Code: response.Error, Message: "No deploy record"}
} else if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
PublishTraceList model.PublishTraces `json:"list"`
}{PublishTraceList: publishTraceList},
}
}
func (Deploy) GetPublishTraceDetail(gp *server.Goploy) server.Response {
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
detail, err := model.PublishTrace{ID: id}.GetDetail()
if err == sql.ErrNoRows {
return response.JSON{Code: response.Error, Message: "No deploy record"}
} else if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Detail string `json:"detail"`
}{Detail: detail},
}
}
func (Deploy) ResetState(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.Project{ID: reqData.ProjectID}).ResetState(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Deploy) FileCompare(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"gt=0"`
FilePath string `json:"filePath" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project, err := model.Project{ID: reqData.ProjectID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
srcPath := path.Join(config.GetProjectPath(reqData.ProjectID), reqData.FilePath)
file, err := os.Open(srcPath)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer file.Close()
hash := md5.New()
_, _ = io.Copy(hash, file)
srcMD5 := hex.EncodeToString(hash.Sum(nil))
projectServers, err := model.ProjectServer{ProjectID: reqData.ProjectID}.GetBindServerListByProjectID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if len(projectServers) == 0 {
return response.JSON{Code: response.Error, Message: "project have no server"}
}
type FileCompareData struct {
ServerName string `json:"serverName"`
ServerIP string `json:"serverIP"`
ServerID int64 `json:"serverId"`
Status string `json:"status"`
IsModified bool `json:"isModified"`
}
var fileCompareList []FileCompareData
ch := make(chan FileCompareData, len(projectServers))
distPath := path.Join(project.Path, reqData.FilePath)
for _, server := range projectServers {
go func(server model.ProjectServer) {
fileCompare := FileCompareData{server.ServerName, server.ServerIP, server.ServerID, "no change", false}
client, err := server.ToSSHConfig().Dial()
if err != nil {
fileCompare.Status = "client error"
ch <- fileCompare
return
}
defer client.Close()
//此时获取了sshClient下面使用sshClient构建sftpClient
sftpClient, err := sftp.NewClient(client)
if err != nil {
fileCompare.Status = "sftp error"
ch <- fileCompare
return
}
defer sftpClient.Close()
file, err := sftpClient.Open(distPath)
if err != nil {
fileCompare.Status = "remote file not exists"
ch <- fileCompare
return
}
defer file.Close()
hash := md5.New()
_, _ = io.Copy(hash, file)
distMD5 := hex.EncodeToString(hash.Sum(nil))
if srcMD5 != distMD5 {
fileCompare.Status = "modified"
fileCompare.IsModified = true
ch <- fileCompare
return
}
ch <- fileCompare
}(server)
}
for i := 0; i < len(projectServers); i++ {
fileCompareList = append(fileCompareList, <-ch)
}
close(ch)
return response.JSON{Data: fileCompareList}
}
func (Deploy) FileDiff(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"gt=0"`
ServerID int64 `json:"serverId" validate:"gt=0"`
FilePath string `json:"filePath" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project, err := model.Project{ID: reqData.ProjectID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
srcText, err := ioutil.ReadFile(path.Join(config.GetProjectPath(reqData.ProjectID), reqData.FilePath))
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
server, err := model.Server{ID: reqData.ServerID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
client, err := server.ToSSHConfig().Dial()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer client.Close()
//此时获取了sshClient下面使用sshClient构建sftpClient
sftpClient, err := sftp.NewClient(client)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer sftpClient.Close()
distFile, err := sftpClient.Open(path.Join(project.Path, reqData.FilePath))
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer distFile.Close()
distText, err := ioutil.ReadAll(distFile)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{Data: struct {
SrcText string `json:"srcText"`
DistText string `json:"distText"`
}{SrcText: string(srcText), DistText: string(distText)}}
}
func (Deploy) ManageProcess(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `json:"serverId" validate:"gt=0"`
ProjectProcessID int64 `json:"projectProcessId" validate:"gt=0"`
Command string `json:"command" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectProcess, err := model.ProjectProcess{ID: reqData.ProjectProcessID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project, err := (model.Project{ID: projectProcess.ProjectID}).GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
server, err := (model.Server{ID: reqData.ServerID}).GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
script := ""
switch reqData.Command {
case "status":
script = projectProcess.Status
case "start":
script = projectProcess.Start
case "stop":
script = projectProcess.Stop
case "restart":
script = projectProcess.Restart
default:
return response.JSON{Code: response.Error, Message: "Command error"}
}
if script == "" {
return response.JSON{Code: response.Error, Message: "Command empty"}
}
script = project.ReplaceVars(script)
client, err := server.ToSSHConfig().Dial()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer session.Close()
var sshOutbuf, sshErrbuf bytes.Buffer
session.Stdout = &sshOutbuf
session.Stderr = &sshErrbuf
err = session.Run(script)
pkg.Log(pkg.TRACE, fmt.Sprintf("%s exec cmd %s, result %t, stdout: %s, stderr: %s", gp.UserInfo.Name, script, err == nil, sshOutbuf.String(), sshErrbuf.String()))
return response.JSON{
Data: struct {
ExecRes bool `json:"execRes"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
}{ExecRes: err == nil, Stdout: sshOutbuf.String(), Stderr: sshErrbuf.String()},
}
}
func (Deploy) Publish(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"gt=0"`
Commit string `json:"commit"`
Branch string `json:"branch"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project, err := model.Project{ID: reqData.ProjectID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if project.DeployState == model.ProjectNotDeploy {
err = projectDeploy(gp, project, "", "")
} else if project.Review == model.Enable {
err = projectReview(gp, project, reqData.Commit, reqData.Branch)
} else {
err = projectDeploy(gp, project, reqData.Commit, reqData.Branch)
}
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Deploy) Rebuild(gp *server.Goploy) server.Response {
type ReqData struct {
Token string `json:"token"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
var err error
publishTraceList, err := model.PublishTrace{Token: reqData.Token}.GetListByToken()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectID := publishTraceList[0].ProjectID
project, err := model.Project{ID: projectID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectServers, err := model.ProjectServer{ProjectID: projectID}.GetBindServerListByProjectID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
needToPublish := project.SymlinkPath == ""
var commitInfo repo.CommitInfo
publishTraceServerCount := 0
for _, publishTrace := range publishTraceList {
// publish failed
if publishTrace.State == 0 {
needToPublish = true
break
}
if publishTrace.Type == model.Pull {
err := json.Unmarshal([]byte(publishTrace.Ext), &commitInfo)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
} else if publishTrace.Type == model.Deploy {
for _, projectServer := range projectServers {
if strings.Contains(publishTrace.Ext, projectServer.ServerIP) {
publishTraceServerCount++
break
}
}
}
}
// project server has changed
if publishTraceServerCount != len(projectServers) {
needToPublish = true
}
if needToPublish == false {
ch := make(chan bool, len(projectServers))
for _, projectServer := range projectServers {
go func(projectServer model.ProjectServer) {
destDir := path.Join(project.SymlinkPath, project.LastPublishToken)
cmdEntity := cmd.New(projectServer.ServerOS)
afterDeployCommands := []string{cmdEntity.Symlink(destDir, project.Path), cmdEntity.ChangeDirTime(destDir)}
if len(project.AfterDeployScript) != 0 {
scriptName := fmt.Sprintf("goploy-after-deploy-p%d-s%d.%s", project.ID, projectServer.ServerID, pkg.GetScriptExt(project.AfterDeployScriptMode))
scriptContent := project.ReplaceVars(project.AfterDeployScript)
scriptContent = projectServer.ReplaceVars(scriptContent)
ioutil.WriteFile(path.Join(config.GetProjectPath(project.ID), scriptName), []byte(project.ReplaceVars(project.AfterDeployScript)), 0755)
afterDeployScriptPath := path.Join(project.Path, scriptName)
afterDeployCommands = append(afterDeployCommands, cmdEntity.Script(project.AfterDeployScriptMode, afterDeployScriptPath))
afterDeployCommands = append(afterDeployCommands, cmdEntity.Remove(afterDeployScriptPath))
}
client, err := projectServer.ToSSHConfig().Dial()
if err != nil {
pkg.Log(pkg.ERROR, "projectID:"+strconv.FormatInt(project.ID, 10)+" dial err: "+err.Error())
ch <- false
return
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
pkg.Log(pkg.ERROR, "projectID:"+strconv.FormatInt(project.ID, 10)+" new session err: "+err.Error())
ch <- false
return
}
// check if the path is existed or not
if output, err := session.CombinedOutput("cd " + destDir); err != nil {
pkg.Log(pkg.ERROR, "projectID:"+strconv.FormatInt(project.ID, 10)+" check symlink path err: "+err.Error()+", detail: "+string(output))
ch <- false
return
}
session, err = client.NewSession()
if err != nil {
pkg.Log(pkg.ERROR, "projectID:"+strconv.FormatInt(project.ID, 10)+" new session err: "+err.Error())
ch <- false
return
}
// redirect to project path
if output, err := session.CombinedOutput(strings.Join(afterDeployCommands, "&&")); err != nil {
pkg.Log(pkg.ERROR, "projectID:"+strconv.FormatInt(project.ID, 10)+" symlink err: "+err.Error()+", detail: "+string(output))
ch <- false
return
}
ch <- true
}(projectServer)
}
for i := 0; i < len(projectServers); i++ {
if <-ch == false {
needToPublish = true
break
}
}
close(ch)
if needToPublish == false {
model.PublishTrace{
Token: reqData.Token,
UpdateTime: time.Now().Format("20060102150405"),
}.EditUpdateTimeByToken()
project.PublisherID = gp.UserInfo.ID
project.PublisherName = gp.UserInfo.Name
project.LastPublishToken = reqData.Token
project.Publish()
return response.JSON{Data: "symlink"}
}
}
if needToPublish == true {
repoEntity, _ := repo.GetRepo(project.RepoType)
if !repoEntity.CanRollback() {
return response.JSON{Code: response.Error, Message: fmt.Sprintf("plesae enable symlink to rollback the %s repo", project.RepoType)}
}
project.PublisherID = gp.UserInfo.ID
project.PublisherName = gp.UserInfo.Name
project.DeployState = model.ProjectDeploying
project.LastPublishToken = uuid.New().String()
err = project.Publish()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
task.AddDeployTask(task.Gsync{
UserInfo: gp.UserInfo,
Project: project,
ProjectServers: projectServers,
CommitID: commitInfo.Commit,
Branch: commitInfo.Branch,
})
}
return response.JSON{Data: "publish"}
}
func (Deploy) GreyPublish(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"gt=0"`
Commit string `json:"commit"`
Branch string `json:"branch"`
ServerIDs []int64 `json:"serverIds"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project, err := model.Project{ID: reqData.ProjectID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
bindProjectServers, err := model.ProjectServer{ProjectID: project.ID}.GetBindServerListByProjectID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectServers := model.ProjectServers{}
for _, projectServer := range bindProjectServers {
for _, serverID := range reqData.ServerIDs {
if projectServer.ServerID == serverID {
projectServers = append(projectServers, projectServer)
}
}
}
project.PublisherID = gp.UserInfo.ID
project.PublisherName = gp.UserInfo.Name
project.DeployState = model.ProjectDeploying
project.LastPublishToken = uuid.New().String()
err = project.Publish()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
task.AddDeployTask(task.Gsync{
UserInfo: gp.UserInfo,
Project: project,
ProjectServers: projectServers,
CommitID: reqData.Commit,
Branch: reqData.Branch,
})
return response.JSON{}
}
func (Deploy) Review(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectReviewID int64 `json:"projectReviewId" validate:"gt=0"`
State uint8 `json:"state" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectReviewModel := model.ProjectReview{
ID: reqData.ProjectReviewID,
State: reqData.State,
Editor: gp.UserInfo.Name,
EditorID: gp.UserInfo.ID,
}
projectReview, err := projectReviewModel.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if projectReview.State != model.PENDING {
return response.JSON{Code: response.Error, Message: "Project review state is invalid"}
}
if reqData.State == model.APPROVE {
project, err := model.Project{ID: projectReview.ProjectID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := projectDeploy(gp, project, projectReview.CommitID, projectReview.Branch); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
}
projectReviewModel.EditRow()
return response.JSON{}
}
func (Deploy) Webhook(gp *server.Goploy) server.Response {
projectID, err := strconv.ParseInt(gp.URLQuery.Get("project_id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
type ReqData struct {
Ref string `json:"ref" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project, err := model.Project{ID: projectID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if project.State != model.Disable {
return response.JSON{Code: response.Deny, Message: "Project is disabled"}
}
if project.AutoDeploy != model.ProjectWebhookDeploy {
return response.JSON{Code: response.Deny, Message: "Webhook auto deploy turn off, go to project setting turn on"}
}
branch := ""
if project.RepoType == model.RepoSVN {
branch = reqData.Ref
} else {
branch = strings.Split(reqData.Ref, "/")[2]
}
if project.Branch != branch {
return response.JSON{Code: response.Deny, Message: "Receive branch:" + branch + " push event, not equal to current branch"}
}
gp.UserInfo, err = model.User{ID: 1}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectServers, err := model.ProjectServer{ProjectID: project.ID}.GetBindServerListByProjectID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project.PublisherID = gp.UserInfo.ID
project.PublisherName = "webhook"
project.DeployState = model.ProjectDeploying
project.LastPublishToken = uuid.New().String()
err = project.Publish()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
task.AddDeployTask(task.Gsync{
UserInfo: gp.UserInfo,
Project: project,
ProjectServers: projectServers,
})
return response.JSON{Message: "receive push signal"}
}
func (Deploy) Callback(gp *server.Goploy) server.Response {
projectReviewID, err := strconv.ParseInt(gp.URLQuery.Get("project_review_id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectReviewModel := model.ProjectReview{
ID: projectReviewID,
State: model.APPROVE,
Editor: "admin",
EditorID: 1,
}
projectReview, err := projectReviewModel.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if projectReview.State != model.PENDING {
return response.JSON{Code: response.Error, Message: "Project review state is invalid"}
}
project, err := model.Project{ID: projectReview.ProjectID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := projectDeploy(gp, project, projectReview.CommitID, projectReview.Branch); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectReviewModel.EditRow()
return response.JSON{}
}
func projectDeploy(gp *server.Goploy, project model.Project, commitID string, branch string) error {
projectServers, err := model.ProjectServer{ProjectID: project.ID}.GetBindServerListByProjectID()
if err != nil {
return err
}
project.PublisherID = gp.UserInfo.ID
project.PublisherName = gp.UserInfo.Name
project.DeployState = model.ProjectDeploying
project.LastPublishToken = uuid.New().String()
err = project.Publish()
if err != nil {
return err
}
task.AddDeployTask(task.Gsync{
UserInfo: gp.UserInfo,
Project: project,
ProjectServers: projectServers,
CommitID: commitID,
Branch: branch,
})
return nil
}
func projectReview(gp *server.Goploy, project model.Project, commitID string, branch string) error {
if len(commitID) == 0 {
return errors.New("commit id is required")
}
projectReviewModel := model.ProjectReview{
ProjectID: project.ID,
Branch: branch,
CommitID: commitID,
Creator: gp.UserInfo.Name,
CreatorID: gp.UserInfo.ID,
}
reviewURL := project.ReviewURL
if len(reviewURL) > 0 {
reviewURL = strings.Replace(reviewURL, "__PROJECT_ID__", strconv.FormatInt(project.ID, 10), 1)
reviewURL = strings.Replace(reviewURL, "__PROJECT_NAME__", project.Name, 1)
reviewURL = strings.Replace(reviewURL, "__BRANCH__", branch, 1)
reviewURL = strings.Replace(reviewURL, "__ENVIRONMENT__", strconv.Itoa(int(project.Environment)), 1)
reviewURL = strings.Replace(reviewURL, "__COMMIT_ID__", commitID, 1)
reviewURL = strings.Replace(reviewURL, "__PUBLISH_TIME__", strconv.FormatInt(time.Now().Unix(), 10), 1)
reviewURL = strings.Replace(reviewURL, "__PUBLISHER_ID__", gp.UserInfo.Name, 1)
reviewURL = strings.Replace(reviewURL, "__PUBLISHER_NAME__", strconv.FormatInt(gp.UserInfo.ID, 10), 1)
projectReviewModel.ReviewURL = reviewURL
}
id, err := projectReviewModel.AddRow()
if err != nil {
return err
}
if len(reviewURL) > 0 {
callback := "http://"
if gp.Request.TLS != nil {
callback = "https://"
}
callback += gp.Request.Host + "/deploy/callback?project_review_id=" + strconv.FormatInt(id, 10)
callback = url.QueryEscape(callback)
reviewURL = strings.Replace(reviewURL, "__CALLBACK__", callback, 1)
resp, err := http.Get(reviewURL)
if err != nil {
return err
}
defer resp.Body.Close()
}
return nil
}

317
cmd/server/api/log.go Normal file
View File

@ -0,0 +1,317 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"net/http"
)
// Log struct
type Log API
func (l Log) Handler() []server.Route {
return []server.Route{
server.NewRoute("/log/getLoginLogList", http.MethodGet, l.GetLoginLogList).Permissions(config.ShowLoginLogPage),
server.NewRoute("/log/getLoginLogTotal", http.MethodGet, l.GetLoginLogTotal).Permissions(config.ShowLoginLogPage),
server.NewRoute("/log/getOperationLogList", http.MethodGet, l.GetOperationLogList).Permissions(config.ShowOperationLogPage),
server.NewRoute("/log/getOperationLogTotal", http.MethodGet, l.GetOperationLogTotal).Permissions(config.ShowOperationLogPage),
server.NewRoute("/log/getSftpLogList", http.MethodGet, l.GetSftpLogList).Permissions(config.ShowSFTPLogPage),
server.NewRoute("/log/getSftpLogTotal", http.MethodGet, l.GetSftpLogTotal).Permissions(config.ShowSFTPLogPage),
server.NewRoute("/log/getTerminalLogList", http.MethodGet, l.GetTerminalLogList).Permissions(config.ShowTerminalLogPage),
server.NewRoute("/log/getTerminalLogTotal", http.MethodGet, l.GetTerminalLogTotal).Permissions(config.ShowTerminalLogPage),
server.NewRoute("/log/getTerminalRecord", http.MethodGet, l.GetTerminalRecord).Permissions(config.ShowTerminalRecord),
server.NewRoute("/log/getPublishLogList", http.MethodGet, l.GetPublishLogList).Permissions(config.ShowPublishLogPage),
server.NewRoute("/log/getPublishLogTotal", http.MethodGet, l.GetPublishLogTotal).Permissions(config.ShowPublishLogPage),
}
}
func (Log) GetLoginLogList(gp *server.Goploy) server.Response {
type ReqData struct {
Account string `schema:"account"`
Page uint64 `schema:"page" validate:"gt=0"`
Rows uint64 `schema:"rows" validate:"gt=0"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
list, err := model.LoginLog{Account: reqData.Account}.GetList(reqData.Page, reqData.Rows)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.LoginLogs `json:"list"`
}{List: list},
}
}
func (Log) GetLoginLogTotal(gp *server.Goploy) server.Response {
type ReqData struct {
Account string `schema:"account"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
total, err := model.LoginLog{Account: reqData.Account}.GetTotal()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Total int64 `json:"total"`
}{Total: total},
}
}
func (Log) GetOperationLogList(gp *server.Goploy) server.Response {
type ReqData struct {
Username string `schema:"username"`
Router string `schema:"router"`
API string `schema:"api"`
Page uint64 `schema:"page" validate:"gt=0"`
Rows uint64 `schema:"rows" validate:"gt=0"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
opLog := model.OperationLog{Username: reqData.Username, Router: reqData.Router, API: reqData.API}
if gp.UserInfo.SuperManager != model.SuperManager {
opLog.NamespaceID = gp.Namespace.ID
}
list, err := opLog.GetList(reqData.Page, reqData.Rows)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.OperationLogs `json:"list"`
}{List: list},
}
}
func (Log) GetOperationLogTotal(gp *server.Goploy) server.Response {
type ReqData struct {
Username string `schema:"username"`
Router string `schema:"router"`
API string `schema:"api"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
opLog := model.OperationLog{Username: reqData.Username, Router: reqData.Router, API: reqData.API}
if gp.UserInfo.SuperManager != model.SuperManager {
opLog.NamespaceID = gp.Namespace.ID
}
total, err := opLog.GetTotal()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Total int64 `json:"total"`
}{Total: total},
}
}
func (Log) GetSftpLogList(gp *server.Goploy) server.Response {
type ReqData struct {
Username string `schema:"username"`
ServerName string `schema:"serverName"`
Page uint64 `schema:"page" validate:"gt=0"`
Rows uint64 `schema:"rows" validate:"gt=0"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
sftpLog := model.SftpLog{Username: reqData.Username, ServerName: reqData.ServerName}
if gp.UserInfo.SuperManager != model.SuperManager {
sftpLog.NamespaceID = gp.Namespace.ID
}
list, err := sftpLog.GetList(reqData.Page, reqData.Rows)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.SftpLogs `json:"list"`
}{List: list},
}
}
func (Log) GetSftpLogTotal(gp *server.Goploy) server.Response {
type ReqData struct {
Username string `schema:"username"`
ServerName string `schema:"serverName"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
sftpLog := model.SftpLog{Username: reqData.Username, ServerName: reqData.ServerName}
if gp.UserInfo.SuperManager != model.SuperManager {
sftpLog.NamespaceID = gp.Namespace.ID
}
total, err := sftpLog.GetTotal()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Total int64 `json:"total"`
}{Total: total},
}
}
func (Log) GetTerminalLogList(gp *server.Goploy) server.Response {
type ReqData struct {
Username string `schema:"username"`
ServerName string `schema:"serverName"`
Page uint64 `schema:"page" validate:"gt=0"`
Rows uint64 `schema:"rows" validate:"gt=0"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
terminalLog := model.TerminalLog{Username: reqData.Username, ServerName: reqData.ServerName}
if gp.UserInfo.SuperManager != model.SuperManager {
terminalLog.NamespaceID = gp.Namespace.ID
}
list, err := terminalLog.GetList(reqData.Page, reqData.Rows)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.TerminalLogs `json:"list"`
}{List: list},
}
}
func (Log) GetTerminalLogTotal(gp *server.Goploy) server.Response {
type ReqData struct {
Username string `schema:"username"`
ServerName string `schema:"serverName"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
terminalLog := model.TerminalLog{Username: reqData.Username, ServerName: reqData.ServerName}
if gp.UserInfo.SuperManager != model.SuperManager {
terminalLog.NamespaceID = gp.Namespace.ID
}
total, err := terminalLog.GetTotal()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Total int64 `json:"total"`
}{Total: total},
}
}
func (Log) GetTerminalRecord(gp *server.Goploy) server.Response {
type ReqData struct {
RecordID int64 `schema:"recordId" validate:"gt=0"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
terminalLog, err := model.TerminalLog{ID: reqData.RecordID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if gp.UserInfo.SuperManager != model.SuperManager && terminalLog.NamespaceID != gp.Namespace.ID {
return response.JSON{Code: response.Error, Message: "You have no access to enter this record"}
}
return response.File{Filename: config.GetTerminalLogPath(reqData.RecordID)}
}
func (Log) GetPublishLogList(gp *server.Goploy) server.Response {
type ReqData struct {
Username string `schema:"username"`
ProjectName string `schema:"projectName"`
Page uint64 `schema:"page" validate:"gt=0"`
Rows uint64 `schema:"rows" validate:"gt=0"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
publishLog := model.PublishTrace{PublisherName: reqData.Username, ProjectName: reqData.ProjectName}
if gp.UserInfo.SuperManager != model.SuperManager {
publishLog.NamespaceID = gp.Namespace.ID
}
list, err := publishLog.GetList(reqData.Page, reqData.Rows)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.PublishTraces `json:"list"`
}{List: list},
}
}
func (Log) GetPublishLogTotal(gp *server.Goploy) server.Response {
type ReqData struct {
Username string `schema:"username"`
ProjectName string `schema:"projectName"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
publishLog := model.PublishTrace{PublisherName: reqData.Username, ProjectName: reqData.ProjectName}
if gp.UserInfo.SuperManager != model.SuperManager {
publishLog.NamespaceID = gp.Namespace.ID
}
total, err := publishLog.GetTotal()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Total int64 `json:"total"`
}{Total: total},
}
}

View File

@ -9,12 +9,12 @@ import (
"encoding/base64"
"errors"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/core"
"github.com/zhenorzz/goploy/internal/server"
"strconv"
"time"
)
func CheckSign(gp *core.Goploy) error {
func CheckSign(gp *server.Goploy) error {
sign := gp.URLQuery.Get("sign")
if sign == "" {
return errors.New("sign missing")

View File

@ -7,14 +7,13 @@ package middleware
import (
"encoding/json"
"errors"
"github.com/zhenorzz/goploy/core"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/model"
"github.com/zhenorzz/goploy/permission"
)
// HasProjectPermission check the user has publish auth
func HasProjectPermission(gp *core.Goploy) error {
if _, ok := gp.Namespace.PermissionIDs[permission.GetAllDeployList]; ok {
func HasProjectPermission(gp *server.Goploy) error {
if _, ok := gp.Namespace.PermissionIDs[config.GetAllDeployList]; ok {
return nil
}
type ReqData struct {
@ -32,8 +31,7 @@ func HasProjectPermission(gp *core.Goploy) error {
return nil
}
// FilterEvent check the webhook event has publish auth
func FilterEvent(gp *core.Goploy) error {
func FilterEvent(gp *server.Goploy) error {
if XGitHubEvent := gp.Request.Header.Get("X-GitHub-Event"); len(XGitHubEvent) != 0 && XGitHubEvent == "push" {
return nil
} else if XGitLabEvent := gp.Request.Header.Get("X-Gitlab-Event"); len(XGitLabEvent) != 0 && XGitLabEvent == "Push Hook" {

View File

@ -6,15 +6,15 @@ package middleware
import (
"encoding/json"
"github.com/zhenorzz/goploy/core"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"github.com/zhenorzz/goploy/response"
"strconv"
"time"
)
func AddLoginLog(gp *core.Goploy, resp core.Response) {
func AddLoginLog(gp *server.Goploy, resp server.Response) {
respJson := resp.(response.JSON)
account := ""
if respJson.Code != response.IllegalParam {
@ -39,7 +39,7 @@ func AddLoginLog(gp *core.Goploy, resp core.Response) {
}
}
func AddOPLog(gp *core.Goploy, resp core.Response) {
func AddOPLog(gp *server.Goploy, resp server.Response) {
requestData := ""
if gp.URLQuery != nil && len(gp.URLQuery) > 0 && len(gp.Body) > 2 {
requestData = "{" + string(gp.Body[1:len(gp.Body)-1]) + ", \"_query\":\"" + gp.URLQuery.Encode() + "\"}"
@ -71,7 +71,7 @@ func AddOPLog(gp *core.Goploy, resp core.Response) {
}
}
func AddUploadLog(gp *core.Goploy, resp core.Response) {
func AddUploadLog(gp *server.Goploy, resp server.Response) {
var serverID int64 = 0
var path = ""
respJson := resp.(response.JSON)
@ -97,7 +97,7 @@ func AddUploadLog(gp *core.Goploy, resp core.Response) {
}
}
func AddDeleteLog(gp *core.Goploy, resp core.Response) {
func AddDeleteLog(gp *server.Goploy, resp server.Response) {
var serverID int64 = 0
var path = ""
respJson := resp.(response.JSON)
@ -127,7 +127,7 @@ func AddDeleteLog(gp *core.Goploy, resp core.Response) {
}
}
func AddDownloadLog(gp *core.Goploy, resp core.Response) {
func AddDownloadLog(gp *server.Goploy, resp server.Response) {
msg := ""
path := ""
var serverID int64 = 0
@ -157,7 +157,7 @@ func AddDownloadLog(gp *core.Goploy, resp core.Response) {
}
}
func AddPreviewLog(gp *core.Goploy, resp core.Response) {
func AddPreviewLog(gp *server.Goploy, resp server.Response) {
msg := ""
path := ""
var serverID int64 = 0

172
cmd/server/api/monitor.go Normal file
View File

@ -0,0 +1,172 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"github.com/zhenorzz/goploy/cmd/server/api/middleware"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/monitor"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"net/http"
"time"
)
type Monitor API
func (m Monitor) Handler() []server.Route {
return []server.Route{
server.NewRoute("/monitor/getList", http.MethodGet, m.GetList).Permissions(config.ShowMonitorPage),
server.NewRoute("/monitor/check", http.MethodPost, m.Check).LogFunc(middleware.AddOPLog),
server.NewRoute("/monitor/add", http.MethodPost, m.Add).Permissions(config.AddMonitor).LogFunc(middleware.AddOPLog),
server.NewRoute("/monitor/edit", http.MethodPut, m.Edit).Permissions(config.EditMonitor).LogFunc(middleware.AddOPLog),
server.NewRoute("/monitor/toggle", http.MethodPut, m.Toggle).Permissions(config.EditMonitor).LogFunc(middleware.AddOPLog),
server.NewRoute("/monitor/remove", http.MethodDelete, m.Remove).Permissions(config.DeleteMonitor).LogFunc(middleware.AddOPLog),
}
}
func (Monitor) GetList(gp *server.Goploy) server.Response {
monitorList, err := model.Monitor{NamespaceID: gp.Namespace.ID}.GetList()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Monitors model.Monitors `json:"list"`
}{Monitors: monitorList},
}
}
func (Monitor) Check(gp *server.Goploy) server.Response {
type ReqData struct {
Type int `json:"type" validate:"oneof=1 2 3 4 5"`
Items []string `json:"items" validate:"required"`
Timeout time.Duration `json:"timeout"`
Process string `json:"process"`
Script string `json:"script"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (monitor.Monitor{
Type: reqData.Type,
Items: reqData.Items,
Timeout: reqData.Timeout,
Process: reqData.Process,
Script: reqData.Script,
}.Check()); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{Message: "Connected"}
}
func (Monitor) Add(gp *server.Goploy) server.Response {
type ReqData struct {
Name string `json:"name" validate:"required"`
Type int `json:"type" validate:"oneof=1 2 3 4 5"`
Target string `json:"target" validate:"required"`
Second int `json:"second" validate:"gt=0"`
Times uint16 `json:"times" validate:"gt=0"`
SilentCycle int `json:"silentCycle" validate:"required"`
NotifyType uint8 `json:"notifyType" validate:"gt=0"`
NotifyTarget string `json:"notifyTarget" validate:"required"`
Description string `json:"description" validate:"max=255"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
id, err := model.Monitor{
NamespaceID: gp.Namespace.ID,
Name: reqData.Name,
Type: reqData.Type,
Target: reqData.Target,
Second: reqData.Second,
Times: reqData.Times,
SilentCycle: reqData.SilentCycle,
NotifyType: reqData.NotifyType,
NotifyTarget: reqData.NotifyTarget,
Description: reqData.Description,
}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: id},
}
}
func (Monitor) Edit(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
Name string `json:"name" validate:"required"`
Type int `json:"type" validate:"oneof=1 2 3 4 5"`
Target string `json:"target" validate:"required"`
Second int `json:"second" validate:"gt=0"`
Times uint16 `json:"times" validate:"gt=0"`
SilentCycle int `json:"silentCycle" validate:"required"`
NotifyType uint8 `json:"notifyType" validate:"gt=0"`
NotifyTarget string `json:"notifyTarget" validate:"required"`
Description string `json:"description" validate:"max=255"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err := model.Monitor{
ID: reqData.ID,
Name: reqData.Name,
Type: reqData.Type,
Target: reqData.Target,
Second: reqData.Second,
Times: reqData.Times,
SilentCycle: reqData.SilentCycle,
NotifyType: reqData.NotifyType,
NotifyTarget: reqData.NotifyTarget,
Description: reqData.Description,
}.EditRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Monitor) Toggle(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
State uint8 `json:"state" validate:"oneof=0 1"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.Monitor{ID: reqData.ID, State: reqData.State}).ToggleState(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Monitor) Remove(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.Monitor{ID: reqData.ID}).DeleteRow(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}

171
cmd/server/api/namespace.go Normal file
View File

@ -0,0 +1,171 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"github.com/zhenorzz/goploy/cmd/server/api/middleware"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"net/http"
"strconv"
)
type Namespace API
func (n Namespace) Handler() []server.Route {
return []server.Route{
server.NewRoute("/namespace/getList", http.MethodGet, n.GetList).Permissions(config.ShowNamespacePage),
server.NewRoute("/namespace/getOption", http.MethodGet, n.GetOption),
server.NewRoute("/namespace/getBindUserList", http.MethodGet, n.GetBindUserList).Permissions(config.ShowNamespacePage),
server.NewRoute("/namespace/getUserOption", http.MethodGet, n.GetUserOption),
server.NewRoute("/namespace/add", http.MethodPost, n.Add).Permissions(config.AddNamespace).LogFunc(middleware.AddOPLog),
server.NewRoute("/namespace/edit", http.MethodPut, n.Edit).Permissions(config.EditNamespace).LogFunc(middleware.AddOPLog),
server.NewRoute("/namespace/addUser", http.MethodPost, n.AddUser).Permissions(config.AddNamespaceUser).LogFunc(middleware.AddOPLog),
server.NewRoute("/namespace/removeUser", http.MethodDelete, n.RemoveUser).Permissions(config.DeleteNamespaceUser).LogFunc(middleware.AddOPLog),
}
}
func (Namespace) GetList(gp *server.Goploy) server.Response {
namespaceList, err := model.Namespace{UserID: gp.UserInfo.ID}.GetList()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Namespaces model.Namespaces `json:"list"`
}{Namespaces: namespaceList},
}
}
func (Namespace) GetOption(gp *server.Goploy) server.Response {
namespaceUsers, err := model.NamespaceUser{UserID: gp.UserInfo.ID}.GetUserNamespaceList()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
NamespaceUsers model.NamespaceUsers `json:"list"`
}{NamespaceUsers: namespaceUsers},
}
}
func (Namespace) GetUserOption(gp *server.Goploy) server.Response {
namespaceUsers, err := model.NamespaceUser{NamespaceID: gp.Namespace.ID}.GetAllUserByNamespaceID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
NamespaceUsers model.NamespaceUsers `json:"list"`
}{NamespaceUsers: namespaceUsers},
}
}
func (Namespace) GetBindUserList(gp *server.Goploy) server.Response {
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
namespaceUsers, err := model.NamespaceUser{NamespaceID: id}.GetBindUserListByNamespaceID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
NamespaceUsers model.NamespaceUsers `json:"list"`
}{NamespaceUsers: namespaceUsers},
}
}
func (Namespace) Add(gp *server.Goploy) server.Response {
type ReqData struct {
Name string `json:"name" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
id, err := model.Namespace{Name: reqData.Name}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.NamespaceUser{NamespaceID: id}).AddAdminByNamespaceID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: id},
}
}
func (Namespace) Edit(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
Name string `json:"name" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err := model.Namespace{
ID: reqData.ID,
Name: reqData.Name,
}.EditRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Namespace) AddUser(gp *server.Goploy) server.Response {
type ReqData struct {
NamespaceID int64 `json:"namespaceId" validate:"gt=0"`
UserIDs []int64 `json:"userIds" validate:"required"`
RoleID int64 `json:"roleId" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
namespaceUsersModel := model.NamespaceUsers{}
for _, userID := range reqData.UserIDs {
namespaceUserModel := model.NamespaceUser{
NamespaceID: reqData.NamespaceID,
UserID: userID,
RoleID: reqData.RoleID,
}
namespaceUsersModel = append(namespaceUsersModel, namespaceUserModel)
}
if err := namespaceUsersModel.AddMany(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Namespace) RemoveUser(gp *server.Goploy) server.Response {
type ReqData struct {
NamespaceUserID int64 `json:"namespaceUserId" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.NamespaceUser{ID: reqData.NamespaceUserID}).DeleteRow(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}

894
cmd/server/api/project.go Normal file
View File

@ -0,0 +1,894 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"bytes"
"fmt"
"github.com/zhenorzz/goploy/cmd/server/api/middleware"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/internal/repo"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"strconv"
"strings"
)
type Project API
func (p Project) Handler() []server.Route {
return []server.Route{
server.NewRoute("/project/getList", http.MethodGet, p.GetList).Permissions(config.ShowProjectPage),
server.NewRoute("/project/pingRepos", http.MethodGet, p.PingRepos),
server.NewRoute("/project/getRemoteBranchList", http.MethodGet, p.GetRemoteBranchList),
server.NewRoute("/project/getBindServerList", http.MethodGet, p.GetBindServerList),
server.NewRoute("/project/getBindUserList", http.MethodGet, p.GetBindUserList),
server.NewRoute("/project/getProjectFileList", http.MethodGet, p.GetProjectFileList).Permissions(config.FileSync),
server.NewRoute("/project/getProjectFileContent", http.MethodGet, p.GetProjectFileContent).Permissions(config.FileSync),
server.NewRoute("/project/getReposFileList", http.MethodGet, p.GetReposFileList).Permissions(config.FileCompare),
server.NewRoute("/project/add", http.MethodPost, p.Add).Permissions(config.AddProject).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/edit", http.MethodPut, p.Edit).Permissions(config.EditProject).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/setAutoDeploy", http.MethodPut, p.SetAutoDeploy).Permissions(config.SwitchProjectWebhook).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/remove", http.MethodDelete, p.Remove).Permissions(config.DeleteProject).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/uploadFile", http.MethodPost, p.UploadFile).Permissions(config.FileSync).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/removeFile", http.MethodDelete, p.RemoveFile).Permissions(config.FileSync).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/addFile", http.MethodPost, p.AddFile).Permissions(config.FileSync).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/editFile", http.MethodPut, p.EditFile).Permissions(config.FileSync).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/addTask", http.MethodPost, p.AddTask).Permissions(config.DeployTask).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/removeTask", http.MethodDelete, p.RemoveTask).Permissions(config.DeployTask).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/getTaskList", http.MethodGet, p.GetTaskList).Permissions(config.DeployTask),
server.NewRoute("/project/getReviewList", http.MethodGet, p.GetReviewList).Permissions(config.DeployReview),
server.NewRoute("/project/getProcessList", http.MethodGet, p.GetProcessList).Permissions(config.ProcessManager),
server.NewRoute("/project/addProcess", http.MethodPost, p.AddProcess).Permissions(config.ProcessManager).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/editProcess", http.MethodPut, p.EditProcess).Permissions(config.ProcessManager).LogFunc(middleware.AddOPLog),
server.NewRoute("/project/deleteProcess", http.MethodDelete, p.DeleteProcess).Permissions(config.ProcessManager).LogFunc(middleware.AddOPLog),
}
}
func (Project) GetList(gp *server.Goploy) server.Response {
var projectList model.Projects
var err error
if _, ok := gp.Namespace.PermissionIDs[config.GetAllProjectList]; ok {
projectList, err = model.Project{NamespaceID: gp.Namespace.ID}.GetList()
} else {
projectList, err = model.Project{NamespaceID: gp.Namespace.ID, UserID: gp.UserInfo.ID}.GetList()
}
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Projects model.Projects `json:"list"`
}{Projects: projectList},
}
}
func (Project) PingRepos(gp *server.Goploy) server.Response {
type ReqData struct {
URL string `schema:"url" validate:"required"`
RepoType string `schema:"repoType" validate:"required"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if strings.Contains(reqData.URL, "git@") {
host := strings.Split(reqData.URL, "git@")[1]
host = strings.Split(host, ":")[0]
homeDir, err := os.UserHomeDir()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
knownHostsPath := homeDir + "/.ssh/known_hosts"
var cmdOutbuf, cmdErrbuf bytes.Buffer
cmd := exec.Command("ssh-keygen", "-F", host, "-f", knownHostsPath)
cmd.Stdout = &cmdOutbuf
cmd.Stderr = &cmdErrbuf
if err := cmd.Run(); err != nil {
cmdOutbuf.Reset()
cmdErrbuf.Reset()
cmd := exec.Command("ssh-keyscan", host)
cmd.Stdout = &cmdOutbuf
cmd.Stderr = &cmdErrbuf
if err := cmd.Run(); err != nil {
return response.JSON{Code: response.Error, Message: cmdErrbuf.String()}
}
f, err := os.OpenFile(knownHostsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer f.Close()
if _, err := f.Write(cmdOutbuf.Bytes()); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
}
}
r, err := repo.GetRepo(reqData.RepoType)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := r.Ping(reqData.URL); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Project) GetRemoteBranchList(gp *server.Goploy) server.Response {
type ReqData struct {
URL string `schema:"url" validate:"required"`
RepoType string `schema:"repoType" validate:"required"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if strings.Contains(reqData.URL, "git@") {
host := strings.Split(reqData.URL, "git@")[1]
host = strings.Split(host, ":")[0]
homeDir, err := os.UserHomeDir()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
knownHostsPath := homeDir + "/.ssh/known_hosts"
var cmdOutbuf, cmdErrbuf bytes.Buffer
cmd := exec.Command("ssh-keygen", "-F", host, "-f", knownHostsPath)
cmd.Stdout = &cmdOutbuf
cmd.Stderr = &cmdErrbuf
if err := cmd.Run(); err != nil {
cmdOutbuf.Reset()
cmdErrbuf.Reset()
cmd := exec.Command("ssh-keyscan", host)
cmd.Stdout = &cmdOutbuf
cmd.Stderr = &cmdErrbuf
if err := cmd.Run(); err != nil {
return response.JSON{Code: response.Error, Message: cmdErrbuf.String()}
}
f, err := os.OpenFile(knownHostsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer f.Close()
if _, err := f.Write(cmdOutbuf.Bytes()); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
}
}
r, err := repo.GetRepo(reqData.RepoType)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
list, err := r.RemoteBranchList(reqData.URL)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{Data: struct {
Branch []string `json:"branch"`
}{Branch: list}}
}
func (Project) GetBindServerList(gp *server.Goploy) server.Response {
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectServers, err := model.ProjectServer{ProjectID: id}.GetBindServerListByProjectID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ProjectServers model.ProjectServers `json:"list"`
}{ProjectServers: projectServers},
}
}
func (Project) GetBindUserList(gp *server.Goploy) server.Response {
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectUsers, err := model.ProjectUser{ProjectID: id, NamespaceID: gp.Namespace.ID}.GetBindUserListByProjectID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ProjectUsers model.ProjectUsers `json:"list"`
}{ProjectUsers: projectUsers},
}
}
func (Project) GetProjectFileList(gp *server.Goploy) server.Response {
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectFiles, err := model.ProjectFile{ProjectID: id}.GetListByProjectID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ProjectFiles model.ProjectFiles `json:"list"`
}{ProjectFiles: projectFiles},
}
}
func (Project) GetProjectFileContent(gp *server.Goploy) server.Response {
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectFileData, err := model.ProjectFile{ID: id}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
fileBytes, err := ioutil.ReadFile(path.Join(config.GetProjectFilePath(projectFileData.ProjectID), projectFileData.Filename))
if err != nil {
fmt.Println("read fail", err)
}
return response.JSON{
Data: struct {
Content string `json:"content"`
}{Content: string(fileBytes)},
}
}
func (Project) GetReposFileList(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `schema:"id" validate:"gt=0"`
Path string `schema:"path" validate:"required"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
files, err := ioutil.ReadDir(path.Join(config.GetProjectPath(reqData.ID), reqData.Path))
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
type fileInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
Mode string `json:"mode"`
ModTime string `json:"modTime"`
IsDir bool `json:"isDir"`
}
var fileList []fileInfo
for _, f := range files {
fileList = append(fileList, fileInfo{
Name: f.Name(),
Size: f.Size(),
Mode: f.Mode().String(),
ModTime: f.ModTime().Format("2006-01-02 15:04:05"),
IsDir: f.IsDir(),
})
}
return response.JSON{Data: fileList}
}
func (Project) Add(gp *server.Goploy) server.Response {
type ReqData struct {
Name string `json:"name" validate:"required"`
RepoType string `json:"repoType" validate:"required"`
URL string `json:"url" validate:"required"`
Path string `json:"path" validate:"required"`
Environment uint8 `json:"environment" validate:"required"`
Branch string `json:"branch" validate:"required"`
SymlinkPath string `json:"symlinkPath"`
SymlinkBackupNumber uint8 `json:"symlinkBackupNumber"`
Review uint8 `json:"review"`
ReviewURL string `json:"reviewURL"`
AfterPullScriptMode string `json:"afterPullScriptMode"`
AfterPullScript string `json:"afterPullScript"`
AfterDeployScriptMode string `json:"afterDeployScriptMode"`
AfterDeployScript string `json:"afterDeployScript"`
TransferType string `json:"transferType"`
TransferOption string `json:"transferOption"`
DeployServerMode string `json:"deployServerMode"`
ServerIDs []int64 `json:"serverIds"`
UserIDs []int64 `json:"userIds"`
NotifyType uint8 `json:"notifyType"`
NotifyTarget string `json:"notifyTarget"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if _, err := pkg.ParseCommandLine(reqData.TransferOption); err != nil {
return response.JSON{Code: response.Error, Message: "Invalid transfer option format"}
}
projectID, err := model.Project{
NamespaceID: gp.Namespace.ID,
Name: reqData.Name,
RepoType: reqData.RepoType,
URL: reqData.URL,
Path: reqData.Path,
Environment: reqData.Environment,
Branch: reqData.Branch,
SymlinkPath: reqData.SymlinkPath,
SymlinkBackupNumber: reqData.SymlinkBackupNumber,
Review: reqData.Review,
ReviewURL: reqData.ReviewURL,
AfterPullScriptMode: reqData.AfterPullScriptMode,
AfterPullScript: reqData.AfterPullScript,
AfterDeployScriptMode: reqData.AfterDeployScriptMode,
AfterDeployScript: reqData.AfterDeployScript,
TransferType: reqData.TransferType,
TransferOption: reqData.TransferOption,
DeployServerMode: reqData.DeployServerMode,
NotifyType: reqData.NotifyType,
NotifyTarget: reqData.NotifyTarget,
}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectServersModel := model.ProjectServers{}
for _, serverID := range reqData.ServerIDs {
projectServerModel := model.ProjectServer{
ProjectID: projectID,
ServerID: serverID,
}
projectServersModel = append(projectServersModel, projectServerModel)
}
if err := projectServersModel.AddMany(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectUsersModel := model.ProjectUsers{}
for _, userID := range reqData.UserIDs {
projectUserModel := model.ProjectUser{
ProjectID: projectID,
UserID: userID,
}
projectUsersModel = append(projectUsersModel, projectUserModel)
}
if err := projectUsersModel.AddMany(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Project) Edit(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
Name string `json:"name"`
RepoType string `json:"repoType"`
URL string `json:"url"`
Path string `json:"path"`
SymlinkPath string `json:"symlinkPath"`
SymlinkBackupNumber uint8 `json:"symlinkBackupNumber"`
Review uint8 `json:"review"`
ReviewURL string `json:"reviewURL"`
Environment uint8 `json:"environment"`
Branch string `json:"branch"`
ServerIDs []int64 `json:"serverIds"`
UserIDs []int64 `json:"userIds"`
AfterPullScriptMode string `json:"afterPullScriptMode"`
AfterPullScript string `json:"afterPullScript"`
AfterDeployScriptMode string `json:"afterDeployScriptMode"`
AfterDeployScript string `json:"afterDeployScript"`
TransferType string `json:"transferType"`
TransferOption string `json:"transferOption"`
DeployServerMode string `json:"deployServerMode"`
NotifyType uint8 `json:"notifyType"`
NotifyTarget string `json:"notifyTarget"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if _, err := pkg.ParseCommandLine(reqData.TransferOption); err != nil {
return response.JSON{Code: response.Error, Message: "Invalid option format"}
}
projectData, err := model.Project{ID: reqData.ID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err = model.Project{
ID: reqData.ID,
Name: reqData.Name,
RepoType: reqData.RepoType,
URL: reqData.URL,
Path: reqData.Path,
Environment: reqData.Environment,
Branch: reqData.Branch,
SymlinkPath: reqData.SymlinkPath,
SymlinkBackupNumber: reqData.SymlinkBackupNumber,
Review: reqData.Review,
ReviewURL: reqData.ReviewURL,
AfterPullScriptMode: reqData.AfterPullScriptMode,
AfterPullScript: reqData.AfterPullScript,
AfterDeployScriptMode: reqData.AfterDeployScriptMode,
AfterDeployScript: reqData.AfterDeployScript,
TransferType: reqData.TransferType,
TransferOption: reqData.TransferOption,
DeployServerMode: reqData.DeployServerMode,
NotifyType: reqData.NotifyType,
NotifyTarget: reqData.NotifyTarget,
}.EditRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if reqData.URL != projectData.URL {
srcPath := config.GetProjectPath(projectData.ID)
_, err := os.Stat(srcPath)
if err == nil || os.IsNotExist(err) == false {
repo := reqData.URL
cmd := exec.Command("git", "remote", "set-url", "origin", repo)
cmd.Dir = srcPath
if err := cmd.Run(); err != nil {
return response.JSON{Code: response.Error, Message: "Project change url fail, you can do it manually, reason: " + err.Error()}
}
}
}
if err := (model.ProjectServer{ProjectID: projectData.ID}).DeleteByProjectID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectServersModel := model.ProjectServers{}
for _, serverID := range reqData.ServerIDs {
projectServerModel := model.ProjectServer{
ProjectID: projectData.ID,
ServerID: serverID,
}
projectServersModel = append(projectServersModel, projectServerModel)
}
if err := projectServersModel.AddMany(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.ProjectUser{ProjectID: projectData.ID}).DeleteByProjectID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectUsersModel := model.ProjectUsers{}
for _, userID := range reqData.UserIDs {
projectUserModel := model.ProjectUser{
ProjectID: projectData.ID,
UserID: userID,
}
projectUsersModel = append(projectUsersModel, projectUserModel)
}
if err := projectUsersModel.AddMany(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Project) SetAutoDeploy(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
AutoDeploy uint8 `json:"autoDeploy" validate:"gte=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err := model.Project{
ID: reqData.ID,
AutoDeploy: reqData.AutoDeploy,
}.SetAutoDeploy()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Project) Remove(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectData, err := model.Project{ID: reqData.ID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
srcPath := config.GetProjectPath(projectData.ID)
if err := os.RemoveAll(srcPath); err != nil {
return response.JSON{Code: response.Error, Message: "Delete folder fail, Detail: " + err.Error()}
}
if err := (model.Project{ID: reqData.ID}).RemoveRow(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Project) UploadFile(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectFileID int64 `schema:"projectFileId" validate:"gte=0"`
ProjectID int64 `schema:"projectId" validate:"gt=0"`
Filename string `schema:"filename" validate:"required"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
file, _, err := gp.Request.FormFile("file")
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer file.Close()
filePath := path.Join(config.GetProjectFilePath(reqData.ProjectID), reqData.Filename)
fileDir := path.Dir(filePath)
if _, err := os.Stat(fileDir); err != nil {
if os.IsNotExist(err) {
err := os.MkdirAll(fileDir, 0755)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
} else {
return response.JSON{Code: response.Error, Message: err.Error()}
}
}
// read all of the contents of our uploaded file into a
// byte array
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := ioutil.WriteFile(filePath, fileBytes, 0755); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if reqData.ProjectFileID == 0 {
reqData.ProjectFileID, err = model.ProjectFile{
Filename: reqData.Filename,
ProjectID: reqData.ProjectID,
}.AddRow()
} else {
err = model.ProjectFile{
ID: reqData.ProjectFileID,
Filename: reqData.Filename,
ProjectID: reqData.ProjectID,
}.EditRow()
}
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: reqData.ProjectFileID},
}
}
func (Project) AddFile(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"gt=0"`
Content string `json:"content" validate:"required"`
Filename string `json:"filename" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
filePath := path.Join(config.GetProjectFilePath(reqData.ProjectID), reqData.Filename)
fileDir := path.Dir(filePath)
if _, err := os.Stat(fileDir); err != nil {
if os.IsNotExist(err) {
err := os.MkdirAll(fileDir, 0755)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
} else {
return response.JSON{Code: response.Error, Message: err.Error()}
}
}
file, err := os.Create(filePath)
if err != nil {
panic(err)
}
defer file.Close()
file.WriteString(reqData.Content)
id, err := model.ProjectFile{
ProjectID: reqData.ProjectID,
Filename: reqData.Filename,
}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: id},
}
}
func (Project) EditFile(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
Content string `json:"content" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectFileData, err := model.ProjectFile{ID: reqData.ID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
_, err = os.Stat(config.GetProjectFilePath(projectFileData.ProjectID))
if err != nil {
err := os.MkdirAll(config.GetProjectFilePath(projectFileData.ProjectID), os.ModePerm)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
}
file, err := os.Create(path.Join(config.GetProjectFilePath(projectFileData.ProjectID), projectFileData.Filename))
if err != nil {
panic(err)
}
defer file.Close()
file.WriteString(reqData.Content)
return response.JSON{}
}
func (Project) RemoveFile(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectFileID int64 `json:"projectFileId" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectFileData, err := model.ProjectFile{ID: reqData.ProjectFileID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := os.Remove(path.Join(config.GetProjectFilePath(projectFileData.ProjectID), projectFileData.Filename)); err != nil {
if !os.IsNotExist(err) {
return response.JSON{Code: response.Error, Message: "Delete file fail, Detail: " + err.Error()}
}
}
if err := (model.ProjectFile{ID: reqData.ProjectFileID}).DeleteRow(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Project) GetReviewList(gp *server.Goploy) server.Response {
pagination, err := model.PaginationFrom(gp.URLQuery)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
ProjectReviews, pagination, err := model.ProjectReview{ProjectID: id}.GetListByProjectID(pagination)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ProjectReviews model.ProjectReviews `json:"list"`
Pagination model.Pagination `json:"pagination"`
}{ProjectReviews: ProjectReviews, Pagination: pagination},
}
}
func (Project) GetTaskList(gp *server.Goploy) server.Response {
pagination, err := model.PaginationFrom(gp.URLQuery)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
projectTaskList, pagination, err := model.ProjectTask{ProjectID: id}.GetListByProjectID(pagination)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ProjectTasks model.ProjectTasks `json:"list"`
Pagination model.Pagination `json:"pagination"`
}{ProjectTasks: projectTaskList, Pagination: pagination},
}
}
func (Project) AddTask(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"gt=0"`
Branch string `json:"branch" validate:"required"`
Commit string `json:"commit" validate:"required"`
Date string `json:"date" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
id, err := model.ProjectTask{
ProjectID: reqData.ProjectID,
CommitID: reqData.Commit,
Branch: reqData.Branch,
Date: reqData.Date,
Creator: gp.UserInfo.Name,
CreatorID: gp.UserInfo.ID,
}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: id},
}
}
func (Project) RemoveTask(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.ProjectTask{ID: reqData.ID}).RemoveRow(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Project) GetProcessList(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `schema:"projectId" validate:"gt=0"`
Page uint64 `schema:"page" validate:"gt=0"`
Rows uint64 `schema:"rows" validate:"gt=0"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
list, err := model.ProjectProcess{ProjectID: reqData.ProjectID}.GetListByProjectID(reqData.Page, reqData.Rows)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.ProjectProcesses `json:"list"`
}{List: list},
}
}
func (Project) AddProcess(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"gt=0"`
Name string `json:"name" validate:"required"`
Status string `json:"status"`
Start string `json:"start"`
Stop string `json:"stop"`
Restart string `json:"restart"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
id, err := model.ProjectProcess{
ProjectID: reqData.ProjectID,
Name: reqData.Name,
Status: reqData.Status,
Start: reqData.Start,
Stop: reqData.Stop,
Restart: reqData.Restart,
}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: id},
}
}
func (Project) EditProcess(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
Name string `json:"name" validate:"required"`
Status string `json:"status"`
Start string `json:"start"`
Stop string `json:"stop"`
Restart string `json:"restart"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err := model.ProjectProcess{
ID: reqData.ID,
Name: reqData.Name,
Status: reqData.Status,
Start: reqData.Start,
Stop: reqData.Stop,
Restart: reqData.Restart,
}.EditRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Project) DeleteProcess(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.ProjectProcess{ID: reqData.ID}).DeleteRow(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}

View File

@ -0,0 +1,114 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"github.com/zhenorzz/goploy/internal/repo"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"net/http"
"strconv"
)
// Repository struct
type Repository API
func (r Repository) Handler() []server.Route {
return []server.Route{
server.NewRoute("/repository/getCommitList", http.MethodGet, r.GetCommitList),
server.NewRoute("/repository/getBranchList", http.MethodGet, r.GetBranchList),
server.NewRoute("/repository/getTagList", http.MethodGet, r.GetTagList),
}
}
// GetCommitList get latest 10 commit list
func (Repository) GetCommitList(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `schema:"id" validate:"gt=0"`
Branch string `schema:"branch" validate:"required"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project, err := model.Project{ID: reqData.ID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
r, err := repo.GetRepo(project.RepoType)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
list, err := r.BranchLog(project.ID, reqData.Branch, 10)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
CommitList []repo.CommitInfo `json:"list"`
}{CommitList: list},
}
}
func (Repository) GetBranchList(gp *server.Goploy) server.Response {
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project, err := model.Project{ID: id}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
r, err := repo.GetRepo(project.RepoType)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
branchList, err := r.BranchList(project.ID)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
BranchList []string `json:"list"`
}{BranchList: branchList},
}
}
func (Repository) GetTagList(gp *server.Goploy) server.Response {
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
project, err := model.Project{ID: id}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
r, err := repo.GetRepo(project.RepoType)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
list, err := r.TagLog(project.ID, 10)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
TagList []repo.CommitInfo `json:"list"`
}{TagList: list},
}
}

181
cmd/server/api/role.go Normal file
View File

@ -0,0 +1,181 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"github.com/zhenorzz/goploy/cmd/server/api/middleware"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"net/http"
)
type Role API
func (r Role) Handler() []server.Route {
return []server.Route{
server.NewRoute("/role/getList", http.MethodGet, r.GetList).Permissions(config.ShowRolePage),
server.NewRoute("/role/getOption", http.MethodGet, r.GetOption),
server.NewRoute("/role/getPermissionList", http.MethodGet, r.GetPermissionList).Permissions(config.ShowRolePage),
server.NewRoute("/role/getPermissionBindings", http.MethodGet, r.GetPermissionBindings).Permissions(config.ShowRolePage),
server.NewRoute("/role/add", http.MethodPost, r.Add).Permissions(config.AddRole).LogFunc(middleware.AddOPLog),
server.NewRoute("/role/edit", http.MethodPut, r.Edit).Permissions(config.EditRole).LogFunc(middleware.AddOPLog),
server.NewRoute("/role/remove", http.MethodDelete, r.Remove).Permissions(config.DeleteRole).LogFunc(middleware.AddOPLog),
server.NewRoute("/role/changePermission", http.MethodPut, r.ChangePermission).Permissions(config.EditPermission).LogFunc(middleware.AddOPLog),
}
}
func (Role) GetList(*server.Goploy) server.Response {
roleList, err := model.Role{}.GetList()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.Roles `json:"list"`
}{List: roleList},
}
}
func (Role) GetOption(*server.Goploy) server.Response {
list, err := model.Role{}.GetAll()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{Data: struct {
List model.Roles `json:"list"`
}{list}}
}
func (Role) GetPermissionList(*server.Goploy) server.Response {
list, err := model.Permission{}.GetList()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.Permissions `json:"list"`
}{List: list},
}
}
func (Role) GetPermissionBindings(gp *server.Goploy) server.Response {
type ReqData struct {
RoleID int64 `json:"roleId" validate:"required"`
}
var reqData ReqData
if err := decodeQuery(gp.URLQuery, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
list, err := model.RolePermission{RoleID: reqData.RoleID}.GetList()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
List model.RolePermissions `json:"list"`
}{List: list},
}
}
func (Role) Add(gp *server.Goploy) server.Response {
type ReqData struct {
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"max=255"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
id, err := model.Role{Name: reqData.Name, Description: reqData.Description}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: id},
}
}
func (Role) Edit(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"max=255"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err := model.Role{ID: reqData.ID, Name: reqData.Name, Description: reqData.Description}.EditRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Role) Remove(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
namespaceUser, err := (model.NamespaceUser{RoleID: reqData.ID}).GetDataByRoleID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if namespaceUser.ID > 0 {
return response.JSON{Code: response.Error, Message: "The role has binding user"}
}
if err := (model.Role{ID: reqData.ID}).DeleteRow(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Role) ChangePermission(gp *server.Goploy) server.Response {
type ReqData struct {
PermissionIDs []int64 `json:"permissionIds" validate:"required"`
RoleID int64 `json:"roleId" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.RolePermission{RoleID: reqData.RoleID}).DeleteByRoleID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
rolePermissionsModel := model.RolePermissions{}
for _, PermissionID := range reqData.PermissionIDs {
rolePermissionModel := model.RolePermission{
PermissionID: PermissionID,
RoleID: reqData.RoleID,
}
rolePermissionsModel = append(rolePermissionsModel, rolePermissionModel)
}
if err := rolePermissionsModel.AddMany(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}

1053
cmd/server/api/server.go Normal file

File diff suppressed because it is too large Load Diff

383
cmd/server/api/user.go Normal file
View File

@ -0,0 +1,383 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package api
import (
"crypto/md5"
"database/sql"
"encoding/hex"
"fmt"
"github.com/go-ldap/ldap/v3"
"github.com/zhenorzz/goploy/cmd/server/api/middleware"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"net/http"
"strconv"
"time"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/model"
)
type User API
func (u User) Handler() []server.Route {
return []server.Route{
server.NewWhiteRoute("/user/login", http.MethodPost, u.Login).LogFunc(middleware.AddLoginLog),
server.NewWhiteRoute("/user/extLogin", http.MethodPost, u.ExtLogin).LogFunc(middleware.AddLoginLog),
server.NewRoute("/user/info", http.MethodGet, u.Info),
server.NewRoute("/user/changePassword", http.MethodPut, u.ChangePassword),
server.NewRoute("/user/getList", http.MethodGet, u.GetList).Permissions(config.ShowMemberPage),
server.NewRoute("/user/getOption", http.MethodGet, u.GetOption),
server.NewRoute("/user/add", http.MethodPost, u.Add).Permissions(config.AddMember).LogFunc(middleware.AddOPLog),
server.NewRoute("/user/edit", http.MethodPut, u.Edit).Permissions(config.EditMember).LogFunc(middleware.AddOPLog),
server.NewRoute("/user/remove", http.MethodDelete, u.Remove).Permissions(config.DeleteMember).LogFunc(middleware.AddOPLog),
}
}
func (User) Login(gp *server.Goploy) server.Response {
type ReqData struct {
Account string `json:"account" validate:"min=1,max=25"`
Password string `json:"password" validate:"password"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
userData, err := model.User{Account: reqData.Account}.GetDataByAccount()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if config.Toml.LDAP.Enabled && userData.ID != 1 {
conn, err := ldap.DialURL(config.Toml.LDAP.URL)
if err != nil {
return response.JSON{Code: response.Deny, Message: err.Error()}
}
if config.Toml.LDAP.BindDN != "" {
if err := conn.Bind(config.Toml.LDAP.BindDN, config.Toml.LDAP.Password); err != nil {
return response.JSON{Code: response.Deny, Message: err.Error()}
}
}
filter := fmt.Sprintf("(%s=%s)", config.Toml.LDAP.UID, reqData.Account)
if config.Toml.LDAP.UserFilter != "" {
filter = fmt.Sprintf("(&(%s)%s)", config.Toml.LDAP.UserFilter, filter)
}
searchRequest := ldap.NewSearchRequest(
config.Toml.LDAP.BaseDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
0,
false,
filter,
[]string{config.Toml.LDAP.UID},
nil)
sr, err := conn.Search(searchRequest)
if err != nil {
return response.JSON{Code: response.Deny, Message: err.Error()}
}
if len(sr.Entries) != 1 {
return response.JSON{Code: response.Deny, Message: fmt.Sprintf("No %s record in baseDN %s", reqData.Account, config.Toml.LDAP.BaseDN)}
}
if err := conn.Bind(sr.Entries[0].DN, reqData.Password); err != nil {
return response.JSON{Code: response.Deny, Message: err.Error()}
}
} else {
if err := userData.Validate(reqData.Password); err != nil {
return response.JSON{Code: response.Deny, Message: err.Error()}
}
}
if userData.State == model.Disable {
return response.JSON{Code: response.AccountDisabled, Message: "Account is disabled"}
}
namespaceList, err := model.Namespace{UserID: userData.ID}.GetAllByUserID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
} else if len(namespaceList) == 0 {
return response.JSON{Code: response.Error, Message: "No space assigned, please contact the administrator"}
}
token, err := userData.CreateToken()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
_ = model.User{ID: userData.ID, LastLoginTime: time.Now().Format("20060102150405")}.UpdateLastLoginTime()
cookie := http.Cookie{
Name: config.Toml.Cookie.Name,
Value: token,
Path: "/",
MaxAge: config.Toml.Cookie.Expire,
HttpOnly: true,
}
http.SetCookie(gp.ResponseWriter, &cookie)
return response.JSON{
Data: struct {
Token string `json:"token"`
NamespaceList model.Namespaces `json:"namespaceList"`
}{Token: token, NamespaceList: namespaceList},
}
}
func (User) ExtLogin(gp *server.Goploy) server.Response {
type ReqData struct {
Account string `json:"account" validate:"min=1,max=25"`
Time int64 `json:"time"`
Token string `json:"token" validate:"len=32"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
}
if time.Now().Unix() > reqData.Time+30 {
return response.JSON{Code: response.IllegalParam, Message: "request time expired"}
}
h := md5.New()
h.Write([]byte(reqData.Account + config.Toml.JWT.Key + strconv.FormatInt(reqData.Time, 10)))
signedToken := hex.EncodeToString(h.Sum(nil))
if signedToken != reqData.Token {
return response.JSON{Code: response.IllegalParam, Message: "sign error"}
}
userData, err := model.User{Account: reqData.Account}.GetDataByAccount()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if userData.State == model.Disable {
return response.JSON{Code: response.AccountDisabled, Message: "Account is disabled"}
}
namespaceList, err := model.Namespace{UserID: userData.ID}.GetAllByUserID()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
} else if len(namespaceList) == 0 {
return response.JSON{Code: response.Error, Message: "No space assigned, please contact the administrator"}
}
token, err := userData.CreateToken()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
_ = model.User{ID: userData.ID, LastLoginTime: time.Now().Format("20060102150405")}.UpdateLastLoginTime()
cookie := http.Cookie{
Name: config.Toml.Cookie.Name,
Value: token,
Path: "/",
MaxAge: config.Toml.Cookie.Expire,
HttpOnly: true,
}
http.SetCookie(gp.ResponseWriter, &cookie)
return response.JSON{
Data: struct {
Token string `json:"token"`
NamespaceList model.Namespaces `json:"namespaceList"`
}{Token: token, NamespaceList: namespaceList},
}
}
func (User) Info(gp *server.Goploy) server.Response {
type RespData struct {
UserInfo struct {
ID int64 `json:"id"`
Account string `json:"account"`
Name string `json:"name"`
SuperManager int64 `json:"superManager"`
} `json:"userInfo"`
Namespace struct {
ID int64 `json:"id"`
PermissionIDs []int64 `json:"permissionIds"`
} `json:"namespace"`
}
data := RespData{}
data.UserInfo.ID = gp.UserInfo.ID
data.UserInfo.Name = gp.UserInfo.Name
data.UserInfo.Account = gp.UserInfo.Account
data.UserInfo.SuperManager = gp.UserInfo.SuperManager
data.Namespace.ID = gp.Namespace.ID
data.Namespace.PermissionIDs = make([]int64, len(gp.Namespace.PermissionIDs))
i := 0
for k := range gp.Namespace.PermissionIDs {
data.Namespace.PermissionIDs[i] = k
i++
}
return response.JSON{Data: data}
}
func (User) GetList(*server.Goploy) server.Response {
users, err := model.User{}.GetList()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Users model.Users `json:"list"`
}{Users: users},
}
}
func (User) GetOption(*server.Goploy) server.Response {
users, err := model.User{}.GetAll()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{Data: struct {
Users model.Users `json:"list"`
}{Users: users}}
}
func (User) Add(gp *server.Goploy) server.Response {
type ReqData struct {
Account string `json:"account" validate:"min=1,max=25"`
Password string `json:"password" validate:"omitempty,password"`
Name string `json:"name" validate:"required"`
Contact string `json:"contact" validate:"omitempty,len=11,numeric"`
SuperManager int64 `json:"superManager" validate:"min=0,max=1"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
userInfo, err := model.User{Account: reqData.Account}.GetDataByAccount()
if err != nil && err != sql.ErrNoRows {
return response.JSON{Code: response.Error, Message: err.Error()}
} else if userInfo != (model.User{}) {
return response.JSON{Code: response.Error, Message: "Account is already exist"}
}
id, err := model.User{
Account: reqData.Account,
Password: reqData.Password,
Name: reqData.Name,
Contact: reqData.Contact,
SuperManager: reqData.SuperManager,
}.AddRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if reqData.SuperManager == model.SuperManager {
if err := (model.NamespaceUser{UserID: id}).AddAdminByUserID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.ProjectUser{UserID: id}).AddAdminByUserID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
}
return response.JSON{
Data: struct {
ID int64 `json:"id"`
}{ID: id},
}
}
func (User) Edit(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
Password string `json:"password" validate:"omitempty,password"`
Name string `json:"name" validate:"required"`
Contact string `json:"contact" validate:"omitempty,len=11,numeric"`
SuperManager int64 `json:"superManager" validate:"min=0,max=1"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
userInfo, err := model.User{ID: reqData.ID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
err = model.User{
ID: reqData.ID,
Password: reqData.Password,
Name: reqData.Name,
Contact: reqData.Contact,
SuperManager: reqData.SuperManager,
}.EditRow()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if userInfo.SuperManager == model.SuperManager && reqData.SuperManager == model.GeneralUser {
if err := (model.NamespaceUser{UserID: reqData.ID}).DeleteByUserID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.ProjectUser{UserID: reqData.ID}).DeleteByUserID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
} else if userInfo.SuperManager == model.GeneralUser && reqData.SuperManager == model.SuperManager {
if err := (model.NamespaceUser{UserID: reqData.ID}).AddAdminByUserID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.ProjectUser{UserID: reqData.ID}).AddAdminByUserID(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
}
return response.JSON{}
}
func (User) Remove(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"gt=0"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if reqData.ID == 1 {
return response.JSON{Code: response.Error, Message: "Can not delete the super manager"}
}
if err := (model.User{ID: reqData.ID}).RemoveRow(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (User) ChangePassword(gp *server.Goploy) server.Response {
type ReqData struct {
OldPassword string `json:"oldPwd" validate:"password"`
NewPassword string `json:"newPwd" validate:"password"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
userData, err := model.User{ID: gp.UserInfo.ID}.GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := userData.Validate(reqData.OldPassword); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if err := (model.User{ID: gp.UserInfo.ID, Password: reqData.NewPassword}).UpdatePassword(); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}

View File

@ -13,11 +13,13 @@ import (
"flag"
"fmt"
"github.com/hashicorp/go-version"
"github.com/zhenorzz/goploy/cmd/server/api"
"github.com/zhenorzz/goploy/cmd/server/task"
"github.com/zhenorzz/goploy/cmd/server/ws"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/model"
"github.com/zhenorzz/goploy/route"
"github.com/zhenorzz/goploy/task"
"io/ioutil"
"log"
"net/http"
@ -87,13 +89,28 @@ func main() {
println("Log: " + config.Toml.Log.Path)
println("Listen: " + config.Toml.Web.Port)
println("Running...")
route.Init()
task.Init()
go checkUpdate()
// server
srv := http.Server{
Addr: ":" + config.Toml.Web.Port,
srv := server.Server{
Server: http.Server{
Addr: ":" + config.Toml.Web.Port,
},
Router: server.NewRouter(),
}
srv.Router.Register(api.User{})
srv.Router.Register(api.Namespace{})
srv.Router.Register(api.Role{})
srv.Router.Register(api.Project{})
srv.Router.Register(api.Repository{})
srv.Router.Register(api.Monitor{})
srv.Router.Register(api.Deploy{})
srv.Router.Register(api.Server{})
srv.Router.Register(api.Log{})
srv.Router.Register(api.Cron{})
srv.Router.Register(api.Agent{})
srv.Router.Register(ws.GetHub())
go func() {
c := make(chan os.Signal, 1)
@ -116,9 +133,7 @@ func main() {
}
println("Task shutdown gracefully")
}()
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("ListenAndServe: ", err.Error())
}
srv.Spin()
_ = os.Remove(config.GetAssetDir())
println("shutdown success")
}

626
cmd/server/task/deploy.go Normal file
View File

@ -0,0 +1,626 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package task
import (
"bytes"
"container/list"
"encoding/json"
"fmt"
"github.com/zhenorzz/goploy/cmd/server/ws"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/internal/pkg/cmd"
"github.com/zhenorzz/goploy/internal/repo"
"github.com/zhenorzz/goploy/internal/transmitter"
"github.com/zhenorzz/goploy/model"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
)
var deployList = list.New()
var deployTick = time.Tick(time.Millisecond)
type Gsync struct {
UserInfo model.User
Project model.Project
ProjectServers model.ProjectServers
CommitInfo repo.CommitInfo
CommitID string
Branch string
}
type syncMessage struct {
serverName string
projectID int64
detail string
state int
}
var projectLogFormat = "projectID: %d %s"
func startDeployTask() {
atomic.AddInt32(&counter, 1)
var deployingNumber int32
var wg sync.WaitGroup
go func() {
for {
select {
case <-deployTick:
if atomic.LoadInt32(&deployingNumber) < config.Toml.APP.DeployLimit {
atomic.AddInt32(&deployingNumber, 1)
if deployElem := deployList.Front(); deployElem != nil {
wg.Add(1)
go func(gsync Gsync) {
gsync.Exec()
atomic.AddInt32(&deployingNumber, -1)
wg.Done()
}(deployList.Remove(deployElem).(Gsync))
} else {
atomic.AddInt32(&deployingNumber, -1)
}
}
case <-stop:
wg.Wait()
atomic.AddInt32(&counter, -1)
return
}
}
}()
}
func AddDeployTask(gsync Gsync) {
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeProject,
Message: ws.ProjectMessage{
ProjectID: gsync.Project.ID,
ProjectName: gsync.Project.Name,
State: ws.TaskWaiting,
Message: "Task waiting",
Ext: struct {
LastPublishToken string `json:"lastPublishToken"`
}{gsync.Project.LastPublishToken},
},
}
model.PublishTrace{
Token: gsync.Project.LastPublishToken,
ProjectID: gsync.Project.ID,
ProjectName: gsync.Project.Name,
PublisherID: gsync.UserInfo.ID,
PublisherName: gsync.UserInfo.Name,
Type: model.QUEUE,
State: model.Success,
}.AddRow()
deployList.PushBack(gsync)
}
func (gsync Gsync) Exec() {
pkg.Logf(pkg.TRACE, "projectID: %d deploy start", gsync.Project.ID)
publishTraceModel := model.PublishTrace{
Token: gsync.Project.LastPublishToken,
ProjectID: gsync.Project.ID,
ProjectName: gsync.Project.Name,
PublisherID: gsync.UserInfo.ID,
PublisherName: gsync.UserInfo.Name,
Type: model.Pull,
}
var err error
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeProject,
Message: ws.ProjectMessage{ProjectID: gsync.Project.ID, ProjectName: gsync.Project.Name, State: ws.RepoFollow, Message: "Repo follow"},
}
r, _ := repo.GetRepo(gsync.Project.RepoType)
if len(gsync.CommitID) == 0 {
err = r.Follow(gsync.Project, "origin/"+gsync.Project.Branch)
} else {
err = r.Follow(gsync.Project, gsync.CommitID)
}
if err != nil {
pkg.Logf(pkg.ERROR, projectLogFormat, gsync.Project.ID, err)
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeProject,
Message: ws.ProjectMessage{ProjectID: gsync.Project.ID, ProjectName: gsync.Project.Name, State: ws.DeployFail, Message: err.Error()},
}
_ = gsync.Project.DeployFail()
publishTraceModel.Detail = err.Error()
publishTraceModel.State = model.Fail
if _, err := publishTraceModel.AddRow(); err != nil {
pkg.Logf(pkg.ERROR, projectLogFormat, gsync.Project.ID, err)
}
gsync.notify(model.ProjectFail, err.Error())
return
}
commitList, _ := r.CommitLog(gsync.Project.ID, 1)
gsync.CommitInfo = commitList[0]
if gsync.Branch != "" {
gsync.CommitInfo.Branch = gsync.Branch
} else {
gsync.CommitInfo.Branch = "origin/" + gsync.Project.Branch
}
ext, _ := json.Marshal(gsync.CommitInfo)
publishTraceModel.Ext = string(ext)
publishTraceModel.State = model.Success
if _, err := publishTraceModel.AddRow(); err != nil {
pkg.Logf(pkg.ERROR, projectLogFormat, gsync.Project.ID, err)
return
}
if totalFileNumber, err := (model.ProjectFile{ProjectID: gsync.Project.ID}).GetTotalByProjectID(); err != nil {
pkg.Logf(pkg.ERROR, projectLogFormat, gsync.Project.ID, err)
return
} else if totalFileNumber > 0 {
if err := pkg.CopyDir(config.GetProjectFilePath(gsync.Project.ID), config.GetProjectPath(gsync.Project.ID)); err != nil {
pkg.Logf(pkg.ERROR, projectLogFormat, gsync.Project.ID, err)
return
}
}
if gsync.Project.AfterPullScript != "" {
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeProject,
Message: ws.ProjectMessage{ProjectID: gsync.Project.ID, ProjectName: gsync.Project.Name, State: ws.AfterPullScript, Message: "Run pull script"},
}
publishTraceModel.Type = model.AfterPull
ext, _ = json.Marshal(struct {
Script string `json:"script"`
}{gsync.Project.AfterPullScript})
publishTraceModel.Ext = string(ext)
pkg.Logf(pkg.TRACE, projectLogFormat, gsync.Project.ID, gsync.Project.AfterPullScript)
if outputString, err := gsync.runAfterPullScript(); err != nil {
pkg.Logf(pkg.ERROR, projectLogFormat, gsync.Project.ID, fmt.Sprintf("err: %s, output: %s", err, outputString))
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeProject,
Message: ws.ProjectMessage{ProjectID: gsync.Project.ID, ProjectName: gsync.Project.Name, State: ws.DeployFail, Message: err.Error()},
}
_ = gsync.Project.DeployFail()
publishTraceModel.Detail = fmt.Sprintf("err: %s\noutput: %s", err, outputString)
publishTraceModel.State = model.Fail
if _, err := publishTraceModel.AddRow(); err != nil {
pkg.Logf(pkg.ERROR, projectLogFormat, gsync.Project.ID, err)
}
gsync.notify(model.ProjectFail, err.Error())
return
} else {
publishTraceModel.Detail = outputString
publishTraceModel.State = model.Success
if _, err := publishTraceModel.AddRow(); err != nil {
pkg.Logf(pkg.ERROR, projectLogFormat, gsync.Project.ID, err)
}
}
}
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeProject,
Message: ws.ProjectMessage{ProjectID: gsync.Project.ID, ProjectName: gsync.Project.Name, State: ws.Rsync, Message: "Sync"},
}
ch := make(chan syncMessage, len(gsync.ProjectServers))
gsync.remoteSync(ch)
message := ""
for i := 0; i < len(gsync.ProjectServers); i++ {
syncMessage := <-ch
if syncMessage.state == model.ProjectFail {
message += syncMessage.serverName + " error message: " + syncMessage.detail
}
}
close(ch)
if message == "" {
_ = gsync.Project.DeploySuccess()
pkg.Logf(pkg.TRACE, projectLogFormat, gsync.Project.ID, "deploy success")
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeProject,
Message: ws.ProjectMessage{ProjectID: gsync.Project.ID, ProjectName: gsync.Project.Name, State: ws.DeploySuccess, Message: "Success", Ext: gsync.CommitInfo},
}
gsync.notify(model.ProjectSuccess, message)
} else {
_ = gsync.Project.DeployFail()
pkg.Logf(pkg.TRACE, projectLogFormat, gsync.Project.ID, "deploy fail")
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeProject,
Message: ws.ProjectMessage{ProjectID: gsync.Project.ID, ProjectName: gsync.Project.Name, State: ws.DeployFail, Message: message},
}
gsync.notify(model.ProjectFail, message)
}
if gsync.Project.SymlinkPath != "" {
gsync.removeExpiredBackup()
}
return
}
func (gsync Gsync) runAfterPullScript() (string, error) {
project := gsync.Project
commitInfo := gsync.CommitInfo
srcPath := config.GetProjectPath(project.ID)
scriptName := "goploy-after-pull." + pkg.GetScriptExt(project.AfterPullScriptMode)
scriptFullName := path.Join(srcPath, scriptName)
scriptMode := "bash"
if len(project.AfterPullScriptMode) != 0 {
scriptMode = project.AfterPullScriptMode
}
scriptText := project.ReplaceVars(commitInfo.ReplaceVars(project.AfterPullScript))
_ = ioutil.WriteFile(scriptFullName, []byte(scriptText), 0755)
var commandOptions []string
if project.AfterPullScriptMode == "cmd" {
commandOptions = append(commandOptions, "/C")
scriptFullName, _ = filepath.Abs(scriptFullName)
}
commandOptions = append(commandOptions, scriptFullName)
handler := exec.Command(scriptMode, commandOptions...)
handler.Dir = srcPath
if output, err := handler.CombinedOutput(); err != nil {
return "", err
} else {
_ = os.Remove(scriptFullName)
return string(output), nil
}
}
func (gsync Gsync) remoteSync(msgChIn chan<- syncMessage) {
var serverSync = func(projectServer model.ProjectServer) {
project := gsync.Project
publishTraceModel := model.PublishTrace{
Token: project.LastPublishToken,
ProjectID: project.ID,
ProjectName: project.Name,
PublisherID: gsync.UserInfo.ID,
PublisherName: gsync.UserInfo.Name,
Type: model.Deploy,
}
// write after deploy script for rsync
scriptName := fmt.Sprintf("goploy-after-deploy-p%d-s%d.%s", project.ID, projectServer.ServerID, pkg.GetScriptExt(project.AfterDeployScriptMode))
if len(project.AfterDeployScript) != 0 {
scriptContent := project.ReplaceVars(project.AfterDeployScript)
scriptContent = projectServer.ReplaceVars(scriptContent)
_ = ioutil.WriteFile(path.Join(config.GetProjectPath(project.ID), scriptName), []byte(scriptContent), 0755)
}
transmitterEntity := transmitter.New(project, projectServer)
logCmd := transmitterEntity.String()
pkg.Log(pkg.TRACE, "projectID: "+strconv.FormatInt(project.ID, 10)+" "+logCmd)
ext, _ := json.Marshal(struct {
ServerID int64 `json:"serverId"`
ServerName string `json:"serverName"`
Command string `json:"command"`
}{projectServer.ServerID, projectServer.ServerName, logCmd})
publishTraceModel.Ext = string(ext)
if transmitterOutput, err := transmitterEntity.Exec(); err != nil {
pkg.Log(pkg.ERROR, fmt.Sprintf("projectID: %d transmit exec err: %s, output: %s", project.ID, err, transmitterOutput))
publishTraceModel.Detail = fmt.Sprintf("err: %s\noutput: %s", err, transmitterOutput)
publishTraceModel.State = model.Fail
publishTraceModel.AddRow()
msgChIn <- syncMessage{
serverName: projectServer.ServerName,
projectID: project.ID,
detail: err.Error(),
state: model.ProjectFail,
}
return
} else {
publishTraceModel.Detail = transmitterOutput
publishTraceModel.State = model.Success
publishTraceModel.AddRow()
}
var afterDeployCommands []string
cmdEntity := cmd.New(projectServer.ServerOS)
if len(project.SymlinkPath) != 0 {
destDir := path.Join(project.SymlinkPath, project.LastPublishToken)
afterDeployCommands = append(afterDeployCommands, cmdEntity.Symlink(destDir, project.Path))
}
if len(project.AfterDeployScript) != 0 {
afterDeployScriptPath := path.Join(project.Path, scriptName)
afterDeployCommands = append(afterDeployCommands, cmdEntity.Script(project.AfterDeployScriptMode, afterDeployScriptPath))
afterDeployCommands = append(afterDeployCommands, cmdEntity.Remove(afterDeployScriptPath))
}
// no symlink and deploy script
if len(afterDeployCommands) == 0 {
msgChIn <- syncMessage{
serverName: projectServer.ServerName,
projectID: project.ID,
state: model.ProjectSuccess,
}
return
}
completeAfterDeployCmd := strings.Join(afterDeployCommands, "&&")
publishTraceModel.Type = model.AfterDeploy
ext, _ = json.Marshal(struct {
ServerID int64 `json:"serverId"`
ServerName string `json:"serverName"`
Script string `json:"script"`
}{projectServer.ServerID, projectServer.ServerName, completeAfterDeployCmd})
publishTraceModel.Ext = string(ext)
client, err := projectServer.ToSSHConfig().Dial()
if err != nil {
pkg.Log(pkg.ERROR, err.Error())
publishTraceModel.Detail = err.Error()
publishTraceModel.State = model.Fail
publishTraceModel.AddRow()
msgChIn <- syncMessage{
serverName: projectServer.ServerName,
projectID: project.ID,
detail: err.Error(),
state: model.ProjectFail,
}
return
}
defer client.Close()
session, sessionErr := client.NewSession()
if sessionErr != nil {
pkg.Log(pkg.ERROR, sessionErr.Error())
publishTraceModel.Detail = sessionErr.Error()
publishTraceModel.State = model.Fail
publishTraceModel.AddRow()
msgChIn <- syncMessage{
serverName: projectServer.ServerName,
projectID: project.ID,
detail: sessionErr.Error(),
state: model.ProjectFail,
}
return
}
defer session.Close()
pkg.Log(pkg.TRACE, fmt.Sprintf("projectID: %d ssh exec: %s", project.ID, completeAfterDeployCmd))
if output, err := session.CombinedOutput(completeAfterDeployCmd); err != nil {
pkg.Log(pkg.ERROR, fmt.Sprintf("projectID: %d ssh exec err: %s, output: %s", project.ID, err, output))
publishTraceModel.Detail = fmt.Sprintf("err: %s\noutput: %s", err, output)
publishTraceModel.State = model.Fail
if _, err := publishTraceModel.AddRow(); err != nil {
pkg.Log(pkg.ERROR, "projectID: "+strconv.FormatInt(project.ID, 10)+" "+err.Error())
}
msgChIn <- syncMessage{
serverName: projectServer.ServerName,
projectID: project.ID,
detail: fmt.Sprintf("%s\noutput: %s", err.Error(), output),
state: model.ProjectFail,
}
return
} else {
publishTraceModel.Detail = string(output)
publishTraceModel.State = model.Success
if _, err := publishTraceModel.AddRow(); err != nil {
pkg.Log(pkg.ERROR, "projectID: "+strconv.FormatInt(project.ID, 10)+" "+err.Error())
}
}
msgChIn <- syncMessage{
serverName: projectServer.ServerName,
projectID: project.ID,
state: model.ProjectSuccess,
}
return
}
for _, projectServer := range gsync.ProjectServers {
if gsync.Project.DeployServerMode == "serial" {
serverSync(projectServer)
} else {
go serverSync(projectServer)
}
}
}
// commit id
// commit message
// server ip & name
// deploy user name
// deploy time
func (gsync Gsync) notify(deployState int, detail string) {
if gsync.Project.NotifyType == 0 {
return
}
serverList := ""
for _, projectServer := range gsync.ProjectServers {
if projectServer.ServerName != projectServer.ServerIP {
serverList += projectServer.ServerName + "(" + projectServer.ServerIP + ")"
} else {
serverList += projectServer.ServerIP
}
serverList += ", "
}
serverList = strings.TrimRight(serverList, ", ")
project := gsync.Project
commitInfo := gsync.CommitInfo
var err error
var resp *http.Response
if project.NotifyType == model.NotifyWeiXin {
type markdown struct {
Content string `json:"content"`
}
type message struct {
Msgtype string `json:"msgtype"`
Markdown markdown `json:"markdown"`
}
content := "Deploy: <font color=\"warning\">" + project.Name + "</font>\n"
content += "Publisher: <font color=\"comment\">" + project.PublisherName + "</font>\n"
content += "Author: <font color=\"comment\">" + commitInfo.Author + "</font>\n"
if commitInfo.Tag != "" {
content += "Tag: <font color=\"comment\">" + commitInfo.Tag + "</font>\n"
}
content += "Branch: <font color=\"comment\">" + commitInfo.Branch + "</font>\n"
content += "CommitSHA: <font color=\"comment\">" + commitInfo.Commit + "</font>\n"
content += "CommitMessage: <font color=\"comment\">" + commitInfo.Message + "</font>\n"
content += "ServerList: <font color=\"comment\">" + serverList + "</font>\n"
if deployState == model.ProjectFail {
content += "State: <font color=\"red\">fail</font> \n"
content += "> Detail: <font color=\"comment\">" + detail + "</font>"
} else {
content += "State: <font color=\"green\">success</font>"
}
msg := message{
Msgtype: "markdown",
Markdown: markdown{
Content: content,
},
}
b, _ := json.Marshal(msg)
resp, err = http.Post(project.NotifyTarget, "application/json", bytes.NewBuffer(b))
} else if project.NotifyType == model.NotifyDingTalk {
type markdown struct {
Title string `json:"title"`
Text string `json:"text"`
}
type message struct {
Msgtype string `json:"msgtype"`
Markdown markdown `json:"markdown"`
}
text := "#### Deploy" + project.Name + " \n "
text += "#### Publisher" + project.PublisherName + " \n "
text += "#### Author" + commitInfo.Author + " \n "
if commitInfo.Tag != "" {
text += "#### Tag" + commitInfo.Tag + " \n "
}
text += "#### Branch" + commitInfo.Branch + " \n "
text += "#### CommitSHA" + commitInfo.Commit + " \n "
text += "#### CommitMessage" + commitInfo.Message + " \n "
text += "#### ServerList" + serverList + " \n "
if deployState == model.ProjectFail {
text += "#### State <font color=\"red\">fail</font> \n "
text += "> Detail: <font color=\"comment\">" + detail + "</font>"
} else {
text += "#### State <font color=\"green\">success</font>"
}
msg := message{
Msgtype: "markdown",
Markdown: markdown{
Title: project.Name,
Text: text,
},
}
b, _ := json.Marshal(msg)
resp, err = http.Post(project.NotifyTarget, "application/json", bytes.NewBuffer(b))
} else if project.NotifyType == model.NotifyFeiShu {
type content struct {
Text string `json:"text"`
}
type message struct {
MsgType string `json:"msg_type"`
Content content `json:"content"`
}
text := ""
text += "Publisher: " + project.PublisherName + "\n"
text += "Author: " + commitInfo.Author + "\n"
if commitInfo.Tag != "" {
text += "Tag: " + commitInfo.Tag + "\n"
}
text += "Branch: " + commitInfo.Branch + "\n"
text += "CommitSHA: " + commitInfo.Commit + "\n"
text += "CommitMessage: " + commitInfo.Message + "\n"
text += "ServerList: " + serverList + "\n"
if deployState == model.ProjectFail {
text += "State: fail\n "
text += "Detail: " + detail
} else {
text += "State: success"
}
msg := message{
MsgType: "text",
Content: content{
Text: text,
},
}
b, _ := json.Marshal(msg)
resp, err = http.Post(project.NotifyTarget, "application/json", bytes.NewBuffer(b))
} else if project.NotifyType == model.NotifyCustom {
type message struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
ProjectID int64 `json:"projectId"`
ProjectName string `json:"projectName"`
Publisher string `json:"publisher"`
Author string `json:"author"`
Branch string `json:"branch"`
Tag string `json:"tag"`
CommitSHA string `json:"commitSHA"`
CommitMessage string `json:"commitMessage"`
ServerList string `json:"serverList"`
} `json:"data"`
}
code := 0
if deployState == model.ProjectFail {
code = 1
}
msg := message{
Code: code,
Message: detail,
}
msg.Data.ProjectID = project.ID
msg.Data.ProjectName = project.Name
msg.Data.Publisher = project.PublisherName
msg.Data.Author = commitInfo.Author
msg.Data.Branch = commitInfo.Branch
msg.Data.Tag = commitInfo.Tag
msg.Data.CommitSHA = commitInfo.Commit
msg.Data.CommitMessage = commitInfo.Message
msg.Data.ServerList = serverList
b, _ := json.Marshal(msg)
resp, err = http.Post(project.NotifyTarget, "application/json", bytes.NewBuffer(b))
}
if err != nil {
pkg.Log(pkg.ERROR, fmt.Sprintf("projectID: %d notify exec err: %s", project.ID, err))
} else {
defer resp.Body.Close()
responseData, err := ioutil.ReadAll(resp.Body)
if err != nil {
pkg.Log(pkg.ERROR, fmt.Sprintf("projectID: %d notify read body err: %s", project.ID, err))
} else {
pkg.Log(pkg.TRACE, fmt.Sprintf("projectID: %d notify success: %s", project.ID, string(responseData)))
}
}
}
//keep the latest 10 project
func (gsync Gsync) removeExpiredBackup() {
var wg sync.WaitGroup
for _, projectServer := range gsync.ProjectServers {
wg.Add(1)
go func(projectServer model.ProjectServer) {
defer wg.Done()
client, err := projectServer.ToSSHConfig().Dial()
if err != nil {
pkg.Log(pkg.ERROR, err.Error())
return
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
pkg.Log(pkg.ERROR, err.Error())
return
}
defer session.Close()
var sshOutbuf, sshErrbuf bytes.Buffer
session.Stdout = &sshOutbuf
session.Stderr = &sshErrbuf
if err = session.Run("cd " + gsync.Project.SymlinkPath + ";ls -t | awk 'NR>" + strconv.Itoa(int(gsync.Project.SymlinkBackupNumber)) + "' | xargs rm -rf"); err != nil {
pkg.Log(pkg.ERROR, err.Error())
}
}(projectServer)
}
wg.Wait()
}

108
cmd/server/task/monitor.go Normal file
View File

@ -0,0 +1,108 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package task
import (
"database/sql"
"github.com/zhenorzz/goploy/cmd/server/ws"
"github.com/zhenorzz/goploy/internal/monitor"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/model"
"sync/atomic"
"time"
)
var monitorTick = time.Tick(time.Minute)
func startMonitorTask() {
atomic.AddInt32(&counter, 1)
go func() {
for {
select {
case <-monitorTick:
monitorTask()
case <-stop:
atomic.AddInt32(&counter, -1)
return
}
}
}()
}
type MonitorCache struct {
errorTimes int
time int64
silentCycle int
}
var monitorCaches = map[int64]MonitorCache{}
func monitorTask() {
monitors, err := model.Monitor{State: model.Enable}.GetAllByState()
if err != nil && err != sql.ErrNoRows {
pkg.Log(pkg.ERROR, "get m list error, detail:"+err.Error())
}
monitorIDs := map[int64]struct{}{}
for _, m := range monitors {
monitorIDs[m.ID] = struct{}{}
monitorCache, ok := monitorCaches[m.ID]
if !ok {
monitorCaches[m.ID] = MonitorCache{}
monitorCache = monitorCaches[m.ID]
}
if monitorCache.silentCycle > 0 {
monitorCache.silentCycle++
if monitorCache.silentCycle >= m.SilentCycle {
monitorCache.silentCycle = 0
}
monitorCaches[m.ID] = monitorCache
continue
}
now := time.Now().Unix()
println(m.Name, "detect", time.Now().String())
if int(now-monitorCache.time) >= m.Second {
println(m.Name, "in", time.Now().String())
monitorCache.time = now
ms, err := monitor.NewMonitorFromTarget(m.Type, m.Target)
if err != nil {
_ = m.TurnOff(err.Error())
pkg.Log(pkg.ERROR, "m "+m.Name+" encounter error, "+err.Error())
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeMonitor,
Message: ws.MonitorMessage{MonitorID: m.ID, State: ws.MonitorTurnOff, ErrorContent: err.Error()},
}
} else if err := ms.Check(); err != nil {
monitorErrorContent := err.Error()
monitorCache.errorTimes++
pkg.Log(pkg.ERROR, "m "+m.Name+" encounter error, "+monitorErrorContent)
if m.Times <= uint16(monitorCache.errorTimes) {
if body, err := m.Notify(monitorErrorContent); err != nil {
pkg.Log(pkg.ERROR, "m "+m.Name+" notify error, "+err.Error())
} else {
monitorCache.errorTimes = 0
monitorCache.silentCycle = 1
pkg.Log(pkg.TRACE, "m "+m.Name+" notify return "+body)
_ = m.TurnOff(monitorErrorContent)
ws.GetHub().Data <- &ws.Data{
Type: ws.TypeMonitor,
Message: ws.MonitorMessage{MonitorID: m.ID, State: ws.MonitorTurnOff, ErrorContent: monitorErrorContent},
}
}
}
} else {
monitorCache.errorTimes = 0
}
monitorCaches[m.ID] = monitorCache
}
}
for cacheID := range monitorCaches {
if _, ok := monitorIDs[cacheID]; !ok {
delete(monitorCaches, cacheID)
}
}
}

View File

@ -0,0 +1,83 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package task
import (
"database/sql"
"github.com/google/uuid"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/model"
"sync/atomic"
"time"
)
var projectTick = time.Tick(time.Minute)
func startProjectTask() {
atomic.AddInt32(&counter, 1)
go func() {
for {
select {
case <-projectTick:
projectTask()
case <-stop:
atomic.AddInt32(&counter, -1)
return
}
}
}()
}
func projectTask() {
date := time.Now().Format("2006-01-02 15:04:05")
projectTasks, err := model.ProjectTask{}.GetNotRunListLTDate(date)
if err != nil && err != sql.ErrNoRows {
pkg.Log(pkg.ERROR, "get project task list error, detail:"+err.Error())
}
for _, projectTask := range projectTasks {
project, err := model.Project{ID: projectTask.ProjectID}.GetData()
if err != nil {
pkg.Log(pkg.ERROR, "publish task has no project, detail:"+err.Error())
continue
}
if err := projectTask.SetRun(); err != nil {
pkg.Log(pkg.ERROR, "publish task set run fail, detail:"+err.Error())
continue
}
projectServers, err := model.ProjectServer{ProjectID: projectTask.ProjectID}.GetBindServerListByProjectID()
if err != nil {
pkg.Log(pkg.ERROR, "publish task has no server, detail:"+err.Error())
continue
}
userInfo, err := model.User{ID: 1}.GetData()
if err != nil {
pkg.Log(pkg.ERROR, "publish task has no user, detail:"+err.Error())
continue
}
project.PublisherID = userInfo.ID
project.PublisherName = userInfo.Name
project.DeployState = model.ProjectDeploying
project.LastPublishToken = uuid.New().String()
err = project.Publish()
if err != nil {
pkg.Log(pkg.ERROR, "publish task change state error, detail:"+err.Error())
continue
}
AddDeployTask(Gsync{
UserInfo: userInfo,
Project: project,
ProjectServers: projectServers,
CommitID: projectTask.CommitID,
Branch: projectTask.Branch,
})
}
}

View File

@ -0,0 +1,115 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package task
import (
"database/sql"
"fmt"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/model"
"strings"
"sync/atomic"
"time"
)
var serverMonitorTick = time.Tick(time.Minute)
func startServerMonitorTask() {
atomic.AddInt32(&counter, 1)
go func() {
for {
select {
case <-serverMonitorTick:
serverMonitorTask()
case <-stop:
atomic.AddInt32(&counter, -1)
return
}
}
}()
}
type ServerMonitorCache struct {
lastCycle int
silentCycle int
}
var serverMonitorCaches = map[int64]ServerMonitorCache{}
var loop = 0
func serverMonitorTask() {
loop++
var serverCaches = map[int64]model.Server{}
serverMonitorTasks, err := model.ServerMonitor{}.GetAllModBy(loop, time.Now().Format("15:04"))
if err != nil && err != sql.ErrNoRows {
pkg.Log(pkg.ERROR, "get server monitor list error, detail:"+err.Error())
}
for _, serverMonitor := range serverMonitorTasks {
monitorCache, ok := serverMonitorCaches[serverMonitor.ID]
if !ok {
serverMonitorCaches[serverMonitor.ID] = ServerMonitorCache{}
monitorCache = serverMonitorCaches[serverMonitor.ID]
}
if monitorCache.silentCycle > 0 {
monitorCache.silentCycle++
if monitorCache.silentCycle >= serverMonitor.SilentCycle {
monitorCache.silentCycle = 0
}
serverMonitorCaches[serverMonitor.ID] = monitorCache
continue
}
cycleValue, err := model.ServerAgentLog{
ServerID: serverMonitor.ServerID,
Item: serverMonitor.Item,
}.GetCycleValue(serverMonitor.GroupCycle, serverMonitor.Formula)
if err != nil {
pkg.Log(pkg.ERROR, "get cycle value failed, detail:"+err.Error())
continue
}
compareRes := false
switch serverMonitor.Operator {
case ">=":
compareRes = strings.Compare(cycleValue, serverMonitor.Value) >= 0
case ">":
compareRes = strings.Compare(cycleValue, serverMonitor.Value) > 0
case "<=":
compareRes = strings.Compare(cycleValue, serverMonitor.Value) <= 0
case "<":
compareRes = strings.Compare(cycleValue, serverMonitor.Value) < 0
case "!=":
compareRes = strings.Compare(cycleValue, serverMonitor.Value) != 0
}
if compareRes {
monitorCache.lastCycle++
} else {
monitorCache.lastCycle = 0
}
if monitorCache.lastCycle >= serverMonitor.LastCycle {
monitorCache.silentCycle = 1
monitorCache.lastCycle = 0
if _, ok := serverCaches[serverMonitor.ServerID]; !ok {
server, err := model.Server{ID: serverMonitor.ServerID}.GetData()
if err != nil {
pkg.Log(pkg.ERROR, fmt.Sprintf("monitor task %d has no server, detail: %s", serverMonitor.ID, err.Error()))
continue
}
serverCaches[serverMonitor.ServerID] = server
}
body, err := serverMonitor.Notify(serverCaches[serverMonitor.ServerID], cycleValue)
if err != nil {
pkg.Log(pkg.ERROR, fmt.Sprintf("monitor task %d notify error, %s", serverMonitor.ID, err.Error()))
} else {
pkg.Log(pkg.TRACE, fmt.Sprintf("monitor task %d notify return %s", serverMonitor.ID, body))
}
}
serverMonitorCaches[serverMonitor.ID] = monitorCache
}
}

37
cmd/server/task/task.go Normal file
View File

@ -0,0 +1,37 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package task
import (
"context"
"sync/atomic"
"time"
)
var counter int32
var stop = make(chan struct{})
func Init() {
startMonitorTask()
startProjectTask()
startServerMonitorTask()
startDeployTask()
}
func Shutdown(ctx context.Context) error {
close(stop)
ticker := time.NewTicker(10 * time.Millisecond)
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if atomic.LoadInt32(&counter) == 0 {
return nil
}
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package ws
type MonitorMessage struct {
MonitorID int64 `json:"monitorId"`
State uint8 `json:"state"`
ErrorContent string `json:"errorContent"`
}
const (
MonitorTurnOff = 0
)
func (projectMessage MonitorMessage) canSendTo(*Client) error {
return nil
}

View File

@ -0,0 +1,26 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package ws
type ProjectMessage struct {
ProjectID int64 `json:"projectId"`
ProjectName string `json:"projectName"`
State uint8 `json:"state"`
Message string `json:"message"`
Ext interface{} `json:"ext"`
}
const (
DeployFail = 0
TaskWaiting = 1
RepoFollow = 2
AfterPullScript = 3
Rsync = 4
DeploySuccess = 5
)
func (projectMessage ProjectMessage) canSendTo(*Client) error {
return nil
}

148
cmd/server/ws/sftp.go Normal file
View File

@ -0,0 +1,148 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package ws
import (
"encoding/json"
"github.com/gorilla/websocket"
"github.com/pkg/sftp"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// sftp the server file information in websocket
func (hub *Hub) sftp(gp *server.Goploy) server.Response {
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
if strings.Contains(r.Header.Get("origin"), strings.Split(r.Host, ":")[0]) {
return true
}
return false
},
}
c, err := upgrader.Upgrade(gp.ResponseWriter, gp.Request, nil)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer c.Close()
c.SetReadLimit(maxMessageSize)
c.SetReadDeadline(time.Now().Add(pongWait))
c.SetPongHandler(func(string) error { c.SetReadDeadline(time.Now().Add(pongWait)); return nil })
serverID, err := strconv.ParseInt(gp.URLQuery.Get("serverId"), 10, 64)
if err != nil {
c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
srv, err := (model.Server{ID: serverID}).GetData()
if err != nil {
c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
client, err := srv.ToSSHConfig().Dial()
if err != nil {
c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
defer client.Close()
sftpClient, err := sftp.NewClient(client)
if err != nil {
c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
defer sftpClient.Close()
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
stop := make(chan bool, 1)
defer func() {
stop <- true
}()
go func() {
for {
select {
case <-ticker.C:
if err := c.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
c.Close()
return
}
case <-stop:
return
}
}
}()
type fileInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
Mode string `json:"mode"`
ModTime string `json:"modTime"`
IsDir bool `json:"isDir"`
}
for {
messageType, message, err := c.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
pkg.Log(pkg.ERROR, err.Error())
}
break
}
if messageType == websocket.TextMessage {
var fileList []fileInfo
code := response.Pass
msg := ""
fileInfos, err := sftpClient.ReadDir(string(message))
if err != nil {
code = response.Error
msg = err.Error()
} else {
for _, f := range fileInfos {
if f.Mode()&os.ModeSymlink != 0 {
continue
}
fileList = append(fileList, fileInfo{
Name: f.Name(),
Size: f.Size(),
Mode: f.Mode().String(),
ModTime: f.ModTime().Format("2006-01-02 15:04:05"),
IsDir: f.IsDir(),
})
}
}
b, _ := json.Marshal(response.JSON{Code: code, Message: msg, Data: fileList})
if err := c.WriteMessage(websocket.TextMessage, b); err != nil {
pkg.Log(pkg.ERROR, err.Error())
break
}
// sftp log
if err := (model.SftpLog{
NamespaceID: gp.Namespace.ID,
UserID: gp.UserInfo.ID,
ServerID: serverID,
RemoteAddr: gp.Request.RemoteAddr,
UserAgent: gp.Request.UserAgent(),
Type: model.SftpLogTypeRead,
Path: string(message),
Reason: msg,
}.AddRow()); err != nil {
pkg.Log(pkg.ERROR, err.Error())
}
}
}
return response.Empty{}
}

189
cmd/server/ws/ws.go Normal file
View File

@ -0,0 +1,189 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package ws
import (
"github.com/gorilla/websocket"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"net/http"
"strings"
"time"
)
const (
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 10240
)
const (
TypeProject = 1
TypeMonitor = 3
)
// Client stores a client information
type Client struct {
Conn *websocket.Conn
UserInfo model.User
}
// Data is message struct
type Data struct {
Type int
UserIDs []int64
Message Message
}
type Message interface {
canSendTo(client *Client) error
}
// Hub is a client struct
type Hub struct {
// Registered clients.
clients map[*Client]bool
// Inbound messages from the clients.
Data chan *Data
// Register requests from the clients.
Register chan *Client
// Unregister requests from clients.
Unregister chan *Client
// ping pong ticker
ticker chan *Client
}
func init() {
go hub.run()
}
func (hub *Hub) Handler() []server.Route {
return []server.Route{
server.NewRoute("/ws/connect", http.MethodGet, hub.connect),
server.NewRoute("/ws/xterm", http.MethodGet, hub.xterm),
server.NewRoute("/ws/sftp", http.MethodGet, hub.sftp),
}
}
var hub = &Hub{
Data: make(chan *Data),
clients: make(map[*Client]bool),
Register: make(chan *Client),
Unregister: make(chan *Client),
ticker: make(chan *Client),
}
func GetHub() *Hub {
return hub
}
func (hub *Hub) connect(gp *server.Goploy) server.Response {
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
if strings.Contains(r.Header.Get("origin"), strings.Split(r.Host, ":")[0]) {
return true
}
return false
},
}
c, err := upgrader.Upgrade(gp.ResponseWriter, gp.Request, nil)
if err != nil {
pkg.Log(pkg.ERROR, err.Error())
return response.JSON{Code: response.Error, Message: err.Error()}
}
c.SetReadLimit(maxMessageSize)
c.SetReadDeadline(time.Now().Add(pongWait))
c.SetPongHandler(func(string) error { c.SetReadDeadline(time.Now().Add(pongWait)); return nil })
client := &Client{
Conn: c,
UserInfo: gp.UserInfo,
}
hub.Register <- client
ticker := time.NewTicker(pingPeriod)
stop := make(chan bool, 1)
go func() {
for {
select {
case <-ticker.C:
hub.ticker <- client
case <-stop:
return
}
}
}()
// you must read message to trigger pong handler
for {
_, _, err = c.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
pkg.Log(pkg.ERROR, err.Error())
}
break
}
}
defer func() {
hub.Unregister <- client
c.Close()
ticker.Stop()
stop <- true
}()
return response.Empty{}
}
// Run goroutine run the sync hub
func (hub *Hub) run() {
for {
select {
case client := <-hub.Register:
hub.clients[client] = true
case client := <-hub.Unregister:
if _, ok := hub.clients[client]; ok {
delete(hub.clients, client)
client.Conn.Close()
}
case data := <-hub.Data:
for client := range hub.clients {
if data.Message.canSendTo(client) != nil {
continue
}
// check userIDs list
for _, userID := range data.UserIDs {
if client.UserInfo.ID != userID {
continue
}
}
if err := client.Conn.WriteJSON(
struct {
Type int `json:"type"`
Message interface{} `json:"message"`
}{
Type: data.Type,
Message: data.Message,
}); websocket.IsCloseError(err) {
hub.Unregister <- client
}
}
case client := <-hub.ticker:
if _, ok := hub.clients[client]; ok {
if err := client.Conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
hub.Unregister <- client
}
}
}
}
}

191
cmd/server/ws/xterm.go Normal file
View File

@ -0,0 +1,191 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package ws
import (
"bytes"
"github.com/gorilla/websocket"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/internal/server"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"golang.org/x/crypto/ssh"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
// write data to WebSocket
// the data comes from ssh server.
type xtermBufferWriter struct {
buffer bytes.Buffer
mu sync.Mutex
}
// implement Write interface to write bytes from ssh server into bytes.Buffer.
func (w *xtermBufferWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Write(p)
}
func (hub *Hub) xterm(gp *server.Goploy) server.Response {
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
if strings.Contains(r.Header.Get("origin"), strings.Split(r.Host, ":")[0]) {
return true
}
return false
},
}
c, err := upgrader.Upgrade(gp.ResponseWriter, gp.Request, nil)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer c.Close()
c.SetReadLimit(maxMessageSize)
c.SetReadDeadline(time.Now().Add(pongWait))
c.SetPongHandler(func(string) error { c.SetReadDeadline(time.Now().Add(pongWait)); return nil })
rows, err := strconv.Atoi(gp.URLQuery.Get("rows"))
if err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
cols, err := strconv.Atoi(gp.URLQuery.Get("cols"))
if err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
serverID, err := strconv.ParseInt(gp.URLQuery.Get("serverId"), 10, 64)
if err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
srv, err := (model.Server{ID: serverID}).GetData()
if err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
client, err := srv.ToSSHConfig().Dial()
if err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
defer client.Close()
// create session
session, err := client.NewSession()
if err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
defer session.Close()
sessionStdin, err := session.StdinPipe()
if err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
comboWriter := new(xtermBufferWriter)
//ssh.stdout and stderr will write output into comboWriter
session.Stdout = comboWriter
session.Stderr = comboWriter
// Request pseudo terminal
if err := session.RequestPty("xterm", rows, cols, ssh.TerminalModes{}); err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
// Start remote shell
if err := session.Shell(); err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
// terminal log
tlID, err := model.TerminalLog{
NamespaceID: gp.Namespace.ID,
UserID: gp.UserInfo.ID,
ServerID: serverID,
RemoteAddr: gp.Request.RemoteAddr,
UserAgent: gp.Request.UserAgent(),
StartTime: time.Now().Format("20060102150405"),
}.AddRow()
if err != nil {
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
return response.Empty{}
}
var recorder *pkg.Recorder
recorder, err = pkg.NewRecorder(config.GetTerminalLogPath(tlID), "xterm", rows, cols)
if err != nil {
pkg.Log(pkg.ERROR, err.Error())
} else {
defer recorder.Close()
}
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
flushMessageTick := time.NewTicker(time.Millisecond * time.Duration(50))
defer flushMessageTick.Stop()
stop := make(chan bool, 1)
defer func() {
stop <- true
}()
go func() {
for {
select {
case <-ticker.C:
if err := c.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
c.Close()
return
}
case <-flushMessageTick.C:
if comboWriter.buffer.Len() != 0 {
err := c.WriteMessage(websocket.BinaryMessage, comboWriter.buffer.Bytes())
if err != nil {
c.Close()
return
}
if recorder != nil {
if err := recorder.WriteData(comboWriter.buffer.String()); err != nil {
pkg.Log(pkg.ERROR, err.Error())
}
}
comboWriter.buffer.Reset()
}
case <-stop:
return
}
}
}()
for {
messageType, p, err := c.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
pkg.Log(pkg.ERROR, err.Error())
}
break
}
if messageType != websocket.PongMessage {
if _, err := sessionStdin.Write(p); err != nil {
pkg.Log(pkg.ERROR, err.Error())
break
}
}
}
if err := (model.TerminalLog{
ID: tlID,
EndTime: time.Now().Format("20060102150405"),
}.EditRow()); err != nil {
pkg.Log(pkg.ERROR, err.Error())
}
return response.Empty{}
}

View File

@ -4,65 +4,4 @@
package config
import (
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
)
var (
AssetDir string
)
func GetAssetDir() string {
if AssetDir != "" {
return AssetDir
}
file, err := exec.LookPath(os.Args[0])
if err != nil {
panic(err)
}
app, err := filepath.Abs(file)
if err != nil {
panic(err)
}
i := strings.LastIndex(app, "/")
if i < 0 {
i = strings.LastIndex(app, "\\")
}
if i < 0 {
panic(err)
}
AssetDir = app[0 : i+1]
return AssetDir
}
func GetConfigFile() string {
return path.Join(GetAssetDir(), "goploy.toml")
}
func GetPidFile() string {
return path.Join(GetAssetDir(), "goploy.pid")
}
func GetRepositoryPath() string {
if Toml.APP.RepositoryPath != "" {
return path.Join(Toml.APP.RepositoryPath, "repository")
}
return path.Join(GetAssetDir(), "repository")
}
func GetProjectFilePath(projectID int64) string {
return path.Join(GetRepositoryPath(), "project-file", "project_"+strconv.FormatInt(projectID, 10))
}
func GetProjectPath(projectID int64) string {
return path.Join(GetRepositoryPath(), "project_"+strconv.FormatInt(projectID, 10))
}
func GetTerminalLogPath(tlID int64) string {
return path.Join(GetRepositoryPath(), "terminal-log", strconv.FormatInt(tlID, 10)+".cast")
}
const NamespaceHeaderName = "G-N-ID"

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package permission
package config
const (
Log = 1

134
internal/monitor/monitor.go Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package monitor
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/zhenorzz/goploy/model"
"net"
"net/http"
"os/exec"
"runtime"
"strconv"
"time"
)
type Monitor struct {
Type int
Items []string
Timeout time.Duration
Process string
Script string
}
func NewMonitorFromTarget(t int, target string) (Monitor, error) {
var m Monitor
if err := json.Unmarshal([]byte(target), &m); err != nil {
return m, err
}
m.Type = t
return m, nil
}
func (m Monitor) Check() error {
var err error
switch m.Type {
case 1:
err = m.CheckSite()
case 2:
err = m.CheckPort()
case 3:
err = m.CheckHostAlive()
case 4:
m.Script = fmt.Sprintf("ps -ef|grep -v grep|grep %s", m.Process)
fallthrough
case 5:
err = m.CheckScript()
default:
err = errors.New("type error")
}
return err
}
func (m Monitor) CheckSite() error {
for _, url := range m.Items {
client := http.Client{
Timeout: m.Timeout * time.Second,
}
if resp, err := client.Get(url); err != nil {
return err
} else if 200 < resp.StatusCode || resp.StatusCode >= 400 {
return errors.New("Unexpected response status code: " + strconv.Itoa(resp.StatusCode))
}
}
return nil
}
func (m Monitor) CheckPort() error {
for _, address := range m.Items {
conn, err := net.DialTimeout("tcp", address, m.Timeout*time.Second)
if err != nil {
return err
}
_ = conn.Close()
}
return nil
}
func (m Monitor) CheckHostAlive() error {
var stdout, stderr bytes.Buffer
for _, addr := range m.Items {
var arg []string
if runtime.GOOS == "windows" {
arg = append(arg, "-n", "1", "-w", strconv.Itoa(int(m.Timeout*1000)), addr)
} else {
arg = append(arg, "-c", "1", "-W", strconv.Itoa(int(m.Timeout)), addr)
}
stdout.Reset()
stderr.Reset()
cmd := exec.Command("ping", arg...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return errors.New(err.Error() + ", detail: " + stderr.String())
}
}
return nil
}
func (m Monitor) CheckScript() error {
for _, serverIDStr := range m.Items {
serverID, err := strconv.ParseInt(serverIDStr, 10, 64)
if err != nil {
return err
}
server, err := (model.Server{ID: serverID}).GetData()
if err != nil {
return err
} else if server.State == model.Disable {
continue
}
client, err := server.ToSSHConfig().SetTimeout(m.Timeout * time.Second).Dial()
if err != nil {
return err
}
session, err := client.NewSession()
if err != nil {
return err
}
var stdout, stderr bytes.Buffer
session.Stdout = &stdout
session.Stderr = &stderr
if err := session.Run(m.Script); err != nil {
return errors.New(err.Error() + ", stdout: " + stdout.String() + ", stderr: " + stderr.String())
}
}
return nil
}

View File

@ -0,0 +1,13 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package response
import (
"net/http"
)
type Empty struct{}
func (Empty) Write(http.ResponseWriter, *http.Request) error { return nil }

View File

@ -0,0 +1,38 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package response
import (
"io"
"net/http"
"os"
"strconv"
)
type File struct {
Filename string
}
func (f File) Write(w http.ResponseWriter, _ *http.Request) error {
file, err := os.Open(f.Filename)
if err != nil {
return err
}
fileStat, err := file.Stat()
if err != nil {
return err
}
w.Header().Set("Content-Disposition", "attachment; filename="+fileStat.Name())
w.Header().Set("Content-Type", "application/x-asciicast")
w.Header().Set("Content-Length", strconv.FormatInt(fileStat.Size(), 10))
_, err = io.Copy(w, file)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,32 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package response
import (
"encoding/json"
"net/http"
)
type JSON struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
const (
Pass = 0
Deny = 1
Error = 2
AccountDisabled = 10000
IllegalRequest = 10001
NamespaceInvalid = 10002
IllegalParam = 10003
LoginExpired = 10086
)
//JSON response
func (j JSON) Write(w http.ResponseWriter, _ *http.Request) error {
return json.NewEncoder(w).Encode(j)
}

View File

@ -0,0 +1,19 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package response
import (
"net/http"
)
type Redirect struct {
URL string
Code int
}
func (rdr Redirect) Write(w http.ResponseWriter, r *http.Request) error {
http.Redirect(w, r, rdr.URL, rdr.Code)
return nil
}

View File

@ -0,0 +1,52 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package response
import (
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"io"
"net/http"
"strconv"
)
type SftpFile struct {
Client *ssh.Client
Filename string
Disposition string // attachment | inline
}
func (sf SftpFile) Write(w http.ResponseWriter, _ *http.Request) error {
defer sf.Client.Close()
sftpClient, err := sftp.NewClient(sf.Client)
if err != nil {
return err
}
defer sftpClient.Close()
srcFile, err := sftpClient.Open(sf.Filename)
if err != nil {
return err
}
defer srcFile.Close()
fileStat, err := srcFile.Stat()
if err != nil {
return err
}
if sf.Disposition == "attachment" {
w.Header().Set("Content-Disposition", "attachment;filename="+fileStat.Name())
w.Header().Set("Content-Type", "application/octet-stream")
} else {
w.Header().Set("Content-Disposition", "inline;filename="+fileStat.Name())
}
w.Header().Set("Content-Length", strconv.FormatInt(fileStat.Size(), 10))
_, err = io.Copy(w, srcFile)
return err
}

72
internal/server/route.go Normal file
View File

@ -0,0 +1,72 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package server
import "errors"
type Route struct {
pattern string
method string // Method specifies the HTTP method (GET, POST, PUT, etc.).
permissionIDs []int64 // permission list
white bool // no need to login
middlewares []func(gp *Goploy) error // Middlewares run before callback, trigger error will end the request
callback func(gp *Goploy) Response // API function
logFunc func(gp *Goploy, resp Response)
}
type RouteHandler interface {
Handler() []Route
}
func NewRoute(pattern, method string, callback func(gp *Goploy) Response) Route {
return newRoute(pattern, method, callback)
}
func NewWhiteRoute(pattern, method string, callback func(gp *Goploy) Response) Route {
route := newRoute(pattern, method, callback)
route.white = true
return route
}
func newRoute(pattern, method string, callback func(gp *Goploy) Response) Route {
return Route{
pattern: pattern,
method: method,
callback: callback,
}
}
func (r Route) Permissions(permissionIDs ...int64) Route {
for _, permissionID := range permissionIDs {
r.permissionIDs = append(r.permissionIDs, permissionID)
}
return r
}
// Middleware global Middleware handle function
func (r Route) Middleware(middleware func(gp *Goploy) error) Route {
r.middlewares = append(r.middlewares, middleware)
return r
}
// LogFunc callback finished
func (r Route) LogFunc(f func(gp *Goploy, resp Response)) Route {
r.logFunc = f
return r
}
func (r Route) hasPermission(permissionIDs map[int64]struct{}) error {
if len(r.permissionIDs) == 0 {
return nil
}
for _, permissionID := range r.permissionIDs {
if _, ok := permissionIDs[permissionID]; ok {
return nil
}
}
return errors.New("no permission")
}

View File

@ -2,148 +2,62 @@
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package core
package server
import (
"database/sql"
"errors"
"fmt"
"github.com/golang-jwt/jwt"
"github.com/vearutop/statigz"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/internal/pkg"
"github.com/zhenorzz/goploy/internal/server/response"
"github.com/zhenorzz/goploy/model"
"github.com/zhenorzz/goploy/response"
"github.com/zhenorzz/goploy/web"
"io/fs"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type Goploy struct {
UserInfo model.User
Namespace struct {
ID int64
PermissionIDs map[int64]struct{}
}
Request *http.Request
ResponseWriter http.ResponseWriter
URLQuery url.Values
Body []byte
}
type RouteApi interface {
Routes() []Route
}
type Response interface {
Write(http.ResponseWriter, *http.Request) error
}
type Route struct {
pattern string
method string // Method specifies the HTTP method (GET, POST, PUT, etc.).
permissionIDs []int64 // permission list
white bool // no need to login
middlewares []func(gp *Goploy) error // Middlewares run before callback, trigger error will end the request
callback func(gp *Goploy) Response // Controller function
logFunc func(gp *Goploy, resp Response)
}
// Router is Route slice and global middlewares
type Router struct {
routes map[string]Route
middlewares *[]func(gp *Goploy) error // Middlewares run before all Route
}
func NewRouter() Router {
return Router{
func NewRouter() *Router {
router := Router{
routes: map[string]Route{},
middlewares: new([]func(gp *Goploy) error),
}
return &router
}
func NewRoute(pattern, method string, callback func(gp *Goploy) Response) Route {
return newRoute(pattern, method, callback)
}
func NewWhiteRoute(pattern, method string, callback func(gp *Goploy) Response) Route {
route := newRoute(pattern, method, callback)
route.white = true
return route
}
func newRoute(pattern, method string, callback func(gp *Goploy) Response) Route {
return Route{
pattern: pattern,
method: method,
callback: callback,
}
}
// Start a router
func (rt Router) Start() {
if config.Toml.Env == "production" {
subFS, err := fs.Sub(web.Dist, "dist")
if err != nil {
log.Fatal(err)
}
http.Handle("/assets/", statigz.FileServer(subFS.(fs.ReadDirFS)))
http.Handle("/favicon.ico", statigz.FileServer(subFS.(fs.ReadDirFS)))
}
http.Handle("/", rt)
}
// Middleware global Middleware handle function
func (rt Router) Middleware(middleware func(gp *Goploy) error) {
func (rt *Router) Middleware(middleware func(gp *Goploy) error) {
*rt.middlewares = append(*rt.middlewares, middleware)
}
// Add pattern path
// callback where path should be handled
func (rt Router) Add(ra RouteApi) Router {
for _, r := range ra.Routes() {
func (rt *Router) Register(ra RouteHandler) {
for _, r := range ra.Handler() {
rt.routes[r.pattern] = r
}
return rt
}
func (r Route) Permissions(permissionIDs ...int64) Route {
for _, permissionID := range permissionIDs {
r.permissionIDs = append(r.permissionIDs, permissionID)
}
return r
}
// Middleware global Middleware handle function
func (r Route) Middleware(middleware func(gp *Goploy) error) Route {
r.middlewares = append(r.middlewares, middleware)
return r
}
// LogFunc callback finished
func (r Route) LogFunc(f func(gp *Goploy, resp Response)) Route {
r.logFunc = f
return r
}
func (rt Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// If in production env, serve file in go server,
// else serve file in npm
if config.Toml.Env == "production" {
if "/" == r.URL.Path {
r, err := web.Dist.Open("dist/index.html")
index, err := web.Dist.Open("dist/index.html")
if err != nil {
log.Fatal(err)
}
defer r.Close()
contents, err := ioutil.ReadAll(r)
defer index.Close()
contents, err := ioutil.ReadAll(index)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, string(contents))
return
@ -157,7 +71,7 @@ func (rt Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
func (rt Router) doRequest(w http.ResponseWriter, r *http.Request) (*Goploy, Response) {
func (rt *Router) doRequest(w http.ResponseWriter, r *http.Request) (*Goploy, Response) {
gp := new(Goploy)
route, ok := rt.routes[r.URL.Path]
if !ok {
@ -190,9 +104,9 @@ func (rt Router) doRequest(w http.ResponseWriter, r *http.Request) (*Goploy, Res
return gp, response.JSON{Code: response.LoginExpired, Message: "Login expired"}
}
namespaceIDRaw := r.Header.Get(NamespaceHeaderName)
namespaceIDRaw := r.Header.Get(config.NamespaceHeaderName)
if namespaceIDRaw == "" {
namespaceIDRaw = r.URL.Query().Get(NamespaceHeaderName)
namespaceIDRaw = r.URL.Query().Get(config.NamespaceHeaderName)
}
namespaceID, err := strconv.ParseInt(namespaceIDRaw, 10, 64)
@ -276,20 +190,6 @@ func (rt Router) doRequest(w http.ResponseWriter, r *http.Request) (*Goploy, Res
return gp, resp
}
func (r Route) hasPermission(permissionIDs map[int64]struct{}) error {
if len(r.permissionIDs) == 0 {
return nil
}
for _, permissionID := range r.permissionIDs {
if _, ok := permissionIDs[permissionID]; ok {
return nil
}
}
return errors.New("no permission")
}
func hasContentType(r *http.Request, mimetype string) bool {
contentType := r.Header.Get("Content-type")
if contentType == "" {

52
internal/server/server.go Normal file
View File

@ -0,0 +1,52 @@
// Copyright 2022 The Goploy Authors. All rights reserved.
// Use of this source code is governed by a GPLv3-style
// license that can be found in the LICENSE file.
package server
import (
"github.com/vearutop/statigz"
"github.com/zhenorzz/goploy/config"
"github.com/zhenorzz/goploy/model"
"github.com/zhenorzz/goploy/web"
"io/fs"
"log"
"net/http"
"net/url"
)
type Goploy struct {
UserInfo model.User
Namespace struct {
ID int64
PermissionIDs map[int64]struct{}
}
Request *http.Request
ResponseWriter http.ResponseWriter
URLQuery url.Values
Body []byte
}
type Response interface {
Write(http.ResponseWriter, *http.Request) error
}
type Server struct {
http.Server
*Router
}
func (srv *Server) Spin() {
if config.Toml.Env == "production" {
subFS, err := fs.Sub(web.Dist, "dist")
if err != nil {
log.Fatal(err)
}
http.Handle("/assets/", statigz.FileServer(subFS.(fs.ReadDirFS)))
http.Handle("/favicon.ico", statigz.FileServer(subFS.(fs.ReadDirFS)))
}
http.Handle("/", srv.Router)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("ListenAndServe: ", err.Error())
}
}