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实战之云编码平台——⑤项目完成、部署
 

1、效果展示

截至目前,云编码平台的基本功能已经实现了,有以下功能:

  • 注册、登录

  • 浏览工作空间模板

  • 创建工作空间

  • 运行工作空间

  • 停止工作空间

  • 删除工作空间

  • 浏览工作空间

使用该平台,我们可以根据空间模板来创建出一个云开发环境。对于每个用户来说,允许存在的工作空间最大数量为20个,允许同时启动的工作空间数量为1个。用户的代码数据以及安装的插件都是可以保存下来的。

目前代码是完全开源的,希望感兴趣的童鞋可以给个star。

实现效果如下:

使用Go语言开发一个云编码平台

 

2、保存用户状态和访问用户服务实现方案

在前面的章节中已经实现了后端Pod的反向代理,在创建工作空间后,用户可以成功访问。接下来要解决的问题主要有两个:

  1. 如何保存用户的状态
  2. 如何访问到用户在工作空间中启动的http服务

2.1 如何保存用户的状态

        由于K8s并不支持停止Pod的功能,因此当用户需要停止工作空间的时候,我们只能将对应的Pod删除,待用户再次启动工作空间时再次创建Pod。但是将Pod删除后,用户的数据、用户自己安装的软件和插件在再次启动Pod后都会没有了,因为Pod一旦被删除其中的数据也不会保存。那么要解决的问题就是如何将用户的数据保存下来。

        K8s的数据持久化方式有很多种,比如EmptyDirHostPathNFS等。

  • EmptyDir:这种方式适用于在容器之间共享数据,一旦Pod被删除,EmptyDir也会被清空。
  • HostPath:这种方式在Pod删除后依然会保留数据,但是由于Pod下次运行的主机是不确定的,可能下次该Pod就被调度到别的主机上了。
  • NFS:这种方式比较适合用来保存用户数据,就算Pod下次被调度到别的节点,依然可以访问到之前的数据。

 
       我们可以使用NFS来保存用户的数据,在Pod启动前数据卷挂载到/user_data/目录下用户的数据用户安装的插件存放在该目录下,这样的话,就算将Pod删除,数据依然存在。当用户再次启动工作空间时,就将之前的数据卷挂载到Pod中,这样就可以保存用户的数据和安装的插件了。但是有一个缺点就是用户安装的软件是无法保存的。
        直接使用NFS挂载显然是比较麻烦的,因此我们可以使用K8s的PV和PVC。我们需要提前创建出一批PV,以待使用。

  • 用户第一次创建工作空间时:先创建出PVC,然后创建Pod,将PVC挂载到Pod中。
  • 用户停止工作空间时:只将Pod删除。
  • 用户重新启动工作空间时:创建Pod,将PVC挂载到Pod中。
  • 用户删除工作空间时:此时才将PVC删除。

        我们将PV的回收策略改为Recycle,这样当关联的PVC被删除后,PV中存放的数据将会K8s自动被清理,以待下次使用。

关于nfs的安装使用以及PV和PVC可以参考:Kubernetes(K8S)学习笔记

在这里插入图片描述

 

2.1.1 解决保留安装的插件问题

        code-server默认的用户设置和插件安装目录在/root/.local/share/code-server/中,插件的默认安装位置为/root/.local/share/code-server/extensions目录中。在启动code-server时指定参数--user-data-dir可以修改用户设置保存的目录指定参数 --extensions-dir可以修改插件的保存位置

        我们将NFS中的一个目录挂载到Pod的/user_data/目录下该目录中的内容就会被清空。所以我们需要在Pod启动后将code-server的用户设置以及已经安装的插件复制到/user_data/.local/share/code-server中,然后在启动code-server时指定用户数据目录插件目录。要想实现这个功能需要我们重新来构建Docker镜像容器的启动命令为启动一个初始化脚本,脚本负责数据的拷贝以及之后code-server的启动

脚本代码如下:

#!/bin/bash 


function graceful_exit () {
    echo "receive SIGTERM, exiting..."
    pid=$(ps -ef | grep code-server | awk '{print $2}')
    kill -SIGTERM "$pid"

    exit 0
}

trap graceful_exit SIGTERM

while :;do

    # 创建用户工作空间
    if [ ! -d "$USER_WORKSPACE" ]; then
        echo "create $USER_WORKSPACE"
        mkdir -p $USER_WORKSPACE
    fi

    # 第一次启动工作空间,拷贝code-server的数据
    if [ ! -f "/user_data/.local/share/.first_start" ]; then
        echo "copy code-server"
        if [ ! -d "/user_data/.local/share" ]; then
            mkdir -p /user_data/.local/share
        fi
        cp -r /root/.local/share/code-server-bak /user_data/.local/share/code-server
        touch /user_data/.local/share/.first_start
    fi

    # 启动code-server
    node_id=$(ps aux | grep -E "/.workspace/code-server/lib/node /.workspace/code-server --port 9999 --host 0.0.0.0 --auth none" | grep -v grep)
    if [ -z "$node_id" ]; then
        nohup code-server --port 9999 --host 0.0.0.0 \
        --auth none --disable-update-check  --locale zh-cn \
        --user-data-dir /user_data/.local/share/code-server \
        --extensions-dir /user_data/.local/share/code-server/extensions \
        --open /user_data/workspace/ &

        echo "start code-server success"
    fi

    sleep 3
done

脚本要做的事情主要有4个:

  • 创建用户工作空间:也就是用户通过浏览器访问的vs-code中的打开目录位置,即/user_data/workspace
  • 拷贝code-server的数据:在第一次启动工作空间时,将/root/.local/share/目录下的code-server-bak备份数据拷贝到/user_data/.local/share/code-server目录中
  • 启动code-server:在前面的初始化工作完成后,启动code-server,指定用户数据保存位置以及插件保存位置。
  • 优雅退出:当删除Pod时,K8s会向容器发送SIGTERM信号以终止容器,默认最大宽限时间为30s,如果30s后容器的1号进程依然没有结束,那么就会发送SIGKILL信号来强行终止容器。但是由于shell脚本默认屏蔽SIGTERM信号,因此如果不做处理,每次删除Pod都需要30s。因此我们需要在脚本中捕获SIGTERM信号,同时在自己退出前通知code-server进程退出。

Dockerfile的内容如下:

FROM ubuntu:20.04

WORKDIR /.workspace

# 拷贝code-server的可执行文件
COPY code-server-4.9.0-linux-amd64.tar.gz .
# 拷贝go sdk
COPY go1.19.4.linux-amd64.tar.gz .
# 拷贝Source Code Pro字体文件
COPY SourceCodeVariable* ./
# 拷贝运行脚本
COPY .init.sh /root/
# 拷贝code-server用户数据(用户数据、插件)
COPY code-server-bak /root/.local/share/code-server-bak

RUN apt-get -y update                                && \          
        apt-get -y install fontconfig                    && \
        mkdir -p ~/.fonts/source-code-pro             && \
        cp SourceCodeVariable* ~/.fonts/source-code-pro  && \
        cd ~/.fonts/source-code-pro                   && \
        fc-cache -f -v                                && \
        cd /.workspace                                && \
        rm -f SourceCodeVariable*                     && \
        tar zxvf code-server-4.9.0-linux-amd64.tar.gz && \
        mv code-server-4.9.0-linux-amd64 code-server  &&\
        rm -f code-server-4.9.0-linux-amd64.tar.gz    && \
        tar zxvf go1.19.4.linux-amd64.tar.gz -C /usr/local     && \
        rm -f go1.19.4.linux-amd64.tar.gz             && \
        mkdir -p /go/src /go/pkg /go/bin              && \
        apt-get -qq update                            && \
        apt-get -qq install -y --no-install-recommends ca-certificates curl

# 拷贝go工具
COPY go_tools/* /go/bin/

ENV GO111MODULE on
ENV GOPROXY https://goproxy.cn,direct
ENV GOROOT /usr/local/go
ENV PATH /usr/local/go/bin:$PATH
ENV GOPATH /go
ENV PATH $GOPATH/bin:$PATH
ENV USER_WORKSPACE /user_data/workspace
ENV CODE_SERVER_DIR /.workspace/code-server
ENV PATH /.workspace/code-server/bin:$PATH

WORKDIR /root

EXPOSE 9999

# 执行脚本文件
CMD ["/bin/bash", ".init.sh"]

该Dockerfile用来构建包含go语言环境和code-server的镜像。

该镜像已经上传到了阿里云镜像仓库,地址:registry.cn-hangzhou.aliyuncs.com/k8s-cloud-ide/code-server-go1.19:v1.0

 

2.2 如何访问到用户在工作空间中启动的http服务

③Code-Server Pod访问实现访问实现中已经实现了对容器中的code-server的反向代理,但是还没有实现用户编写的http服务的反向代理。在测试的时候,我发现code-server已经帮我们实现了代理。

首先回顾一下是如何通过nginx的反向代理来访问到Pod中的code-server的,主要有下面几个步骤:

  1. 在启动Pod时,获取到Pod的ip,然后将Pod的ip和code-server监听的端口保存到redis中。
  2. 通过浏览器访问http://host:ip/ws/sid/时,会访问到nginx中,nginx解析出路径中的sid,然后根据sid从redis取得Pod的ip和code-server的端口
  3. nginx通过取得的ip和port将请求代理到对应的Pod中,这样就访问到了Pod中的code-server

在这里插入图片描述

2.2.1 code-server如何帮我们实现了用户程序的代理

当我们在启动的工作空间中实现了一个http服务并启动后,code-server中自带的功能可以检测到我们监听了新的端口,然后询问是否在新的页面访问,如下图:

在这里插入图片描述

点击Open In Brower的按钮后,就会在打开新的页面访问:

在这里插入图片描述

但是发现在没有配置反向代理的情况下,竟然访问到了里面的服务。后来经过多次的调试我才发现原来code-server有代理的功能。

推测依据如下:

根据上面的地址http://192.168.44.100/ws/63f598a85e93db0001a38757/proxy/8080/访问,首先会访问到nginx,然后nginx对路径进行解析,会将请求代理到http://PodIP:Port/proxy/8080,这个路径显然是访问code-server的,但是竟然可以访问到我们自己的服务。因此推测,在访问/proxy/8080时,code-server会将该请求代理到localhost:8080/中,这样就访问到了我们自己的服务。

在这里插入图片描述

 

3、Operator功能实现

        在第②章中实现了controller的初步功能,但是在第二章的实现中我们并没有使用CRD来实现,只是通过创建删除Pod来进行了实现,这种方案实现起来比较简单,功能已经基本实现了:https://github.com/mangohow/cloud-ide-k8s-controller
        但是在本章中并不会讲解这种方式的实现,我们将使用KubeBuilder来构建一个Operator。Operator的代码:https://github.com/mangohow/cloud-ide-k8s-operator

        Operator和Controller实现的功能是一样的,但是实现的方式不同。

 

3.1 使用KubeBuilder创建项目

首先,创建一个文件夹,并且初始化go mod

mkdir <yourproject>
cd <yourproject>
go mod init <projectname>

初始化项目:

kubebuilder init --domain <yourdomain>

创建api

在创建api时,要指定group、version和kind

在此我的kind指定为WorkSpace

kubebuilder create api --group <yourgroup> --version <version> --kind WorkSpace
3.1.1 完善kind中的字段

项目创建完成后,我们需要完善api/<version>\workspace_types.go中的go结构体

type WorkSpaceOperation string

const (
	WorkSpaceStart WorkSpaceOperation = "Start"
	WorkSpaceStop                     = "Stop"
)

type WorkSpacePhase string

const (
	WorkspacePhaseRunning WorkSpacePhase = "Running"
	WorkspacePhaseStopped                = "Stopped"
)


type WorkSpaceSpec struct {
	// Workspace machine specifications
	Cpu     string `json:"cpu,omitempty"`
	Memory  string `json:"memory,omitempty"`
	Storage string `json:"storage,omitempty"`

	// 硬件资源
	Hardware string `json:"hardware,omitempty"`

	// The image
	Image string `json:"image,omitempty"`

	// Exposed port
	Port int32 `json:"port,omitempty"`

	// Volume mount path
	MountPath string `json:"mountPath"`

	// The operation to do
	Operation WorkSpaceOperation `json:"operation,omitempty"`
}

type WorkSpaceStatus struct {
	Phase WorkSpacePhase `json:"phase,omitempty"`
}


//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Hardware",type=string,JSONPath=`.spec.hardware`
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"

// WorkSpace is the Schema for the workspaces API
type WorkSpace struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   WorkSpaceSpec   `json:"spec,omitempty"`
	Status WorkSpaceStatus `json:"status,omitempty"`
}

在WorkSpaceSpec中定义了多个字段:

  • Cpu、Memory、Storage:表示该工作空间使用的cpu、内存和存储的规格
  • Hardware:是一个用于描述硬件资源的字段,用于在使用kubectl查询时显示信息
  • Image:pod使用的镜像
  • Port:pod中code-server监听的端口
  • MountPath:存储卷的挂载位置
  • Operation:要进行的操作,用于启动或者停止工作空间

使用// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=.status.phase我们可以告诉K8s在使用kubectl时可以显示的字段

在这里插入图片描述

同时,我们还使用了K8s的subresource,也就是status字段,在status中定义了一个Phase,该字段用于表示当前Workspace的状态,分别是运行状态停止状态

在使用status子资源时一定要加上//+kubebuilder:subresource:status,正常情况下Kubebuilder创建的项目已经自动帮我们加上了

字段修改完成后,可以使用下面的命令来生成crd等的yaml文件:

make manifest

使用下面的命令可以将crd安装到集群中

# 安装CRD到集群
make install

# 卸载CRD
make uninstall

安装完成后,可以使用kubectl来查看crd

kubectl get crd

在这里插入图片描述

 

3.2 controller功能实现

接下来我们就需要完善controller的功能了,也就是完善controllers包下的workspace_controller.go中的Reconcile

Reconcile的意思是协调,因此我们需要在Reconcile中做的事情就是协调Workspace的状态。

  • 用户创建工作空间:用户创建的工作空间就对应于我们的CR,也就是WorkSpace,当用户创建工作空间时,我们就创建出一个CR,这个可以在RPC中实现
  • 用户启动工作空间:用户启动工作空间,就需要使用Update操作来更新Workspace中的Operation字段为Start。当用户更新了字段后,我们就可以获取到WorkSpace的最新状态,然后查看Operation字段,如果是Start,那么我们就需要创建出PVC和Pod,同时更新status为Running即可
  • 用户停止工作空间:于启动工作空间相似,修改Operation字段为Stop,我们只需将Pod删除,同时更新status为Stopped即可
  • 用户删除工作空间:当用户删除了工作空间后,我们就需要将Pod和PVC同时删除。

Reconcile的流程如下:

在这里插入图片描述

controller的代码如下:

package controllers

var Mode string

const (
	ModeRelease = "release"
	ModDev      = "dev"
)

// WorkSpaceReconciler reconciles a WorkSpace object
type WorkSpaceReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

func (r *WorkSpaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = log.FromContext(ctx)

	//先查询WorkSpace
	wp := mv1.WorkSpace{}
	err := r.Client.Get(context.Background(), req.NamespacedName, &wp)
	// case 1、没有找到Workspace,说明WorkSpace被删除了,删除对应的Pod和PVC即可
	if err != nil {
		if errors.IsNotFound(err) {
			if e1 := r.deletePod(req.NamespacedName); e1 != nil {
				klog.Errorf("[Delete Workspace] delete pod error:%v", e1)
				return ctrl.Result{Requeue: true}, e1
			}
			if e2 := r.deletePVC(req.NamespacedName); e2 != nil {
				klog.Errorf("[Delete Workspace] delete pvc error:%v", e2)
				return ctrl.Result{Requeue: true}, e2
			}

			return ctrl.Result{}, nil
		}

		klog.Errorf("get workspace error:%v", err)
		return ctrl.Result{Requeue: true}, err
	}

	// 找到了WorkSpace,根据WorkSpace的Operation字段判断要进行的操作
	switch wp.Spec.Operation {
	// case2: 启动WorkSpace,检查PVC是否存在,如果不存在则创建
	case mv1.WorkSpaceStart:
		// 检查PVC是否存在,不存在则创建
		err = r.createPVC(&wp, req.NamespacedName)
		if err != nil {
			klog.Errorf("[Start Workspace] create pvc error:%v", err)
			return ctrl.Result{Requeue: true}, err
		}
		// 创建Pod
		err = r.createPod(&wp, req.NamespacedName)
		if err != nil {
			klog.Errorf("[Start Workspace] create pod error:%v", err)
			return ctrl.Result{Requeue: true}, err
		}
		r.updateStatus(&wp, mv1.WorkspacePhaseRunning)

	// case3: 停止WorkSpace,删除Pod
	case mv1.WorkSpaceStop:
		//删除Pod
		err = r.deletePod(req.NamespacedName)
		if err != nil {
			klog.Errorf("[Stop Workspace] delete pod error:%v", err)
			return ctrl.Result{Requeue: true}, err
		}

		r.updateStatus(&wp, mv1.WorkspacePhaseStopped)
	}

	return ctrl.Result{}, nil
}

func (r WorkSpaceReconciler) updateStatus(wp *mv1.WorkSpace, phase mv1.WorkSpacePhase) {
	wp.Status.Phase = phase
	err := r.Client.Status().Update(context.Background(), wp)
	if err != nil {
		klog.Errorf("update status error:%v", err)
	}
}

// 在Owns中使用过滤器,防止Pod和PVC状态发生改变时触发Reconcile方法
func (r *WorkSpaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		WithOptions(controller.Options{MaxConcurrentReconciles: 8}).
		For(&mv1.WorkSpace{}).
		Owns(&v1.Pod{}, builder.WithPredicates(predicatePod)).
		Owns(&v1.PersistentVolumeClaim{}, builder.WithPredicates(predicatePVC)).
		Complete(r)
}

func (r *WorkSpaceReconciler) createPod(space *mv1.WorkSpace, key client.ObjectKey) error {
	// 1.检查Pod是否存在
	exist, err := r.checkPodExist(key)
	if err != nil {
		return err
	}

	// Pod已存在,直接返回
	if exist {
		return nil
	}

	// 2.创建Pod
	pod := r.constructPod(space)

	// 设置控制器,如果设置了控制器,那么被控制的资源的变化也会被发送到队列中
	//if err = controllerutil.SetControllerReference(space, pod, r.Scheme); err != nil {
	//	return err
	//}

	ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*30)
	defer cancelFunc()
	err = r.Client.Create(ctx, pod)
	if err != nil {
		// 如果Pod已经存在,直接返回
		if errors.IsAlreadyExists(err) {
			return nil
		}

		return err
	}

	return nil
}

// 构造一个Pod对象
func (r *WorkSpaceReconciler) constructPod(space *mv1.WorkSpace) *v1.Pod {
	volumeName := "volume-user-workspace"
	pod := &v1.Pod{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Pod",
			APIVersion: "v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      space.Name,
			Namespace: space.Namespace,
			Labels: map[string]string{
				"app": "cloud-ide",
			},
		},
		Spec: v1.PodSpec{
			Volumes: []v1.Volume{
				{
					Name: volumeName,
					VolumeSource: v1.VolumeSource{
						PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
							ClaimName: space.Name,
							ReadOnly:  false,
						},
					},
				},
			},
			Containers: []v1.Container{
				{
					Name:            space.Name,
					Image:           space.Spec.Image,
					ImagePullPolicy: v1.PullIfNotPresent,
					Ports: []v1.ContainerPort{
						{
							ContainerPort: space.Spec.Port,
						},
					},
					// 容器挂载存储卷
					VolumeMounts: []v1.VolumeMount{
						{
							Name:      volumeName,
							ReadOnly:  false,
							MountPath: space.Spec.MountPath,
						},
					},
				},
			},
		},
	}

	if Mode == ModeRelease {
		// 最小需求CPU2核、内存1Gi == 1 * 2^10
		pod.Spec.Containers[0].Resources = v1.ResourceRequirements{
			Requests: map[v1.ResourceName]resource.Quantity{
				v1.ResourceCPU:    resource.MustParse("2"),
				v1.ResourceMemory: resource.MustParse("1Gi"),
			},
			Limits: map[v1.ResourceName]resource.Quantity{
				v1.ResourceCPU:    resource.MustParse(space.Spec.Cpu),
				v1.ResourceMemory: resource.MustParse(space.Spec.Memory),
			},
		}
	}

	return pod
}

func (r *WorkSpaceReconciler) createPVC(space *mv1.WorkSpace, key client.ObjectKey) error {
	// 1.先检查PVC是否已经存在
	exist, err := r.checkPVCExist(key)
	if err != nil {
		// PVC已经存在
		return err
	}

	// PVC已经存在,无需创建
	if exist {
		return nil
	}

	// 2.PVC不存在,创建PVC
	pvc, err := r.constructPVC(space)
	if err != nil {
		klog.Errorf("construct pvc error:%v", err)
		return err
	}

	// 设置了OwnerReference之后,PVC的状态发生变化,也会触发Reconcile方法
	// 但是对于PVC来说,我们不希望它触发这个方法,因此我们可以使用过滤器来进行过滤
	//if err = controllerutil.SetControllerReference(space, pvc, r.Scheme); err != nil {
	//	return err
	//}

	ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*30)
	defer cancelFunc()
	err = r.Client.Create(ctx, pvc)
	if err != nil {
		if errors.IsAlreadyExists(err) {
			return nil
		}

		return err
	}

	return nil
}

// 构造PVC对象
func (r *WorkSpaceReconciler) constructPVC(space *mv1.WorkSpace) (*v1.PersistentVolumeClaim, error) {
	quantity, err := resource.ParseQuantity(space.Spec.Storage)
	if err != nil {
		return nil, err
	}

	pvc := &v1.PersistentVolumeClaim{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "v1",
			Kind:       "PersistentVolumeClaim",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      space.Name,
			Namespace: space.Namespace,
		},
		Spec: v1.PersistentVolumeClaimSpec{
			AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteMany},
			Resources: v1.ResourceRequirements{
				Limits:   v1.ResourceList{v1.ResourceStorage: quantity},
				Requests: v1.ResourceList{v1.ResourceStorage: quantity},
			},
		},
	}

	return pvc, nil
}

func (r *WorkSpaceReconciler) checkPodExist(key client.ObjectKey) (bool, error) {
	pod := &v1.Pod{}
	// 先查询一下
	err := r.Client.Get(context.Background(), key, pod)
	if err != nil {
		if errors.IsNotFound(err) {
			return false, nil
		}

		klog.Errorf("get pod error:%v", err)
		return false, err
	}

	return true, nil
}

func (r *WorkSpaceReconciler) deletePod(key client.ObjectKey) error {
	exist, err := r.checkPodExist(key)
	if err != nil {
		return err
	}

	// Pod不存在,直接返回
	if !exist {
		return nil
	}

	pod := &v1.Pod{}
	pod.Name = key.Name
	pod.Namespace = key.Namespace

	ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*35)
	defer cancelFunc()
	// 删除Pod
	err = r.Client.Delete(ctx, pod)
	if err != nil {
		if errors.IsNotFound(err) {
			return nil
		}

		klog.Errorf("delete pod error:%v", err)
		return err
	}

	return nil
}

func (r *WorkSpaceReconciler) checkPVCExist(key client.ObjectKey) (bool, error) {
	pvc := &v1.PersistentVolumeClaim{}
	err := r.Client.Get(context.Background(), key, pvc)
	if err != nil {
		if errors.IsNotFound(err) {
			return false, nil
		}

		klog.Errorf("get pvc error:%v", err)
		return false, err
	}

	return true, nil
}

func (r *WorkSpaceReconciler) deletePVC(key client.ObjectKey) error {
	exist, err := r.checkPVCExist(key)
	if err != nil {
		return err
	}

	// pvc不存在,无需再删除
	if !exist {
		return nil
	}

	pvc := &v1.PersistentVolumeClaim{}
	pvc.Name = key.Name
	pvc.Namespace = key.Namespace

	ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*30)
	defer cancelFunc()
	err = r.Client.Delete(ctx, pvc)
	if err != nil {
		if errors.IsNotFound(err) {
			return nil
		}

		klog.Errorf("delete pvc error:%v", err)
		return err
	}

	return nil
}

由于我们在代码中创建PVC和Pod时,设置了它的控制器,因此当Pod和PVC的状态发生改变后也会触发Reconcile方法,但是我们并不希望这两个资源触发该方法,因此我们可以使用过滤器。

package controllers

import (
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// 过滤所有的PVC, 防止其触发Reconcile方法
var predicatePVC = predicate.NewPredicateFuncs(func(object client.Object) bool {
	return false
})

var predicatePod = predicate.NewPredicateFuncs(func(object client.Object) bool {
	return false
})

 

3.3 Grpc service实现

为了防止WebServer直接和Apiserver进行通信,我们使用grpc的方式使webserver和operator来进行通信来完成Workspace的创建、启动、停止删除、已经查询

至于为什么不直接在WebServer中使用client-go来和Apisever通信有的原因:webserver最好是保持无状态的,如果我们直接和Apiserver通信,就算使用informer的方式,那么如果我们部署多个web实例的话,也会造成Apiserver的较大压力

3.3.1 proto文件的定义

grpc的proto文件定义如下:

syntax = "proto3";

package pb;

option go_package = "./;pb";

// 工作空间的资源限制
message ResourceLimit {
  string cpu = 1;
  string Memory = 2;
  string Storage = 3;
}

// 工作空间信息
message WorkspaceInfo {
  string name = 1;
  string namespace = 2;
  string image = 3;
  int32 port = 4;
  string volumeMountPath = 5;
  ResourceLimit resourceLimit = 6;
}

message Response {
  int32 status = 1;
  string message = 2;
}

message QueryOption {
  string name = 1;
  string namespace = 2;
}

// 工作空间的状态
message WorkspaceStatus {
  int32 status = 1;
  string message = 2;
}

// 工作空间运行信息
message WorkspaceRunningInfo {
  string nodeName = 1;
  string ip = 2;
  int32 port = 3;
}

service CloudIdeService {
  // 创建云IDE空间并等待Pod状态变为Running,第一次创建,需要挂载存储卷
  rpc createSpace(WorkspaceInfo) returns (WorkspaceRunningInfo);
  // 启动(创建)云IDE空间,非第一次创建,无需挂载存储卷,使用之前的存储卷
  rpc startSpace(WorkspaceInfo) returns (WorkspaceRunningInfo);
  // 删除云IDE空间,需要删除存储卷
  rpc deleteSpace(QueryOption) returns (Response);
  // 停止(删除)云工作空间,无需删除存储卷
  rpc stopSpace(QueryOption) returns (Response);
  // 获取Pod运行状态
  rpc getPodSpaceStatus(QueryOption) returns (WorkspaceStatus);
  // 获取云IDE空间Pod的信息
  rpc getPodSpaceInfo(QueryOption) returns (WorkspaceRunningInfo);
}

最终要实现的接口如下:

type CloudIdeServiceServer interface {
	// 创建云IDE空间并等待Pod状态变为Running,第一次创建,需要挂载存储卷
	CreateSpace(context.Context, *WorkspaceInfo) (*WorkspaceRunningInfo, error)
	// 启动(创建)云IDE空间,非第一次创建,无需挂载存储卷,使用之前的存储卷
	StartSpace(context.Context, *WorkspaceInfo) (*WorkspaceRunningInfo, error)
	// 删除云IDE空间,需要删除存储卷
	DeleteSpace(context.Context, *QueryOption) (*Response, error)
	// 停止(删除)云工作空间,无需删除存储卷
	StopSpace(context.Context, *QueryOption) (*Response, error)
	// 获取Pod运行状态
	GetPodSpaceStatus(context.Context, *QueryOption) (*WorkspaceStatus, error)
	// 获取云IDE空间Pod的信息
	GetPodSpaceInfo(context.Context, *QueryOption) (*WorkspaceRunningInfo, error)
}

创建一个结构体来实现这个接口:

type WorkSpaceService struct {
	client client.Client
}

func NewWorkSpaceService(c client.Client) *WorkSpaceService {
	return &WorkSpaceService{
		client: c,
	}
}

var _ = pb.CloudIdeServiceServer(&WorkSpaceService{})

 

3.3.2 CreateSpace

CreateSpace用于创建工作空间并启动,我们只需要创建出WorkSpace即可,将其Operation设置为Start,我们的controller就会自动创建出PVC和Pod

步骤如下:

  1. 首先我们要查询工作空间是否存在,如果已经存在,就直接返回
  2. 创建工作空间,并且等待Pod处于Running状态再返回
  3. 由于Pod从启动到Running状态需要一些时间,因此我们每隔一段时间从缓存中获取,如果超过最大重试此次,那么说明可以是由于资源不足等原因导致了Pod无法正常启动,因此我们需要停止工作空间。

代码如下:

// CreateSpace 创建并且启动Workspace,将Operation字段置为"Start",当Workspace被创建时,PVC和Pod也会被创建
func (s *WorkSpaceService) CreateSpace(ctx context.Context, info *pb.WorkspaceInfo) (*pb.WorkspaceRunningInfo, error) {
	// 1.先查询workspace是否存在
	var wp mv1.WorkSpace
	exist := s.checkWorkspaceExist(ctx, client.ObjectKey{Name: info.Name, Namespace: info.Namespace}, &wp)
	stus := status.New(codes.AlreadyExists, WorkspaceAlreadyExist)
	if exist {
		return EmptyWorkspaceRunningInfo, stus.Err()
	}

	// 2.如果不存在就创建
	w := s.constructWorkspace(info)
	if err := s.client.Create(ctx, w); err != nil {
		if errors.IsAlreadyExists(err) {
			return EmptyWorkspaceRunningInfo, stus.Err()
		}

		klog.Errorf("create workspace error:%v", err)
		return EmptyWorkspaceRunningInfo, status.Error(codes.Unknown, err.Error())
	}

	// 3.等待Pod处于Running状态
	return s.waitForPodRunning(ctx, client.ObjectKey{Name: w.Name, Namespace: w.Namespace}, w)
}

func (s *WorkSpaceService) waitForPodRunning(ctx context.Context, key client.ObjectKey, wp *mv1.WorkSpace) (*pb.WorkspaceRunningInfo, error) {
	// 获取Pod的运行信息,可能会因为资源不足而导致Pod无法运行
	// 最多重试四次,如果还不行,就停止工作空间
	retry, maxRetry := 0, 5
	sleepDuration := []time.Duration{1, 3, 5, 8, 12}
	po := v1.Pod{}

loop:
	for {
		select {
		case <-ctx.Done():
			break loop
		default:
			if retry >= maxRetry {
				break loop
			}

			// 先休眠,等待Pod被创建并且运行起来
			time.Sleep(time.Second * sleepDuration[retry])

			if err := s.client.Get(context.Background(), key, &po); err != nil {
				if !errors.IsNotFound(err) {
					klog.Errorf("get pod error:%v", err)
				}
			} else {
				if po.Status.Phase == v1.PodRunning {
					return &pb.WorkspaceRunningInfo{
						NodeName: po.Spec.NodeName,
						Ip:       po.Status.PodIP,
						Port:     po.Spec.Containers[0].Ports[0].ContainerPort,
					}, nil
				}
			}

			retry++
		}
	}

	// 5.处理错误情况,停止工作空间
	s.StopSpace(ctx, &pb.QueryOption{
		Name:      wp.Name,
		Namespace: wp.Namespace,
	})

	return EmptyWorkspaceRunningInfo, status.Error(codes.ResourceExhausted, WorkspaceStartFailed)
}

 

3.3.3 StartSpace

StartSpace的主要功能就是启动Pod,我们只需要更新Workspace的Operation字段为Start即可,controller会字段创建PVC和Pod

步骤如下:

  1. 首先查询Workspace是否存在,不存在就无法启动
  2. 查询Pod是否已经正在运行了,如果是就直接返回数据
  3. 更新Workspace的Operation字段。

注意:由于K8s使用的是乐观锁的并发控制,通过ResourceVersion字段来进行控制,因此当我们在更新资源时,可能会因为我们的资源已经被修改,从而会导致我们更新失败,因此在更新失败时,需要重新尝试,但是在更新前,必须要从缓存中获取资源的最新状态。但是client-go已经为我们提供了方便的接口,直接使用即可。关于K8s的并发控制可以自己查阅相关资料

代码如下:

// StartSpace 启动Workspace
func (s *WorkSpaceService) StartSpace(ctx context.Context, info *pb.WorkspaceInfo) (*pb.WorkspaceRunningInfo, error) {
	// 1.先获取workspace,如果不存在返回错误
	var wp mv1.WorkSpace
	key := client.ObjectKey{Name: info.Name, Namespace: info.Namespace}
	exist := s.checkWorkspaceExist(ctx, key, &wp)
	if !exist {
		return EmptyWorkspaceRunningInfo, status.Error(codes.NotFound, WorkspaceNotExist)
	}

	// 2.查询Pod是否存在,如果存在直接返回数据
	pod := v1.Pod{}
	if err := s.client.Get(context.Background(), key, &pod); err == nil {
		return &pb.WorkspaceRunningInfo{
			NodeName: pod.Spec.NodeName,
			Ip:       pod.Status.PodIP,
			Port:     pod.Spec.Containers[0].Ports[0].ContainerPort,
		}, nil
	}

	// 3.更新Workspace,使用RetryOnConflict,当资源版本冲突时重试
	err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		// 每次更新前要获取最新的版本
		var p mv1.WorkSpace
		exist := s.checkWorkspaceExist(ctx, key, &p)
		if !exist {
			return nil
		}

		// 更新workspace的Operation字段
		wp.Spec.Operation = mv1.WorkSpaceStart
		if err := s.client.Update(ctx, &wp); err != nil {
			klog.Errorf("update workspace to start error:%v", err)
			return err
		}

		return nil
	})

	if err != nil {
		return EmptyWorkspaceRunningInfo, status.Error(codes.Unknown, err.Error())
	}

	if !exist {
		return EmptyWorkspaceRunningInfo, status.Error(codes.NotFound, WorkspaceNotExist)
	}

	return s.waitForPodRunning(ctx, key, &wp)
}

 

3.3.4 StopSpace

StopSpace用来停止工作空间,同StartSpace相似,只需要更新Operation字段为Stop即可

代码如下:

// StopSpace 停止Workspace,只需要删除对应的Pod,因此修改Workspace的操作为Stop即可
func (s *WorkSpaceService) StopSpace(ctx context.Context, option *pb.QueryOption) (*pb.Response, error) {
	// 使用Update时,可能由于版本冲突而导致失败,需要重试
	exist := true
	err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		var wp mv1.WorkSpace
		exist = s.checkWorkspaceExist(ctx, client.ObjectKey{Name: option.Name, Namespace: option.Namespace}, &wp)
		if !exist {
			return nil
		}

		// 更新workspace的Operation字段
		wp.Spec.Operation = mv1.WorkSpaceStop
		if err := s.client.Update(ctx, &wp); err != nil {
			klog.Errorf("update workspace to start error:%v", err)
			return err
		}

		return nil
	})

	if err != nil {
		return EmptyResponse, status.Error(codes.Unknown, err.Error())
	}

	if !exist {
		return EmptyResponse, status.Error(codes.NotFound, WorkspaceNotExist)
	}

	return EmptyResponse, nil
}

 

3.3.5 DeleteSpace

DeleteSpace的功能为删除Workspace,只需要将对应的Workspace资源删除,controller会负责删除Pod和PVC

// DeleteSpace 只需要将workspace删除即可,controller会负责删除对应的Pod和PVC
func (s *WorkSpaceService) DeleteSpace(ctx context.Context, option *pb.QueryOption) (*pb.Response, error) {
	// 先查询是否存在,如果不存在则也认为成功
	var wp mv1.WorkSpace
	exist := s.checkWorkspaceExist(ctx, client.ObjectKey{Name: option.Name, Namespace: option.Namespace}, &wp)
	if !exist {
		return EmptyResponse, nil
	}

	// 删除Workspace
	if err := s.client.Delete(ctx, &wp); err != nil {
		klog.Errorf("delete workspace error:%v", err)
		return EmptyResponse, status.Error(codes.Unknown, err.Error())
	}

	return EmptyResponse, nil
}

 

3.3.6 其它方法
// 获取WorkSpace的运行状态
func (s *WorkSpaceService) GetPodSpaceStatus(ctx context.Context, option *pb.QueryOption) (*pb.WorkspaceStatus, error) {
	pod := v1.Pod{}
	err := s.client.Get(ctx, client.ObjectKey{Name: option.Name, Namespace: option.Namespace}, &pod)
	if err != nil {
		if errors.IsNotFound(err) {
			return EmptyWorkspaceStatus, status.Error(codes.NotFound, "workspace is not running")
		}

		klog.Errorf("get pod space status error:%v", err)
		return &pb.WorkspaceStatus{Status: PodNotExist, Message: "NotExist"}, status.Error(codes.NotFound, err.Error())
	}

	return &pb.WorkspaceStatus{Status: PodExist, Message: string(pod.Status.Phase)}, nil
}

// 获取运行中的Pod的信息
func (s *WorkSpaceService) GetPodSpaceInfo(ctx context.Context, option *pb.QueryOption) (*pb.WorkspaceRunningInfo, error) {
	pod := v1.Pod{}
	err := s.client.Get(ctx, client.ObjectKey{Name: option.Name, Namespace: option.Namespace}, &pod)
	if err != nil {
		if errors.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, "workspace is not running")
		}

		klog.Errorf("get pod space info error:%v", err)
		return EmptyWorkspaceRunningInfo, status.Error(codes.Unknown, err.Error())
	}

	return &pb.WorkspaceRunningInfo{
		NodeName: pod.Spec.NodeName,
		Ip:       pod.Status.PodIP,
		Port:     pod.Spec.Containers[0].Ports[0].ContainerPort,
	}, nil
}

func (s *WorkSpaceService) checkWorkspaceExist(ctx context.Context, key client.ObjectKey, w *mv1.WorkSpace) bool {
	if err := s.client.Get(ctx, key, w); err != nil {
		if errors.IsNotFound(err) {
			return false
		}

		klog.Errorf("get workspace error:%v", err)
		return false
	}

	return true
}

func (s *WorkSpaceService) constructWorkspace(space *pb.WorkspaceInfo) *mv1.WorkSpace {
	hardware := fmt.Sprintf("%sC%s%s", space.ResourceLimit.Cpu,
		strings.Split(space.ResourceLimit.Memory, "i")[0], strings.Split(space.ResourceLimit.Storage, "i")[0])
	return &mv1.WorkSpace{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "cloud-ide.mangohow.com/v1",
			Kind:       "WorkSpace",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      space.Name,
			Namespace: space.Namespace,
		},
		Spec: mv1.WorkSpaceSpec{
			Cpu:       space.ResourceLimit.Cpu,
			Memory:    space.ResourceLimit.Memory,
			Storage:   space.ResourceLimit.Storage,
			Hardware:  hardware,
			Image:     space.Image,
			Port:      space.Port,
			MountPath: space.VolumeMountPath,
			Operation: mv1.WorkSpaceStart,
		},
	}
}

 

4、web server

web server要完善的功能就是与工作空间相关的服务,主要有以下方法:

  • CreateWorkspace:创建云工作空间,由于只是创建,因此只需要生成记录保存在数据库中即可
  • CreateAndStartWorkspace:创建并且启动工作空间,创建工作空间,然后启动Pod。
  • StopWorkspace:启动工作空间,如果是第一次启动,需要创建PVC和Pod,否则只需要创建Pod。
  • StopWorkspace:停止工作空间,也就是将Pod删除
  • DeleteWorkspace:删除工作空间,需要在工作空间停止的状态下删除。删除PVC,以及数据库中的记录

代码参考:https://github.com/mangohow/cloud-ide-webserver

 

4.1 CreateWorkspace

步骤如下:

  1. 默认能创建的工作空间的数量为20个,因此首先要检查确保不能超过20个
  2. 对于同一个用户来说,工作空间的名称不能重复,因此要验证名称是否重复
  3. 然后构造数据,保存到数据库中

代码如下:

// 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, ErrSpaceCreate
	}
	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.GetTmpl(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, ErrSpaceCreate
	}
	space.Id = spaceId

	return space, nil
}

 

4.2 StartSpace

步骤如下:

  1. 首先要检查是否有其它工作空间正在运行,因为限制一个用户同一时刻运行的工作空间只能有一个
  2. 然后检查该工作空间是否在数据库中存在
  3. 然后判断工作空间是否是第一次启动,如果是第一次启动就需要创建PVC
  4. 然后生成Pod的信息,调用rpc来创建pod
  5. 最后将Pod的信息保存到redis中以及更新数据库中的状态信息

代码如下:

// StartWorkspace 启动云工作空间
func (c *CloudCodeService) StartWorkspace(id, userId uint32, uid string) (*model.Space, error) {
	// 1、检查是否有其它工作空间正在运行, 同时只能有一个工作空间启动
	isRunning, err := rdis.CheckHasRunningSpace(uid)
	if err != nil {
		return nil, ErrSpaceStart
	}
	if isRunning {
		return nil, ErrOtherSpaceIsRunning
	}

	// 2.查询该工作空间是否存在
	space, err := c.dao.FindByIdAndUserId(id, userId)
	if err != nil {
		c.logger.Warnf("find space error:%v", err)
		return nil, ErrWorkSpaceNotExist
	}
	space.Id = id
	space.UserId = userId

	// 3.该工作空间是否是第一次启动
	startFunc := c.rpc.StartSpace
	switch space.Status {
	case model.SpaceStatusDeleted:
		return nil, ErrWorkSpaceNotExist
	case model.SpaceStatusUncreated:
		startFunc = c.rpc.CreateSpace
		spec := c.specCache.Get(space.SpecId)
		if spec == nil {
			return nil, ErrSpaceStart
		}
		space.Spec = *spec
	}

	// 4.启动工作空间
	ret, err := c.startWorkspace(&space, uid, startFunc)
	if err != nil {
		c.logger.Warnf("start workspace error:%v", err)
		return nil, err
	}

	return ret, nil
}

// startWorkspace 启动工作空间
func (c *CloudCodeService) startWorkspace(space *model.Space, uid string, startFunc StartFunc) (*model.Space, error) {
	// 1、获取空间模板
	tmpl := c.tmplCache.GetTmpl(space.TmplId)
	if tmpl == nil {
		c.logger.Warnf("get tmpl cache error")
		return nil, ErrSpaceStart
	}

	// 2、生成Pod信息
	podName := c.generatePodName(space.Sid, uid)
	pod := pb.WorkspaceInfo{
		Name:      podName,
		Namespace: CloudCodeNamespace,
		Image:     tmpl.Image,
		Port:      DefaultPodPort,
		VolumeMountPath: "/user_data/",
		ResourceLimit: &pb.ResourceLimit{
			Cpu:     space.Spec.CpuSpec,
			Memory:  space.Spec.MemSpec,
			Storage: space.Spec.StorageSpec,
		},
	}

	var retErr error
loop:
	for i := 0; i < 1; i++ {
		// 3、请求k8s controller创建云空间
		// 设置一分钟的超时时间
		timeout, cancelFunc := context.WithTimeout(context.Background(), time.Minute)
		defer cancelFunc()
		spaceInfo, err := startFunc(timeout, &pod)
		if err != nil {
			s, ok := status.FromError(err)
			if !ok {
				return nil, err
			}

			switch s.Code() {
			// 创建工作空间时,工作空间已存在,修改数据库中的status
			case codes.AlreadyExists:
				retErr = ErrSpaceAlreadyExist
				break loop

			// 启动工作空间时,工作空间不存在
			case codes.NotFound:
				return nil, ErrSpaceNotFound

			// 资源耗尽,无法启动
			case codes.ResourceExhausted:
				return nil, ErrResourceExhausted
			case codes.Unknown:
				c.logger.Errorf("rpc start space error:%v", err)
				return nil, ErrSpaceStart
			}
		}

		// 访问路径为  http://domain/ws/uid/...   ws: workspace
		// 4、将相关信息保存到redis
		host := spaceInfo.Ip + ":" + strconv.Itoa(int(spaceInfo.Port))
		err = rdis.AddRunningSpace(uid, &model.RunningSpace{
			Sid:  space.Sid,
			Host: host,
		})
		if err != nil {
			c.logger.Errorf("add pod info to redis error, err:%v", err)
			return nil, ErrSpaceStart
		}

		space.RunningStatus = model.RunningStatusRunning
	}

	// 5、修改数据库中的状态信息
	if space.Status == model.SpaceStatusUncreated {
		// 更新数据库
		err := c.dao.UpdateStatusById(space.Id, model.SpaceStatusAvailable)
		if err != nil {
			c.logger.Warnf("update space status error:%v", err)
		}
	}

	if retErr != nil {
		return nil, retErr
	}

	return space, nil
}

 

4.3 CreateAndStartWorkspace

CreateAndStartWorkspace就是CreateSpace和StartSpace的组合

// CreateAndStartWorkspace 创建并且启动云工作空间
func (c *CloudCodeService) CreateAndStartWorkspace(req *reqtype.SpaceCreateOption, userId uint32, uid string) (*model.Space, error) {
	// 1、检查是否有其它工作空间正在运行, 同时只能有一个工作空间启动
	isRunning, err := rdis.CheckHasRunningSpace(uid)
	if err != nil {
		return nil, ErrSpaceCreate
	}
	if isRunning {
		return nil, ErrOtherSpaceIsRunning
	}

	// 2、创建工作空间
	space, err := c.CreateWorkspace(req, userId)
	if err != nil {
		return nil, err
	}

	// 3、启动工作空间
	return c.startWorkspace(space, uid, c.rpc.CreateSpace)
}

 

4.4 StopWorkspace

步骤如下:

  1. 从redis中查询该工作空间是否正在运行
  2. 如果正在运行,就调用rpc来停止

代码如下:

// 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.StopSpace(context.Background(), &pb.QueryOption{
		Name:      name,
		Namespace: CloudCodeNamespace,
	})
	if err != nil {
		c.logger.Warnf("rpc delete space error:%v", err)
		return err
	}

	return nil
}

 

4.5 DeleteWorkspace

步骤如下:

  1. 检查工作空间是否正在运行,需要先停止工作空间才能删除
  2. 调用rpc删除PVC
  3. 从数据库中删除记录
// DeleteWorkspace 删除云工作空间
func (c *CloudCodeService) DeleteWorkspace(id uint32, uid string) error {
	// 1、检查该工作空间是否正在运行,如果正在运行就返回错误
	space, err := c.dao.FindSidAndStatusById(id)
	if err != nil {
		c.logger.Warnf("find sid error:%v", err)
		return err
	}
	// 从redis中查询
	isRunning, err := rdis.CheckIsRunning(space.Sid)
	if err != nil {
		c.logger.Warnf("check is running error:%v", err)
		return err
	}
	if isRunning {
		return ErrWorkSpaceIsRunning
	}

	// 2、通知controller删除该workspace关联的资源
	ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*30)
	defer cancelFunc()
	name := c.generatePodName(space.Sid, uid)
	_, err = c.rpc.DeleteSpace(ctx, &pb.QueryOption{Name: name, Namespace: CloudCodeNamespace})
	if err != nil {
		c.logger.Warnf("delete workspace err:%v", err)
		return err
	}


	// 3、从mysql中删除记录
	return c.dao.DeleteSpaceById(id)
}

 

5、前端实现

由于本人对前端并不是很熟悉,因此只是进行了简单的开发,代码:https://github.com/mangohow/cloud_ide_vue

开发过程也不在介绍,页面展示如下:

登录注册页面:

在这里插入图片描述

在这里插入图片描述

空间模板:

在这里插入图片描述

工作空间:

在这里插入图片描述

 

6、项目部署

需要部署的组件如下:

  • Mysql:用于保存用户信息,单实例部署,可以使用Deployment,使用PVC来挂载存储卷
  • Redis:存储工作空间的运行信息,单实例部署,使用Deployment,使用PVC来挂载存储卷
  • Openresty:工作空间的反向代理、页面静态资源访问、后端web反向代理,直接部署在机器上,不使用容器(更简单方便)
  • Operator:使用Deployment的方式部署,由于需要访问Apiserver,因此需要配置serviceasscount
  • Web:使用Deployment的方式部署,配置文件使用ConfigMap来保存
  • 前端:使用nginx来托管前端页面

部署文件(里面包含了部署所需的yaml以及sql文件和nginx的配置文件):https://github.com/mangohow/cloud-ide-apps-deploy

6.1 安装nfs

参考Kubernetes(K8S)学习笔记 9.2章

6.2 部署mysql、redis、operator、web

修改application.yaml:首先修改application.yaml中的email,添加你的senderEmail以及authCode,这个是注册时通过邮箱发送验证码的功能,如果没有可以打开你的qq邮箱,按下面的步骤开启
在这里插入图片描述
在这里插入图片描述
修改mysql_deploy.yaml:修改PersistentVolume中的nfs配置,需要提前创建好path中的文件夹
在这里插入图片描述
修改redis_deploy.yaml:同上面步骤
生成ConfigMap:执行下面的命令

kubectl create cm cloud-ide-web-cm --from-file=application.yaml --dry-run=client -o yaml 

然后将data部分复制到web_cm.yaml文件的data下:
在这里插入图片描述
到此,就可以直接创建出这些服务了,执行下面的命令:

kubectl apply -f .

报了下面的错没事,因为它吧我们的web配置文件当k8s的yaml执行了
在这里插入图片描述
查看部署情况:
在这里插入图片描述
已经成功部署

 

6.3 部署openresty

部署openresty可以选择使用k8s部署或者直接在机器上部署,我选择直接部署,更加方便。关于openresty的安装就不再介绍,网上很多教程,他本质就是一个nginx。
openresty安装后通常在/usr/local/openresty
修改nginx.conf,把下面的两个地址修改为cloud-ide-web-svc的地址
在这里插入图片描述
修改proxy.lua:将redis的地址修改为cloud-ide-redis-svc的地址
在这里插入图片描述
然后将配置文件和lua脚本放入nginx下,分别放入lua目录和conf目录下,重启nginx
在这里插入图片描述

6.4 前端部署

编译前端,将生成的dist文件夹中的内容放入/usr/local/openresty/nginx/html

至此所有内容都已经部署,最后一步别忘了执行sql文件来创建数据库表

 

踩坑记录:

        在K8S中运行cloud-ide-controller时需要配置role、serviceaccount和rolebinding。cloud-ide-controller运行在cloud-ide-apps命名空间下,但是其要访问的资源在cloud-ide命名空间下。当时将role、serviceAccount和rolebinding的命名空间都指定为了cloud-ide-apps或者cloud-ide,这两种都不行。第一种cloud-ide-controller一直失败重启,原因是没有对资源的操作权限;第二种,创建了deployment却不会创建pod,应该是因为sa和controller不在同一个命名空间下,因此找不到sa。正确的解决方法是:将role和rolebinding的命名空间指定为controller要操作的资源的命名空间(也就是cloud-ide),将sa的命名空间指定为controller运行的命名空间(也就是cloud-ide-apps)

​ 再配置operator的role时,一直提示没有操作Pod的权限,找了半天问题,最终发现operator_role.yaml中的resources写成了pod,正确的应该是pods,当场气死。

 

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐