文章目录
- 1、简介
- 1.1 目录结构
- 1.2 开发模式
- 2、数据库设计
- 2.1 user表
- 2.2 space_template和space_kind表
- 2.3 space和spacespec表
- 3、gRPC客户端
- 4、数据库访问
- 4.1 mysql
- 4.2 redis
- 5、缓存加载
- 5.1 通用缓存
- 5.2 数据加载
- 6、功能开发
- 6.1 用户登录
- 6.2 获取所有模板
- 6.3 创建工作空间
- 6.4 创建工作空间并启动
- 6.5 停止工作空间
- 6.6 删除工作空间
- 6.7 列出工作空间
- 7、功能测试
- 7.1 创建工作空间
- 7.2 停止工作空间
k8s编程operator系列:
k8s编程operator——(1) client-go基础部分
k8s编程operator——(2) client-go中的informer
k8s编程operator——(3) 自定义资源CRD
k8s编程operator——(4) kubebuilder & controller-runtime
k8s编程operator实战之云编码平台——①架构设计
k8s编程operator实战之云编码平台——②controller初步实现
k8s编程operator实战之云编码平台——③Code-Server Pod访问实现
k8s编程operator实战之云编码平台——④web后端实现
在前两章中分别实现了k8s controller和后端pod的访问。通过controller可以实现code-server容器的创建、删除以及状态维护等,通过openresty可以实现后端pod的动态反向代理。
接下来将会实现web后端,使用的web框架为Gin
;mysql驱动为sqlx
;redis驱动为go-redis
;以及grpc
。
项目Github地址:https://github.com/mangohow/cloud-ide-webserver
1、简介
1.1 目录结构
开发时按照下面的路径来组织代码:
- cmd:入口文件的目录
- conf:配置文件和配置文件加载的代码路径
- internal:存放项目相关的核心代码
- pkg:存放一些工具代码
- routes:路由注册相关代码
目录结构:
我在github上有一套gin框架的模板,这个模板是根据自己喜好来进行代码组织的,上面的目录结构就来自gin-template
github地址:https://github.com/mangohow/gin-template
1.2 开发模式
开发模式就采用常用的mvc模式,采用前后端分离的方式。代码分为三层:
controller:
用户请求数据的接收,调用service层处理,响应数据service:
主要的业务处理逻辑,调用dao层来查询数据dao:
数据库访问层
其它代码目录:
model:
用来存放数据库、请求、响应等数据的结构体middleware:
存放中间件代码
2、数据库设计
数据库采用mysql,目前主要的表有5个:user、space_template、space_kind、space_spec、space
user:
用户表,保存用户的基本信息space_template:
工作空间模板表,保存工作空间的信息,比如工作空间的镜像、描述等space_kind:
工作空间的类别,将工作空间分为不同的类别,比如常用模板、框架模板等等spacespec:
工作空间的规格,也就是可以使用的资源,CPU核心数、内存大小、存储大小space:
用户创建的工作空间
2.1 user表
user表如下所示:
uid将会采用mongodb的_id生成方式来生成,长度固定为24
2.2 space_template和space_kind表
space_template是工作空间的模板,用户可以根据模板来创建工作空间:
image为要容器镜像的名称
space_kind表很简单,就是对工作空间进行分类:
2.3 space和spacespec表
space是用户根据模板创建的工作空间:
user_id为所属的用户、tmpl_id为根据哪个模板创建的、spec_id为工作空间的规格id
sid为工作空间的space id,当工作空间被创建时生成,在访问工作空间时会存在于路径中
spacespec为工作空间的规格,cpu_spec和mem_spec用于在创建pod时指定的resourceLimit
3、gRPC客户端
之前,我们在k8s controller中通过gRPC实现了工作空间的pod的创建、删除等服务,接下来要在web中添加gRPC的客户端来调用服务。
1、创建pb/proto目录,将之前的service.proto文件拷贝过去,然后编译:
protoc --go_out=plugins=grpc:./pb ./pb/proto/*.proto
2、在internal中创建rpc/clinet.go文件,封装一个用来获取grpc client的接口:
package rpc
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"sync"
"time"
)
var (
clients = map[string]*grpc.ClientConn{}
lock sync.Mutex
)
func GrpcClient(name string) *grpc.ClientConn {
lock.Lock()
defer lock.Unlock()
if c, ok := clients[name]; ok {
return c
}
conn := newClient()
clients[name] = conn
return conn
}
func newClient() *grpc.ClientConn {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// 开发阶段,先使用不安全的传输
conn, err := grpc.DialContext(ctx, "192.168.44.100:6387", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
return conn
}
4、数据库访问
4.1 mysql
mysql的驱动在这里就使用sqlx,比起gorm这些框架,我还是更喜欢简单一点的sqlx。
mysql的初始化:
dao/db/init.go
package db
import (
"context"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/mangohow/cloud-ide-webserver/conf"
"time"
)
var sqlDb *sqlx.DB
func InitMysql() error {
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
var err error
db, err := sqlx.ConnectContext(timeoutCtx, "mysql", conf.MysqlConfig.DataSourceName)
if err != nil {
return err
}
db.SetMaxOpenConns(int(conf.MysqlConfig.MaxOpenConns))
db.SetMaxIdleConns(int(conf.MysqlConfig.MaxIdleConns))
sqlDb = db
return nil
}
func CloseMysql() {
sqlDb.Close()
}
func DB() *sqlx.DB {
return sqlDb
}
dao层主要封装mysql的CRUD,目前主要有三个dao:
userdao:
用来对用户进行CRUD
spacetmpldao:
用来对space_template、space_kind和spacespec进行CRUD
spacedao:
用来对space进行CRUD
4.2 redis
redis采用go-redis这个库:github.com/go-redis/redis/v8
redis初始化:
dao/rdis/init.go
package rdis
import (
"context"
"github.com/go-redis/redis/v8"
"github.com/mangohow/cloud-ide-webserver/conf"
"time"
)
var client *redis.Client
func InitRedis() error {
client = redis.NewClient(&redis.Options{
Addr: conf.RedisConfig.Addr,
Password: conf.RedisConfig.Password,
DB: int(conf.RedisConfig.DB),
PoolSize: int(conf.RedisConfig.PoolSize), // 连接池最大socket连接数
MinIdleConns: int(conf.RedisConfig.MinIdleConns), // 最少连接维持数
})
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := client.Ping(timeoutCtx).Result()
if err != nil {
return err
}
return nil
}
func CloseRedisConn() {
client.Close()
}
目录结构:
5、缓存加载
由于space_tmplate和spacespec数据库的数据访问比较频繁,而且数据量又很小、数据修改非常不频繁,因此可以在程序启动时先将这部分数据加载到我们的程序中,就使用map来实现一个简单的缓存。
首先实现一个通用的缓存,放在pkg/cache目录下:
5.1 通用缓存
pkg/cache/cache.go
package cache
import (
"strconv"
"sync"
)
var caches = map[string]*Cache{}
var lock = sync.Mutex{}
func New(name string) *Cache {
lock.Lock()
defer lock.Unlock()
if c, ok := caches[name]; ok {
return c
}
c := &Cache{items: make(map[string]interface{})}
caches[name] = c
return c
}
type Cache struct {
lock sync.RWMutex
items map[string]interface{}
}
func (c *Cache) Set(key string, val interface{}) {
c.lock.Lock()
defer c.lock.Unlock()
c.items[key] = val
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.lock.RLock()
defer c.lock.RUnlock()
item, ok := c.items[key]
return item, ok
}
func (c *Cache) GetByInt(key int) (interface{}, bool) {
return c.Get(strconv.Itoa(key))
}
func (c *Cache) GetAll() []interface{} {
c.lock.RLock()
c.lock.RUnlock()
ret := make([]interface{}, 0, len(c.items))
for _, v := range c.items {
ret = append(ret, v)
}
return ret
}
func (c *Cache) Replace(items map[string]interface{}) {
c.lock.Lock()
defer c.lock.Unlock()
c.items = items
}
func (c *Cache) Clear() {
c.lock.Lock()
defer c.lock.Unlock()
c.items = make(map[string]interface{})
}
5.2 数据加载
接下来实现具体的数据库数据的加载
space_template数据库数据加载
internal/caches/tmplcache.go
package caches
import (
"github.com/mangohow/cloud-ide-webserver/internal/dao"
"github.com/mangohow/cloud-ide-webserver/internal/model"
"github.com/mangohow/cloud-ide-webserver/pkg/cache"
"strconv"
)
// 加载mysql中的SpaceTemplate到内存中,数据量不大
type TmplCache struct {
cache *cache.Cache
dao *dao.SpaceTemplateDao
}
func newTmplCache(dao *dao.SpaceTemplateDao) *TmplCache {
return &TmplCache{
cache: cache.New("spaceTmpl"),
dao: dao,
}
}
func (t *TmplCache) LoadCache() {
tmpls, err := t.dao.GetAllUsingTmpl()
if err != nil {
panic(err)
}
// 巨坑,不能 使用_, item := range tmlpls 然后添加, item不是指针
for i, _ := range tmpls {
tmpl := tmpls[i]
t.cache.Set(strconv.Itoa(int(tmpl.Id)), &tmpl)
}
}
func (t *TmplCache) Get(key uint32) *model.SpaceTemplate {
item, ok := t.cache.GetByInt(int(key))
if !ok {
return nil
}
// 复制一份,防止外面把缓存数据给修改了
tp := item.(*model.SpaceTemplate)
tmpl := *tp
return &tmpl
}
func (t *TmplCache) GetAll() []*model.SpaceTemplate {
// 返回的slice都是指针
all := t.cache.GetAll()
// 拷贝一份
items := make([]*model.SpaceTemplate, len(all))
for i := 0; i < len(all); i++ {
item := all[i].(*model.SpaceTemplate)
t := *item
items[i] = &t
}
return items
}
spacespec的数据加载也是如此。
为了防止多个对象被创建浪费内存,我们可以实现一个简单工厂,使用工厂来创建这些cache
internal/caches/cachefactory.go
package caches
import (
"github.com/mangohow/cloud-ide-webserver/internal/dao"
"reflect"
"sync"
)
type cacheFactory struct {
caches map[reflect.Type]interface{}
lock sync.Mutex
}
var factory = &cacheFactory{caches: make(map[reflect.Type]interface{})}
func CacheFactory() *cacheFactory {
return factory
}
func (f *cacheFactory) TmplCache(dao *dao.SpaceTemplateDao) *TmplCache {
t := reflect.TypeOf(&TmplCache{})
f.lock.Lock()
defer f.lock.Unlock()
if c, ok := f.caches[t]; ok {
return c.(*TmplCache)
}
cache := newTmplCache(dao)
f.caches[t] = cache
cache.LoadCache()
return cache
}
func (f *cacheFactory) SpecCache(dao *dao.SpaceTemplateDao) *SpecCache {
t := reflect.TypeOf(&SpecCache{})
f.lock.Lock()
defer f.lock.Unlock()
if c, ok := f.caches[t]; ok {
return c.(*SpecCache)
}
cache := newSpecCache(dao)
f.caches[t] = cache
cache.LoadCache()
return cache
}
6、功能开发
目前先完善主要的功能:
用户登录
获取所有模板
创建工作空间(只创建,不运行)
创建工作空间并运行
启动工作空间
停止工作空间
删除工作空间
列出所有工作空间
6.1 用户登录
用户登录的请求数据:username和password
,暂时先不处理加密的事情
controller:
从请求中解析出数据,调用service进行登录业务的处理
type UserController struct {
logger *logrus.Logger
service *service.UserService
}
// Login 用户登录 method: POST path: /login
func (u *UserController) Login(ctx *gin.Context) *serialize.Response {
// 获取请求参数
username := ctx.PostForm("username")
password := ctx.PostForm("password")
u.logger.Debugf("username:%s passowrd:%s", username, password)
if username == "" || password == "" {
return serialize.NewResponseOk(code.LoginFailed, nil)
}
// 调用service处理
user, err := u.service.Login(username, username)
if err != nil {
if err == service.ErrUserDeleted {
return serialize.NewResponseOk(code.LoginUserDeleted, nil)
}
u.logger.Warnf("login error:%v", err)
return serialize.NewResponseOk(code.LoginFailed, nil)
}
return serialize.NewResponseOk(code.LoginSuccess, user)
}
在上面的controller中返回了serialize.Response
类型的数据,这个类型是封装的一个统一的返回数据结构:
其中包含了http status
以及一个resResult
的结构,resResult是返回给用户的json数据,包含数据、状态以及信息
type resResult struct {
Data interface{} `json:"data"`
Status uint32 `json:"status"`
Message string `json:"message"`
}
type Response struct {
HttpStatus int
R resResult
}
在注册路由
时通过装饰器
进行处理:
// 注册路由
engine.POST("/login", Decorate(userController.Login))
// 装饰器
type Handler func(ctx *gin.Context) *serialize.Response
func Decorate(h Handler) gin.HandlerFunc {
return func(ctx *gin.Context) {
r := h(ctx)
if r != nil {
ctx.JSON(r.HttpStatus, &r.R)
}
serialize.PutResponse(r)
}
}
service:
处理用户登录逻辑,调用dao根据username和password从数据库中查询,如果查询到,而且用户账户正常,则生成token
token的生成采用jwt
func (u *UserService) Login(username, password string) (*model.User, error) {
// 1、从数据库中查询
user, err := u.dao.FindByUsernameAndPassword(username, password)
if err != nil {
return nil, err
}
// 2、检查用户状态是否正常
if code.UserStatus(user.Status) == code.StatusDeleted {
return nil, ErrUserDeleted
}
// 3、生成token
token, err := encrypt.CreateToken(user.Id, user.Username, user.Uid)
if err != nil {
return nil, err
}
user.Token = token
return user, nil
}
dao:
从数据库中根据用户名和密码查询
func (u *UserDao) FindByUsernameAndPassword(username, password string) (user *model.User, _ error) {
sql := `SELECT id, uid, username, nickname, email, avatar, status FROM t_user WHERE username = ? AND password = ?`
user = &model.User{}
err := u.db.Get(user, sql, username, password)
return user, err
}
6.2 获取所有模板
controller
// SpaceTmpls 获取所有模板 method: GET path: /api/tmpls
func (s *SpaceTmplController) SpaceTmpls(ctx *gin.Context) *serialize.Response {
tmpls, err := s.service.GetAllUsingTmpl()
if err != nil {
s.logger.Warnf("get tmpls err:%v", err)
return serialize.NewResponseOk(code.QueryFailed, nil)
}
return serialize.NewResponseOk(code.QuerySuccess, tmpls)
}
service:
由于已经实现了数据的缓存,可以直接从缓存中获取数据
func (s *SpaceTmplService) GetAllUsingTmpl() (tmpls []*model.SpaceTemplate, err error) {
return s.tmplCache.GetAll(), nil
}
6.3 创建工作空间
创建工作空间仅仅在数据库中插入一条数据。用户所能创建的工作空间的个数需要进行一个限制,不能让其进行无限制的创建,可以设置最大数量为20
.
用户请求的数据有:工作空间的名称、依据创建的模板id、空间规格id、用户id
controller:
获取请求参数、参数验证,调用service处理
// CreateSpace 创建一个云空间 method: POST path: /api/space
// Request Param: reqtype.SpaceCreateOption
func (c *CloudCodeController) CreateSpace(ctx *gin.Context) *serialize.Response {
// 1、用户参数获取和验证
req := c.creationCheck(ctx)
if req == nil {
ctx.Status(http.StatusBadRequest)
return nil
}
// 2、获取用户id,在token验证时已经解析出并放入ctx中了
idi, _ := ctx.Get("id")
id := idi.(uint32)
// 3、调用service处理然后响应结果
space, err := c.spaceService.CreateWorkspace(req, id)
switch err {
case service.ErrNameDuplicate:
return serialize.NewResponseOKND(code.SpaceCreateNameDuplicate)
case service.ErrReachMaxSpaceCount:
return serialize.NewResponseOKND(code.SpaceCreateReachMaxCount)
case service.ErrCreate:
return serialize.NewResponseOKND(code.SpaceCreateFailed)
case service.ErrReqParamInvalid:
ctx.Status(http.StatusBadRequest)
return nil
}
if err != nil {
return serialize.NewResponseOKND(code.SpaceCreateFailed)
}
return serialize.NewResponseOk(code.SpaceCreateSuccess, space)
}
// creationCheck 用户参数验证
func (c *CloudCodeController) creationCheck(ctx *gin.Context) *reqtype.SpaceCreateOption {
// 获取用户请求参数
var req reqtype.SpaceCreateOption
// 绑定数据
err := ctx.ShouldBind(&req)
if err != nil {
return nil
}
c.logger.Debug(req)
// 参数验证
get1, exist1 := ctx.Get("id")
_, exist2 := ctx.Get("username")
if !exist1 || !exist2 {
return nil
}
id, ok := get1.(uint32)
if !ok || id != req.UserId {
return nil
}
return &req
}
service:
验证创建的工作空间数量是否达到最大数量、验证工作空间的名称是否重复、获取模板和规格、构造数据,然后保存用户数据
// CreateWorkspace 创建云工作空间
func (c *CloudCodeService) CreateWorkspace(req *reqtype.SpaceCreateOption, userId uint32) (*model.Space, error) {
// 1、验证创建的工作空间是否达到最大数量
count, err := c.dao.FindCountByUserId(userId)
if err != nil {
c.logger.Warnf("get space count error:%v", err)
return nil, ErrCreate
}
if count >= MaxSpaceCount {
return nil, ErrReachMaxSpaceCount
}
// 2、验证名称是否重复
if err := c.dao.FindByUserIdAndName(userId, req.Name); err == nil {
c.logger.Warnf("find space error:%v", err)
return nil, ErrNameDuplicate
}
// 3、从缓存中获取要创建的云空间的模板
tmpl := c.tmplCache.Get(req.TmplId)
if tmpl == nil {
c.logger.Warnf("get tmpl cache error:%v", err)
return nil, ErrReqParamInvalid
}
// 4、从缓存中获取要创建的云空间的规格
spec := c.specCache.Get(req.SpaceSpecId)
if spec == nil {
return nil, ErrReqParamInvalid
}
// 5、构造云工作空间结构
now := time.Now()
space := &model.Space{
UserId: userId,
TmplId: tmpl.Id,
SpecId: spec.Id,
Spec: *spec,
Name: req.Name,
Status: model.SpaceStatusUncreated,
CreateTime: now,
DeleteTime: now,
StopTime: now,
TotalTime: 0,
Sid: generateSID(),
}
//6、 添加到数据库
spaceId, err := c.dao.Insert(space)
if err != nil {
c.logger.Errorf("add space error:%v", err)
return nil, ErrCreate
}
space.Id = spaceId
return space, nil
}
6.4 创建工作空间并启动
创建工作空间并启动就是在数据库中插入一条数据
,然后使用rpc来创建一个Pod并等待Pod就绪
controller:
解析验证数据,调用service来实现业务
// CreateSpaceAndStart 创建一个新的云空间并启动 method: POST path: /api/space_cas
// Request Param: reqtype.SpaceCreateOption
func (c *CloudCodeController) CreateSpaceAndStart(ctx *gin.Context) *serialize.Response {
req := c.creationCheck(ctx)
if req == nil {
ctx.Status(http.StatusBadRequest)
return nil
}
idi, _ := ctx.Get("id")
id := idi.(uint32)
uidi, _ := ctx.Get("uid")
uid := uidi.(string)
space, err := c.spaceService.CreateAndStartWorkspace(req, id, uid)
switch err {
case service.ErrNameDuplicate:
return serialize.NewResponseOKND(code.SpaceCreateNameDuplicate)
case service.ErrReachMaxSpaceCount:
return serialize.NewResponseOKND(code.SpaceCreateReachMaxCount)
case service.ErrCreate:
return serialize.NewResponseOKND(code.SpaceCreateFailed)
case service.ErrSpaceStart:
return serialize.NewResponseOKND(code.SpaceStartFailed)
case service.ErrReqParamInvalid:
ctx.Status(http.StatusBadRequest)
return nil
}
if err != nil {
return serialize.NewResponseOKND(code.SpaceCreateFailed)
}
return serialize.NewResponseOk(code.SpaceStartSuccess, space)
}
service:
首先创建工作空间也就是记录数据
、生成Pod的名称(采用ws-uid-sid作为Pod名称
)、使用rpc请求controller创建pod
、将sid和Pod的ip保存到redis中
// CreateAndStartWorkspace 创建并且启动云工作空间
func (c *CloudCodeService) CreateAndStartWorkspace(req *reqtype.SpaceCreateOption, userId uint32, uid string) (*model.Space, error) {
// TODO 检查是否有工作空间正在运行, 需要停止
// 1、创建工作空间
space, err := c.CreateWorkspace(req, userId)
if err != nil {
return nil, err
}
// 2、获取模板
tmpl := c.tmplCache.Get(req.TmplId)
if tmpl == nil {
c.logger.Warnf("get tmpl cache error:%v", err)
return nil, ErrCreate
}
// 3、生成Pod名称
podName := c.generatePodName(space.Sid, uid)
pod := pb.PodInfo{
Name: podName,
Namespace: CloudCodeNamespace,
Image: tmpl.Image,
Port: DefaultPodPort,
ResourceLimit: &pb.ResourceLimit{
Cpu: space.Spec.CpuSpec,
Memory: space.Spec.MemSpec,
Storage: space.Spec.StorageSpec,
},
}
// 5、请求k8s controller创建云空间
// 设置一分钟的超时时间
timeout, cancelFunc := context.WithTimeout(context.Background(), time.Minute)
defer cancelFunc()
spaceInfo, err := c.rpc.CreateSpaceAndWaitForRunning(timeout, &pod)
if err != nil {
c.logger.Warnf("rpc create space and wait error:%v", err)
return nil, ErrSpaceStart
}
// 访问路径为 http://domain/ws/uid/... ws: workspace
// 7、将相关信息保存到redis
host := spaceInfo.Ip + ":" + strconv.Itoa(int(spaceInfo.Port))
err = rdis.AddRunningSpace(uid, &model.RunningSpace{
Sid: space.Sid,
Uid: space.Name,
Host: host,
})
if err != nil {
c.logger.Errorf("add pod info to redis error, err:%v", err)
return nil, ErrSpaceStart
}
space.RunningStatus = model.RunningStatusRunning
return space, nil
}
6.5 停止工作空间
停止工作空间就是将正在运行的pod删除
,而且删除redis中的相关数据
controller:
获取sid和uid,调用service停止工作空间
// StopSpace 停止正在运行的云空间 method: PUT path: /api/space_stop
// Request Param: sid
func (c *CloudCodeController) StopSpace(ctx *gin.Context) *serialize.Response {
var req struct{
Sid string `json:"sid"`
}
err := ctx.ShouldBind(&req)
if err != nil {
c.logger.Warningf("bind error:%v", err)
ctx.Status(http.StatusBadRequest)
return nil
}
uidi, ok := ctx.Get("uid")
if !ok {
ctx.Status(http.StatusBadRequest)
return nil
}
uid := uidi.(string)
err = c.spaceService.StopWorkspace(req.Sid, uid)
if err != nil {
if err == service.ErrWorkSpaceIsNotRunning {
return serialize.NewResponseOKND(code.SpaceStopIsNotRunning)
}
return serialize.NewResponseOKND(code.SpaceStopFailed)
}
return serialize.NewResponseOKND(code.SpaceStopSuccess)
}
service:
查询reids,判断工作空间是否正在运行,如果正在运行就先删除redis中的数据,然后调用rpc将pod删除
// StopWorkspace 停止云工作空间
func (c *CloudCodeService) StopWorkspace(sid, uid string) error {
// 1、查询云工作空间是否正在运行并删除数据
isRunning, err := rdis.CheckRunningSpaceAndDelete(uid)
if err != nil {
c.logger.Warnf("check is running error:%v", err)
return err
}
if !isRunning {
return ErrWorkSpaceIsNotRunning
}
// 2、停止workspace
name := c.generatePodName(sid, uid)
_, err = c.rpc.DeleteSpace(context.Background(), &pb.QueryOption{
Name: name,
Namespace: CloudCodeNamespace,
})
if err != nil {
c.logger.Warnf("rpc delete space error:%v", err)
return err
}
return nil
}
6.6 删除工作空间
删除工作空间时要验证工作空间是否正在运行
,如果正在运行就不允许删除,需要先停止
。如果没有在运行
,就将数据库中对应的记录更新一下
,暂时先不删除,只将其状态设置为已删除
controller:
解析出要删除的工作空间的id,交由service处理
// DeleteSpace 删除已存在的云空间 method: DELETE path: /api/delete
// Request Param: id
func (c *CloudCodeController) DeleteSpace(ctx *gin.Context) *serialize.Response {
// 获取id
id, err := utils.QueryUint32(ctx, "id")
if err != nil {
c.logger.Warningf("get param sid failed:%v", err)
ctx.Status(http.StatusBadRequest)
return nil
}
c.logger.Debug("id:", id)
// 删除工作空间
err = c.spaceService.DeleteWorkspace(id)
if err != nil {
if err == service.ErrWorkSpaceIsRunning {
return serialize.NewResponseOKND(code.SpaceDeleteIsRunning)
}
return serialize.NewResponseOKND(code.SpaceDeleteFailed)
}
return serialize.NewResponseOKND(code.SpaceDeleteSuccess)
}
service:
查询工作空间是否正在运行,如果正在运行,直接返回错误。否则就更新数据库中的记录
// DeleteWorkspace 删除云工作空间
func (c *CloudCodeService) DeleteWorkspace(id uint32) error {
// 1、检查该工作空间是否正在运行,如果正在运行就返回错误
sid, err := c.dao.FindSidById(id)
if err != nil {
c.logger.Warnf("find sid error:%v", err)
return err
}
// 从redis中查询
isRunning, err := rdis.CheckIsRunning(sid)
if err != nil {
c.logger.Warnf("check is running error:%v", err)
return err
}
if isRunning {
return ErrWorkSpaceIsRunning
}
// 2、从mysql中删除记录
return c.dao.DeleteSpaceById(id)
}
6.7 列出工作空间
列出工作空间就是从数据中查询然后返回给用户数据
controller:
获取用户id和uid,调用service处理
// ListSpace 获取所有创建的云空间 method: GET path: /api/spaces
// Request param: id uid
func (c *CloudCodeController) ListSpace(ctx *gin.Context) *serialize.Response {
v1, e1 := ctx.Get("id")
v2, e2 := ctx.Get("uid")
if !e1 || !e2 {
ctx.Status(http.StatusBadRequest)
return nil
}
id := v1.(uint32)
uid := v2.(string)
spaces, err := c.spaceService.ListWorkspace(id, uid)
if err != nil {
return serialize.NewResponseOKND(code.QueryFailed)
}
return serialize.NewResponseOk(code.QuerySuccess, spaces)
}
service:
分别从mysql中查询用户的所有工作空间信息和从redis中查询正在运行的工作空间信息
// ListWorkspace 列出云工作空间
func (c *CloudCodeService) ListWorkspace(userId uint32, uid string) ([]model.Space, error) {
// 从mysql中查询所有的工作空间
spaces, err := c.dao.FindAllSpaceByUserId(userId)
if err != nil {
c.logger.Warnf("find spaces error:%v", err)
return nil, err
}
// 从redis中查询正在运行的工作空间信息
runningSpace, err := rdis.GetRunningSpace(uid)
if err != nil {
c.logger.Warnf("get running space error:%v", err)
return spaces, nil
}
if runningSpace == nil {
return spaces, nil
}
for i, item := range spaces {
if item.Name == runningSpace.Uid {
spaces[i].RunningStatus = model.RunningStatusRunning
}
}
return spaces, nil
}
7、功能测试
测试前需要启动controller、openresty和web服务
,在下面只展示两个主要的功能,其它功能测试就先不展示
controller:
web服务:
7.1 创建工作空间
首先测试创建工作空间的功能是否ok
使用apiPost发生http请求:
首先要登录,获取token,否则后面的请求都访问不了
然后将token加入其它请求的header中,接下来测试创建工作空间并启动:
响应中返回了一个sid,而且可以看的请求花费了3.73s,因为要创建并且等待pod运行,还是挺快的
在浏览器中访问,路径为:http://yourip/ws/${sid}/
成功访问到了我们启动的工作空间
7.2 停止工作空间
接下来测试将工作空间停止
停止后在浏览器中刷新已经访问不到。
目前后端的核心功能差不多都实现了,接下来将会实现前端部分以及其它功能的完善。