|
|
|
@ -1,493 +1,20 @@ |
|
|
|
|
package main |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"bytes" |
|
|
|
|
"database/sql" |
|
|
|
|
"database/sql/driver" |
|
|
|
|
"encoding/gob" |
|
|
|
|
"encoding/json" |
|
|
|
|
"errors" |
|
|
|
|
"gopkg.in/yaml.v3" |
|
|
|
|
"devops/build" |
|
|
|
|
"devops/entities" |
|
|
|
|
"devops/routes" |
|
|
|
|
"html/template" |
|
|
|
|
"io" |
|
|
|
|
"log" |
|
|
|
|
"net/http" |
|
|
|
|
"os" |
|
|
|
|
"os/exec" |
|
|
|
|
"sync" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"github.com/gorilla/sessions" |
|
|
|
|
"github.com/gorilla/websocket" |
|
|
|
|
"github.com/wader/gormstore/v2" |
|
|
|
|
"gorm.io/driver/sqlite" |
|
|
|
|
"gorm.io/gorm" |
|
|
|
|
"zestack.dev/misc" |
|
|
|
|
"gorm.io/gorm/schema" |
|
|
|
|
"zestack.dev/env" |
|
|
|
|
"zestack.dev/slim" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
// Time allowed to write a message to the peer.
|
|
|
|
|
writeWait = 10 * time.Second |
|
|
|
|
|
|
|
|
|
// 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 = 512 |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
var ( |
|
|
|
|
newline = []byte{'\n'} |
|
|
|
|
space = []byte{' '} |
|
|
|
|
|
|
|
|
|
upgrader *websocket.Upgrader |
|
|
|
|
clients *sync.Map |
|
|
|
|
broadcast chan []byte |
|
|
|
|
|
|
|
|
|
store sessions.Store |
|
|
|
|
db *gorm.DB |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
type NullString sql.NullString |
|
|
|
|
|
|
|
|
|
// Scan implements the [Scanner] interface.
|
|
|
|
|
func (ns *NullString) Scan(value any) error { |
|
|
|
|
ss := new(sql.NullString) |
|
|
|
|
err := ss.Scan(value) |
|
|
|
|
ns.String = ss.String |
|
|
|
|
ns.Valid = ss.Valid |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Value implements the [driver.Valuer] interface.
|
|
|
|
|
func (ns NullString) Value() (driver.Value, error) { |
|
|
|
|
if !ns.Valid { |
|
|
|
|
return nil, nil |
|
|
|
|
} |
|
|
|
|
return ns.String, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (ns NullString) MarshalJSON() ([]byte, error) { |
|
|
|
|
if ns.Valid { |
|
|
|
|
return json.Marshal(ns.String) |
|
|
|
|
} |
|
|
|
|
return json.Marshal(nil) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (ns *NullString) UnmarshalJSON(b []byte) error { |
|
|
|
|
if string(b) == "null" { |
|
|
|
|
ns.Valid = false |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
err := json.Unmarshal(b, &ns.String) |
|
|
|
|
if err == nil { |
|
|
|
|
ns.Valid = true |
|
|
|
|
} |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type NullTime sql.NullTime |
|
|
|
|
|
|
|
|
|
func (nt *NullTime) Scan(value any) error { |
|
|
|
|
st := new(sql.NullTime) |
|
|
|
|
err := st.Scan(value) |
|
|
|
|
nt.Time = st.Time |
|
|
|
|
nt.Valid = st.Valid |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (nt NullTime) Value() (driver.Value, error) { |
|
|
|
|
if !nt.Valid { |
|
|
|
|
return nil, nil |
|
|
|
|
} |
|
|
|
|
return nt.Time, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (nt NullTime) MarshalJSON() ([]byte, error) { |
|
|
|
|
if nt.Valid { |
|
|
|
|
return nt.Time.MarshalJSON() |
|
|
|
|
} |
|
|
|
|
return json.Marshal(nil) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (nt *NullTime) UnmarshalJSON(b []byte) error { |
|
|
|
|
if string(b) == "null" { |
|
|
|
|
nt.Valid = false |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
err := json.Unmarshal(b, &nt.Time) |
|
|
|
|
if err == nil { |
|
|
|
|
nt.Valid = true |
|
|
|
|
} |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type NullUint struct { |
|
|
|
|
V uint |
|
|
|
|
Valid bool |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (nu *NullUint) Scan(value any) error { |
|
|
|
|
sn := new(sql.Null[uint]) |
|
|
|
|
err := sn.Scan(value) |
|
|
|
|
nu.V = sn.V |
|
|
|
|
nu.Valid = sn.Valid |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (nu NullUint) Value() (driver.Value, error) { |
|
|
|
|
if !nu.Valid { |
|
|
|
|
return nil, nil |
|
|
|
|
} |
|
|
|
|
// 注意:driver.Value 不支持 uint
|
|
|
|
|
return int64(nu.V), nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (nu NullUint) MarshalJSON() ([]byte, error) { |
|
|
|
|
if nu.Valid { |
|
|
|
|
return json.Marshal(nu.V) |
|
|
|
|
} |
|
|
|
|
return json.Marshal(nil) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (nu *NullUint) UnmarshalJSON(b []byte) error { |
|
|
|
|
if string(b) == "null" { |
|
|
|
|
nu.Valid = false |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
err := json.Unmarshal(b, &nu.V) |
|
|
|
|
if err == nil { |
|
|
|
|
nu.Valid = true |
|
|
|
|
} |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// User 用户
|
|
|
|
|
type User struct { |
|
|
|
|
ID uint `json:"id" gorm:"primaryKey"` |
|
|
|
|
Username string `json:"username" gorm:"unique"` |
|
|
|
|
RawPassword string `json:"-" gorm:"-"` // 原始密码
|
|
|
|
|
Password string `json:"password"` |
|
|
|
|
Disabled bool `json:"disabled" gorm:"default:false"` |
|
|
|
|
CreatedAt time.Time `json:"createdAt"` |
|
|
|
|
UpdatedAt time.Time `json:"updatedAt"` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (u *User) BeforeCreate(_ *gorm.DB) error { |
|
|
|
|
if u.RawPassword != "" { |
|
|
|
|
hash, err := misc.PasswordHash(u.RawPassword) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
u.Password = hash |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
return errors.New("缺少密码") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Project 项目
|
|
|
|
|
type Project struct { |
|
|
|
|
// 项目编号,推荐使用 ObjectId 算法生成,也可以使用 UUID/NanoID 等。
|
|
|
|
|
ID string `json:"id" gorm:"primaryKey"` |
|
|
|
|
// 项目名称
|
|
|
|
|
Name string `json:"name" gorm:"unique"` |
|
|
|
|
// 项目介绍
|
|
|
|
|
Intro string `json:"intro"` |
|
|
|
|
// 项目网站
|
|
|
|
|
Website string `json:"website"` |
|
|
|
|
// 项目的仓库地址,比如 GitHub、Gitee、Gitlab、Gitea 或自建的仓库,
|
|
|
|
|
// 如果是私有仓库,应该携带授权信息(建议使用私有令牌而不是账号密码)。
|
|
|
|
|
RepositoryURL string `json:"repositoryURL"` |
|
|
|
|
// 用于构建的仓库分支,若置空则使用主分支
|
|
|
|
|
Branch string `json:"branch"` |
|
|
|
|
// 创建项目的用户编号
|
|
|
|
|
CreatedBy uint `json:"createdBy"` |
|
|
|
|
// 创建项目时的时间
|
|
|
|
|
CreatedAt time.Time `json:"createdAt"` |
|
|
|
|
// 上一次修改项目信息的用户编号
|
|
|
|
|
UpdatedBy NullUint `json:"updatedBy"` |
|
|
|
|
// 上一次修改项目信息的时间
|
|
|
|
|
UpdatedAt NullTime `json:"updatedAt" gorm:"autoUpdateTime:false"` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// BuildStatus 构建状态
|
|
|
|
|
type BuildStatus string |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
BuildRunning BuildStatus = "running" // 正在运行
|
|
|
|
|
BuildSuccess BuildStatus = "success" // 构建成功
|
|
|
|
|
BuildFailure BuildStatus = "failure" // 构建失败
|
|
|
|
|
BuildAborted BuildStatus = "aborted" // 构建中断
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// Build 项目构建
|
|
|
|
|
type Build struct { |
|
|
|
|
// 构建编号
|
|
|
|
|
ID string `json:"id" gorm:"primaryKey"` |
|
|
|
|
// 构建项目
|
|
|
|
|
ProjectID string `json:"projectId"` |
|
|
|
|
// 关联项目,如果项目删除也删除
|
|
|
|
|
Project *Project `json:"project,omitempty" gorm:"foreignKey:ProjectID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` |
|
|
|
|
// 构建时,项目最后一次提交的 commit 标识
|
|
|
|
|
CommitId string `json:"commitId"` |
|
|
|
|
// 构建时,项目最后一次提交的 commit 信息
|
|
|
|
|
CommitText string `json:"commitText"` |
|
|
|
|
// 非构建步骤失败时的错误信息
|
|
|
|
|
Error NullString `json:"startedError"` |
|
|
|
|
// 启动构建程序的用户的编号,如果是通过 webhook 修改,会被置空
|
|
|
|
|
StartedBy NullUint `json:"startedBy"` |
|
|
|
|
// 启动构建项目时的时间
|
|
|
|
|
StartedAt time.Time `json:"startedAt"` |
|
|
|
|
// 手动停止或自动完成该项目构建时的时间
|
|
|
|
|
StoppedAt NullTime `json:"stoppedAt"` |
|
|
|
|
// 手动停止该项目构建的用户的编号,自动完成会将该值置空。
|
|
|
|
|
StoppedBy NullUint `json:"stoppedBy"` |
|
|
|
|
// 当前所处状态
|
|
|
|
|
Status BuildStatus `json:"status"` |
|
|
|
|
|
|
|
|
|
// 步骤日志
|
|
|
|
|
Logs []BuildLog `json:"logs"` |
|
|
|
|
|
|
|
|
|
// 创建构建步骤的用户编号,如果是通过 webhook 修改,会被置空
|
|
|
|
|
CreatedBy NullUint `json:"createdBy"` |
|
|
|
|
// 创建构建步骤时的时间
|
|
|
|
|
CreatedAt time.Time `json:"createdAt"` |
|
|
|
|
// 上一次修改构建步骤信息的用户编号,如果是通过 webhook 修改,会被置空
|
|
|
|
|
UpdatedBy NullUint `json:"updatedBy"` |
|
|
|
|
// 上一次修改构建步骤信息的时间
|
|
|
|
|
UpdatedAt NullTime `json:"updatedAt" gorm:"autoUpdateTime:false"` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// BuildLog 构建步骤日志
|
|
|
|
|
type BuildLog struct { |
|
|
|
|
// 日志编号
|
|
|
|
|
Id uint `json:"id" gorm:"primaryKey"` |
|
|
|
|
// 归属的构建编号
|
|
|
|
|
BuildID string `json:"buildId"` |
|
|
|
|
// 关联项目,如果项目删除也删除
|
|
|
|
|
Build *Build `json:"build,omitempty" gorm:"foreignKey:BuildID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` |
|
|
|
|
// 构建步骤名称
|
|
|
|
|
Name string `json:"name"` |
|
|
|
|
// 该构建步骤使用的程序
|
|
|
|
|
Uses string `json:"uses"` |
|
|
|
|
// 该构建步骤时传递给程序的参数
|
|
|
|
|
With map[string]string `json:"with" gorm:"serializer:json"` |
|
|
|
|
// 程序的运行日志
|
|
|
|
|
Logs []string `json:"logs" gorm:"serializer:json"` |
|
|
|
|
// 当前所处状态
|
|
|
|
|
Status BuildStatus `json:"status"` |
|
|
|
|
// 开始该步骤时的时间
|
|
|
|
|
StartedAt time.Time `json:"startedAt"` |
|
|
|
|
// 结束该步骤时的时间
|
|
|
|
|
StoppedAt time.Time `json:"stoppedAt"` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type BuildConfig struct { |
|
|
|
|
Name string `json:"name" yaml:"name"` |
|
|
|
|
VCS VCSConfig `json:"vcs" yaml:"vcs"` |
|
|
|
|
Steps []StepConfig `json:"steps" yaml:"steps"` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type VCSConfig struct { |
|
|
|
|
Branch string `json:"branch" yaml:"branch"` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type StepConfig struct { |
|
|
|
|
Name string `json:"name" yaml:"name"` |
|
|
|
|
Uses string `json:"uses" yaml:"uses"` |
|
|
|
|
With map[string]string `json:"with" yaml:"with"` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func Fail(c slim.Context, msg string, code ...int) error { |
|
|
|
|
statusCode := http.StatusBadRequest |
|
|
|
|
if len(code) > 0 { |
|
|
|
|
statusCode = code[0] |
|
|
|
|
} |
|
|
|
|
return c.JSON(statusCode, slim.Map{ |
|
|
|
|
"ok": false, |
|
|
|
|
"msg": msg, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func AuthSession(c slim.Context, next slim.HandlerFunc) error { |
|
|
|
|
sess, err := store.Get(c.Request(), "session-key") |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
user, ok := sess.Values["user"].(*User) |
|
|
|
|
if !ok { |
|
|
|
|
return Fail(c, "请登录", http.StatusUnauthorized) |
|
|
|
|
} |
|
|
|
|
c.Set("user", user) |
|
|
|
|
return next(c) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleLogin(c slim.Context) error { |
|
|
|
|
var req struct { |
|
|
|
|
Username string `json:"username" form:"username"` |
|
|
|
|
Password string `json:"password" form:"password"` |
|
|
|
|
} |
|
|
|
|
if err := c.Bind(&req); err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
if req.Username == "" { |
|
|
|
|
return c.Redirect(301, "/login?error=用户名不能为空") |
|
|
|
|
} |
|
|
|
|
if req.Password == "" { |
|
|
|
|
return c.Redirect(301, "/login?error=登录密码不能为空") |
|
|
|
|
} |
|
|
|
|
var user User |
|
|
|
|
err := db.Model(&User{}).Where("username", req.Username).First(&user).Error |
|
|
|
|
if err != nil { |
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) { |
|
|
|
|
return c.Redirect(301, "/login?error=用户名或密码错误") |
|
|
|
|
} |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
if !misc.PasswordVerify(req.Password, user.Password) { |
|
|
|
|
return c.Redirect(301, "/login?error=用户名或密码错误") |
|
|
|
|
} |
|
|
|
|
sess, err := store.Get(c.Request(), "session-key") |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
sess.Values["user"] = &user |
|
|
|
|
err = sess.Save(c.Request(), c.Response()) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
return c.Redirect(http.StatusMovedPermanently, "/") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleLogout(c slim.Context) error { |
|
|
|
|
// Get a session. We're ignoring the error resulted from decoding an
|
|
|
|
|
// existing session: Get() always returns a session, even if empty.
|
|
|
|
|
session, _ := store.Get(c.Request(), "session-name") |
|
|
|
|
// Set some session values.
|
|
|
|
|
session.Values["foo"] = nil |
|
|
|
|
// Save it before we write to the response/return from the handler.
|
|
|
|
|
err := session.Save(c.Request(), c.Response()) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
return c.Redirect(http.StatusMovedPermanently, "/") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleEcho(c slim.Context) error { |
|
|
|
|
conn, err := upgrader.Upgrade(c.Response(), c.Request(), nil) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
clients.Store(conn, struct{}{}) |
|
|
|
|
defer clients.Delete(conn) |
|
|
|
|
|
|
|
|
|
// TODO 使用 pool 实现复用
|
|
|
|
|
exit := make(chan error) |
|
|
|
|
defer close(exit) |
|
|
|
|
|
|
|
|
|
stop := func(err error) { |
|
|
|
|
select { |
|
|
|
|
case <-exit: |
|
|
|
|
default: |
|
|
|
|
exit <- err |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
go func() { |
|
|
|
|
conn.SetReadLimit(maxMessageSize) |
|
|
|
|
conn.SetReadDeadline(time.Now().Add(pongWait)) |
|
|
|
|
conn.SetPongHandler(func(string) error { |
|
|
|
|
conn.SetReadDeadline(time.Now().Add(pongWait)) |
|
|
|
|
return nil |
|
|
|
|
}) |
|
|
|
|
for { |
|
|
|
|
_, message, ex := conn.ReadMessage() |
|
|
|
|
if ex != nil { |
|
|
|
|
if websocket.IsUnexpectedCloseError(ex, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { |
|
|
|
|
stop(ex) |
|
|
|
|
} else { |
|
|
|
|
stop(nil) |
|
|
|
|
} |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) |
|
|
|
|
broadcast <- message |
|
|
|
|
} |
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
go func() { |
|
|
|
|
ticker := time.NewTicker(pingPeriod) |
|
|
|
|
defer func() { |
|
|
|
|
ticker.Stop() |
|
|
|
|
conn.Close() |
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
for { |
|
|
|
|
for { |
|
|
|
|
select { |
|
|
|
|
case message, ok := <-broadcast: |
|
|
|
|
conn.SetWriteDeadline(time.Now().Add(writeWait)) |
|
|
|
|
if !ok { |
|
|
|
|
// The hub closed the channel.
|
|
|
|
|
conn.WriteMessage(websocket.CloseMessage, []byte{}) |
|
|
|
|
stop(nil) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
w, err2 := conn.NextWriter(websocket.TextMessage) |
|
|
|
|
if err2 != nil { |
|
|
|
|
stop(err2) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
w.Write(message) |
|
|
|
|
|
|
|
|
|
// Add queued chat messages to the current websocket message.
|
|
|
|
|
n := len(broadcast) |
|
|
|
|
for i := 0; i < n; i++ { |
|
|
|
|
w.Write(newline) |
|
|
|
|
w.Write(<-broadcast) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if err3 := w.Close(); err3 != nil { |
|
|
|
|
stop(err3) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
case <-ticker.C: |
|
|
|
|
conn.SetWriteDeadline(time.Now().Add(writeWait)) |
|
|
|
|
if err4 := conn.WriteMessage(websocket.PingMessage, nil); err4 != nil { |
|
|
|
|
stop(err4) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
return <-exit |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleStart(c slim.Context) error { |
|
|
|
|
cmd := exec.Command("go") |
|
|
|
|
cmd.Stdout = c.Response() |
|
|
|
|
cmd.Stderr = c.Response() |
|
|
|
|
cmd.Start() |
|
|
|
|
cmd.Wait() |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleAbort(c slim.Context) error { |
|
|
|
|
return c.String(200, "ok") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleHome(c slim.Context) error { |
|
|
|
|
return c.Render(http.StatusOK, "index.gohtml", "ws://"+c.Request().Host+"/echo") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type Template struct { |
|
|
|
|
templates *template.Template |
|
|
|
|
} |
|
|
|
@ -498,71 +25,48 @@ func (t *Template) Render(c slim.Context, w io.Writer, name string, data any) er |
|
|
|
|
|
|
|
|
|
func main() { |
|
|
|
|
var err error |
|
|
|
|
db, err = gorm.Open(sqlite.Open("devops.db"), &gorm.Config{}) |
|
|
|
|
if err != nil { |
|
|
|
|
var db *gorm.DB |
|
|
|
|
|
|
|
|
|
// 初始化环境变量
|
|
|
|
|
if err = env.Init(); err != nil { |
|
|
|
|
log.Fatalln(err) |
|
|
|
|
} |
|
|
|
|
err = db.AutoMigrate( |
|
|
|
|
&User{}, |
|
|
|
|
&Project{}, |
|
|
|
|
&Build{}, |
|
|
|
|
&BuildLog{}, |
|
|
|
|
) |
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
|
|
// 初始化数据库
|
|
|
|
|
if db, err = gorm.Open(sqlite.Open(env.String("DB_DSN", "devops.db")), &gorm.Config{ |
|
|
|
|
NamingStrategy: schema.NamingStrategy{ |
|
|
|
|
TablePrefix: env.String("DB_TABLE_PREFIX", "do_"), |
|
|
|
|
}, |
|
|
|
|
TranslateError: env.Bool("DB_TRANSLATE_ERROR", true), |
|
|
|
|
}); err != nil { |
|
|
|
|
log.Fatalln(err) |
|
|
|
|
} |
|
|
|
|
err = db.Model(&User{}).FirstOrCreate(&User{ |
|
|
|
|
ID: 1, |
|
|
|
|
Username: "admin", |
|
|
|
|
RawPassword: "111111", |
|
|
|
|
}, User{ID: 1}).Error |
|
|
|
|
if err != nil { |
|
|
|
|
} else if err = entities.Init(db); err != nil { |
|
|
|
|
log.Fatalln(err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
upgrader = &websocket.Upgrader{} |
|
|
|
|
clients = new(sync.Map) |
|
|
|
|
broadcast = make(chan []byte) |
|
|
|
|
store = gormstore.New(db, []byte("secret")) |
|
|
|
|
|
|
|
|
|
gob.Register(&User{}) |
|
|
|
|
// 启动构建程序
|
|
|
|
|
if err = build.Init(db); err != nil { |
|
|
|
|
log.Fatalln(err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 初始化 http
|
|
|
|
|
s := slim.New() |
|
|
|
|
s.Debug = true |
|
|
|
|
s.Renderer = &Template{templates: template.Must(template.ParseGlob("views/*.gohtml"))} |
|
|
|
|
s.ResetRouterCreator(func(s *slim.Slim) slim.Router { |
|
|
|
|
return slim.NewRouter(slim.RouterConfig{ |
|
|
|
|
RoutingTrailingSlash: true, |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
s.Use(slim.Logging()) |
|
|
|
|
s.Use(slim.Recovery()) |
|
|
|
|
s.Use(slim.Static("public")) |
|
|
|
|
|
|
|
|
|
s.File("/login", "login.html") |
|
|
|
|
s.POST("/login", handleLogin) |
|
|
|
|
s.GET("/logout", handleLogout) |
|
|
|
|
s.GET("/echo", handleEcho) |
|
|
|
|
s.GET("/", handleHome) |
|
|
|
|
s.Route("/p/", func(sub slim.RouteCollector) { |
|
|
|
|
sub.Use(AuthSession) |
|
|
|
|
sub.POST(":id/start", handleStart) |
|
|
|
|
sub.POST(":id/abort", handleAbort) |
|
|
|
|
sub.GET("*", handleStart) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
s.GET("/~", func(c slim.Context) error { |
|
|
|
|
file, err := os.ReadFile("deploy.yml") |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var config BuildConfig |
|
|
|
|
err = yaml.Unmarshal(file, &config) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
// 注册路由
|
|
|
|
|
if err = routes.Init(db, s); err != nil { |
|
|
|
|
log.Fatalln(err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return c.JSONPretty(200, config, " ") |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
//http.HandleFunc("/echo", handleWebsocket)
|
|
|
|
|
//http.HandleFunc("/restart", restart)
|
|
|
|
|
//http.ListenAndServe(":5000", nil)
|
|
|
|
|
// 启动网络服务
|
|
|
|
|
log.Fatalln(s.Start(":5000")) |
|
|
|
|
} |
|
|
|
|