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

news2024/11/25 18:34:22

文章目录

    • 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框架为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 停止工作空间

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

在这里插入图片描述

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

 

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/92767.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何进行C++动态转换

&#x1f4d2;博客主页&#xff1a; ​​开心档博客主页​​ &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐留言&#x1f4dd; &#x1f4cc;本文由开心档原创&#xff01; &#x1f4c6;51CTO首发时间&#xff1a;&#x1f334;2022年12月12日&#x1f334; ✉…

如何评价「仙剑奇侠传六」使用Unity 3D引擎?

2022年看到问题&#xff0c;根据2022年Unity3D、Unreal Engine及仙剑六来一波“穿越马后炮式”回答。 1&#xff1a; 2022年看到问题&#xff0c;根据2022年Unity3D、Unreal Engine及仙剑六来一波“穿越马后炮式”回答。 1&#xff1a; 仙剑奇侠传六确为unity引擎制作&#…

大学生简单静态HTML网页模板源码——家乡介绍美丽乡村11页

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

【leetcode】将有序数组转换为二叉搜索树

一、题目描述 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。 示例 1&#xff1a; 输入&#xff1a;nums …

微信对接chatGPT,实现自动回复消息、在线答疑等功能

前言 近来chatGPT挺火的&#xff0c;也试玩了一下&#xff0c;确实挺有意思。这里记录一下在在微信中也可以使用chatGPT&#xff0c;顺带可以自动回复微信消息、实时答疑等。。。 上一篇文章回顾&#xff1a;https://frica.blog.csdn.net/article/details/128316712 本文章在u…

Spark3.3.0的DataFrame及Spark SQL编程的性能对比【单机模式下】

Spark3.3.0的DataFrame及Spark SQL编程的性能对比【单机模式下】 前言 Spark3.3.0较老早的2.4.0有极大的性能优化&#xff0c;尤其是对SQL做了大量的优化【数据倾斜等】&#xff0c;恰好近期遇到一些性能问题&#xff0c;特意写个Demo测试下DataFrame和Spark SQL在获取到相同…

c#入门-接口的抽象成员和虚成员

接口的抽象成员 接口的成员如果不指定主体&#xff0c;那么就是抽象成员。 你可以主动为他们加抽象修饰符&#xff0c;不过没什么用。 interface I属性 {public abstract int Hp { get; set; }public abstract int Mp { get; set; } }接口的虚成员 接口的成员可以指定主体。…

Jina 开箱即用的云原生多模态系统解决方案

Jina 是一个基于云原生的多模态应用框架&#xff0c;开发者使用 Jina 可以轻松构建、部署和维护高性能的云原生应用。你可能认为这些都只是空泛的营销口号&#xff0c;甚至产生疑问&#xff0c;到底什么是云原生&#xff1f;对构建多模态应用有什么帮助&#xff1f;它是否只是 …

Linux CentoOs7中用命令打开并修改文本文件

概述&#xff1a;VI/VIM VI 是 Unix 操作系统和类 Unix 操作系统中最通用的**文本编辑器**。 VIM 编辑器是从 VI 发展出来的一个性能更强大的**文本编辑器**。可以主动的以字体颜色辨别语法的正确性&#xff0c;方便程序设计。VIM 与 VI 编辑器完全兼容。 我们常常用vim命令…

C#语言实例源码系列-实现热键屏幕和设置热键

专栏分享点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册 &#x1f449;关于作者 众所周知&#xff0c;人生是一个漫长的流程&#xff0c;不断克服困难&#xff0c;不断反思前进的过程。在这个过程中…

C++ 初阶 文件操作和io流

作者&#xff1a;小萌新 专栏&#xff1a;C初阶 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;简单介绍C中的文件操作和io流 文件操作和io流C语言中的输入和输出流是什么&#xff1f;Cio流C标准io流C中流的特性C文件io流以二进制形…

Jenkins全局安全配置

文章目录一、进入全局安全设置页面二、各选项功能Disable remember me&#xff08;禁用记住我&#xff09;Security Realm(安全域)Delegate to servlet container&#xff08;servlet 容器代理&#xff09;Jenkins’ own user database&#xff08;Jenkins 专有用户数据库&…

Pr:导出页面之预览与摘要窗口

使用“预览”窗口&#xff0c;可在导出前预览媒体&#xff0c;设置自定义的持续时间&#xff0c;如果导出为不同的帧大小&#xff0c;还可以控制源视频适应输出帧的方式。使用“摘要”窗口&#xff0c;可以快速查看源及输出的音视频信息。◆ ◆ ◆预览窗口范围Range可自定义导…

居家办公如何避免数据泄露?

随着疫情管控政策的调整&#xff0c;越来越多的“小阳人”出现&#xff0c;企业不得不面对员工在家远程办公。 面对突如其来的远程办公&#xff0c;很多企业都没有做好准备&#xff0c;甚至采取微信、QQ、互联网邮箱、远程会议等方式传递秘密信息。但是&#xff0c;这样的居家…

ChatGpt详细注册流程

ChatGpt详细注册流程ChatGpt的网址&#xff1a;直接点击我 点击链接后向下滑动看到TRY CHATGPT如下图所示&#xff1a; 点击TRY CHATGPT后会跳转如下图界面&#xff1a; 点击Log in(登录)如下图&#xff1a; 因为首次登录你肯定是没有账号的所以需要先点击红框框出的Sign up…

QQ注册界面仿写(HTML+CSS+JS)

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;前端案例分…

整合当地商圈资源,异业联盟打出促消费花式组合拳

如今的市场竞争白热化&#xff0c;大商家逐渐形成垄断的格局。许多行业的第一品牌跟第二品牌主宰着市场&#xff0c;为了打破这种局面&#xff0c;小商家联合起来对抗大商家&#xff0c;所以异业联盟因此诞生。那么建立异业联盟对大家有什么好处呢&#xff1f; 异业联盟针对于商…

RK3568平台开发系列讲解(工具命令篇)Android Debug Bridge常用命令

🚀返回专栏总目录 文章目录 一、工作常用的adb命令二、ADB的原理沉淀、分享、成长,让自己和他人都能有所收获!😄 📢ADB-Android Debug Bridge,它是 Android 开发/测试人员不可替代的强大工具,也是 Android 设备玩家的好玩具。安卓调试桥 (Android Debug Bridge, adb)…

uniapp 之 扫普通二维码进入小程序

前言 因项目的需求&#xff0c;需要我完成 在微信的扫一扫中&#xff0c;扫后端定义的二维码进入小程序中 这个需求说简单也很简单&#xff0c;说难也不难&#xff0c;就是花费了我几天的时间 需要在 开发 ---> 开发管理 下的开发设置 中的 一直往下滑 直到出现 扫普通…

11、Redis_事务_秒杀案例

文章目录11、Redis_事务_秒杀案例11.1 解决计数器和人员记录的事务操作11.2 Redis事务--秒杀并发模拟11.2.1 联网11.2.2 无网络11.2.3 测试及结果11.2.3.1 通过ab测试11.2.3.2 超卖11.3 超卖问题11.4 利用乐观锁淘汰用户&#xff0c;解决超卖问题。11.5 继续增加并发测试11.5.1…