k8s编程operator实战之云编码平台——④web后端实现

20 篇文章 4 订阅
12 篇文章 10 订阅


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编程operator实战之云编码平台——⑤项目完成、部署
 
        在前两章中分别实现了k8s controller和后端pod的访问。通过controller可以实现code-server容器的创建、删除以及状态维护等,通过openresty可以实现后端pod的动态反向代理。

        接下来将会实现web后端,使用的web框架为Ginmysql驱动为sqlxredis驱动为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 停止工作空间

接下来测试将工作空间停止

在这里插入图片描述

停止后在浏览器中刷新已经访问不到。

 

目前后端的核心功能差不多都实现了,接下来将会实现前端部分以及其它功能的完善。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值