当前位置: 首页 > news >正文

k8s编程operator实战之云编码平台——⑤项目完成、部署

文章目录

    • 1、效果展示
    • 2、保存用户状态和访问用户服务实现方案
      • 2.1 如何保存用户的状态
        • 2.1.1 解决保留安装的插件问题
      • 2.2 如何访问到用户在工作空间中启动的http服务
        • 2.2.1 code-server如何帮我们实现了用户程序的代理
    • 3、Operator功能实现
      • 3.1 使用KubeBuilder创建项目
        • 3.1.1 完善kind中的字段
      • 3.2 controller功能实现
      • 3.3 Grpc service实现
        • 3.3.1 proto文件的定义
        • 3.3.2 CreateSpace
        • 3.3.3 StartSpace
        • 3.3.4 StopSpace
        • 3.3.5 DeleteSpace
        • 3.3.6 其它方法
    • 4、web server
      • 4.1 CreateWorkspace
      • 4.2 StartSpace
      • 4.3 CreateAndStartWorkspace
      • 4.4 StopWorkspace
      • 4.5 DeleteWorkspace
    • 5、前端实现
    • 6、项目部署
      • 6.1Mysql部署
      • 6.2 Redis部署
      • 6.3 Openresty部署
      • 6.4 Operator部署
      • 6.5 Web部署
      • 6.6 前端部署
    • 踩坑记录:

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 SIGTERMwhile :;do# 创建用户工作空间if [ ! -d "$USER_WORKSPACE" ]; thenecho "create $USER_WORKSPACE"mkdir -p $USER_WORKSPACEfi# 第一次启动工作空间,拷贝code-server的数据if [ ! -f "/user_data/.local/share/.first_start" ]; thenecho "copy code-server"if [ ! -d "/user_data/.local/share" ]; thenmkdir -p /user_data/.local/shareficp -r /root/.local/share/code-server-bak /user_data/.local/share/code-servertouch /user_data/.local/share/.first_startfi# 启动code-servernode_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" ]; thennohup 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"fisleep 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.04WORKDIR /.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-bakRUN 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:$PATHWORKDIR /rootEXPOSE 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 stringconst (WorkSpaceStart WorkSpaceOperation = "Start"WorkSpaceStop                     = "Stop"
)type WorkSpacePhase stringconst (WorkspacePhaseRunning WorkSpacePhase = "Running"WorkspacePhaseStopped                = "Stopped"
)type WorkSpaceSpec struct {// Workspace machine specificationsCpu     string `json:"cpu,omitempty"`Memory  string `json:"memory,omitempty"`Storage string `json:"storage,omitempty"`// 硬件资源Hardware string `json:"hardware,omitempty"`// The imageImage string `json:"image,omitempty"`// Exposed portPort int32 `json:"port,omitempty"`// Volume mount pathMountPath string `json:"mountPath"`// The operation to doOperation 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 controllersvar Mode stringconst (ModeRelease = "release"ModDev      = "dev"
)// WorkSpaceReconciler reconciles a WorkSpace object
type WorkSpaceReconciler struct {client.ClientScheme *runtime.Scheme
}func (r *WorkSpaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {_ = log.FromContext(ctx)//先查询WorkSpacewp := 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}// 创建Poderr = 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,删除Podcase mv1.WorkSpaceStop://删除Poderr = 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 = phaseerr := 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.创建Podpod := 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^10pod.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不存在,创建PVCpvc, 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.Namepod.Namespace = key.Namespacectx, cancelFunc := context.WithTimeout(context.Background(), time.Second*35)defer cancelFunc()// 删除Poderr = 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.Namepvc.Namespace = key.Namespacectx, 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 controllersimport ("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.WorkSpaceexist := 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, 5sleepDuration := []time.Duration{1, 3, 5, 8, 12}po := v1.Pod{}loop:for {select {case <-ctx.Done():break loopdefault: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.WorkSpacekey := 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.WorkSpaceexist := s.checkWorkspaceExist(ctx, key, &p)if !exist {return nil}// 更新workspace的Operation字段wp.Spec.Operation = mv1.WorkSpaceStartif 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 := trueerr := retry.RetryOnConflict(retry.DefaultRetry, func() error {var wp mv1.WorkSpaceexist = s.checkWorkspaceExist(ctx, client.ObjectKey{Name: option.Name, Namespace: option.Namespace}, &wp)if !exist {return nil}// 更新workspace的Operation字段wp.Spec.Operation = mv1.WorkSpaceStopif 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.WorkSpaceexist := s.checkWorkspaceExist(ctx, client.ObjectKey{Name: option.Name, Namespace: option.Namespace}, &wp)if !exist {return EmptyResponse, nil}// 删除Workspaceif 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 = spaceIdreturn 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 = idspace.UserId = userId// 3.该工作空间是否是第一次启动startFunc := c.rpc.StartSpaceswitch space.Status {case model.SpaceStatusDeleted:return nil, ErrWorkSpaceNotExistcase model.SpaceStatusUncreated:startFunc = c.rpc.CreateSpacespec := 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() {// 创建工作空间时,工作空间已存在,修改数据库中的statuscase codes.AlreadyExists:retErr = ErrSpaceAlreadyExistbreak loop// 启动工作空间时,工作空间不存在case codes.NotFound:return nil, ErrSpaceNotFound// 资源耗尽,无法启动case codes.ResourceExhausted:return nil, ErrResourceExhaustedcase codes.Unknown:c.logger.Errorf("rpc start space error:%v", err)return nil, ErrSpaceStart}}// 访问路径为  http://domain/ws/uid/...   ws: workspace// 4、将相关信息保存到redishost := 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、停止workspacename := 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.1Mysql部署

部署mysql可以使用Deployment,同时可以配置Nodeport类型的Service以便可以在外部访问

mysql_deploy.yaml:

# pv
apiVersion: v1
kind: PersistentVolume
metadata:name: cloud-ide-mysql-pvlabels:app: mysql-pv
spec:nfs: # 存储类型,和底层正则的存储对应path: /data/nfs/cloud-ide-mysqlserver: 192.168.44.100capacity: # 存储能力,目前只支持存储空间的设置storage: 5GiaccessModes: # 访问模式- ReadWriteManypersistentVolumeReclaimPolicy: Retain # 回收策略# pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:name: cloud-ide-mysql-pvcnamespace: cloud-ide-apps
spec:selector:matchLabels:app: mysql-pvaccessModes: # 访客模式- ReadWriteManyresources: # 请求空间requests:storage: 5Gi# 定义mysql的Deployment
---
apiVersion: apps/v1
kind: Deployment
metadata:labels:app: cloud-ide-mysqlname: mysqlnamespace: cloud-ide-apps
spec:selector:matchLabels:app: cloud-ide-mysqltemplate:metadata:labels:app: cloud-ide-mysqlspec:containers:- image: mysql:8.0name: mysqlenv:- name: MYSQL_ROOT_PASSWORDvalue: "123456"ports:- containerPort: 3306volumeMounts:- name: mysqlvolumemountPath: /var/lib/mysqlvolumes:- name: mysqlvolume# 使用pvcpersistentVolumeClaim:claimName: cloud-ide-mysql-pvc#定义mysql的Service
---
apiVersion: v1
kind: Service
metadata:labels:app: cloud-ide-mysql-svcname: cloud-ide-mysql-svcnamespace: cloud-ide-apps
spec:selector:app: cloud-ide-mysqltype: NodePortports:- port: 3306protocol: TCPtargetPort: 3306nodePort: 30306

6.2 Redis部署

redis_deploy.yaml

# pv
apiVersion: v1
kind: PersistentVolume
metadata:name: cloud-ide-redis-pvlabels:app: redis-pv
spec:nfs: # 存储类型,和底层正则的存储对应path: /data/nfs/cloud-ide-redisserver: 192.168.44.100capacity: # 存储能力,目前只支持存储空间的设置storage: 5GiaccessModes: # 访问模式- ReadWriteManypersistentVolumeReclaimPolicy: Retain # 回收策略# pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:name: cloud-ide-redis-pvcnamespace: cloud-ide-apps
spec:selector:matchLabels:app: redis-pvaccessModes: # 访客模式- ReadWriteManyresources: # 请求空间requests:storage: 5Gi# 定义mysql的Deployment
---
apiVersion: apps/v1
kind: Deployment
metadata:labels:app: cloud-ide-redisname: reidsnamespace: cloud-ide-apps
spec:selector:matchLabels:app: cloud-ide-redistemplate:metadata:labels:app: cloud-ide-redisspec:containers:- image: redisname: redisports:- containerPort: 6379volumeMounts:- name: redisvolumemountPath: /datavolumes:- name: redisvolume# 使用pvcpersistentVolumeClaim:claimName: cloud-ide-redis-pvc#定义mysql的Service
---
apiVersion: v1
kind: Service
metadata:labels:app: cloud-ide-redis-svcname: cloud-ide-redis-svcnamespace: cloud-ide-apps
spec:selector:app: cloud-ide-redistype: ClusterIPports:- port: 6379protocol: TCPtargetPort: 6379

6.3 Openresty部署

Openresty的部署选择直接部署在主机上,网上有很多教程,跟安装nginx差不多,就不再介绍

部署好之后,需要修改配置文件以及添加lua脚本。相关文件在部署文件夹中可以找到。

6.4 Operator部署

Operator的镜像已经上传到了阿里云镜像仓库,需要可以直接使用

部署deploy:

使用–mode参数可以指定运行模式,如果运行在release模式下,需要集群有较高配置,否则会导致Pod由于资源不足无法启动

apiVersion: apps/v1
kind: Deployment
metadata:labels:app: cloud-ide-operator-deployname: cloud-ide-operatornamespace: cloud-ide-apps
spec:replicas: 1selector:matchLabels:app: cloud-ide-operatortemplate:metadata:labels:app: cloud-ide-operatorspec:serviceAccountName: cloud-ide-operator-sacontainers:- name: operatorimage: registry.cn-hangzhou.aliyuncs.com/mangohow-apps/cloud-ide-k8s-operator:v1.0args: ["--mode", "dev"]livenessProbe:httpGet:path: /healthzport: 8081initialDelaySeconds: 15periodSeconds: 20readinessProbe:httpGet:path: /readyzport: 8081initialDelaySeconds: 5periodSeconds: 10ports:- containerPort: 6387- containerPort: 8081# service ClusterIp
---
apiVersion: v1
kind: Service
metadata:labels:app: cloud-ide-operator-svcname: cloud-ide-operator-svcnamespace: cloud-ide-apps
spec:ports:- port: 6387protocol: TCPtargetPort: 6387selector:app: cloud-ide-operatortype: ClusterIP

operator需要访问k8s,因此需要为其配置权限:

role:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:name: cloud-ide-operator-rolenamespace: cloud-ide
rules:
- apiGroups:- ""resources:- podsverbs:- create- delete- get- list- watch
- apiGroups:- ""resources:- persistentvolumeclaimsverbs:- create- delete- get- list- watch
- apiGroups:- cloud-ide.mangohow.comresources:- workspacesverbs:- create- delete- get- list- patch- update- watch
- apiGroups:- cloud-ide.mangohow.comresources:- workspaces/finalizersverbs:- update
- apiGroups:- cloud-ide.mangohow.comresources:- workspaces/statusverbs:- get- patch- update

rolebinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:name: cloud-ide-operator-rbnamespace: cloud-ide
roleRef:apiGroup: rbac.authorization.k8s.iokind: Rolename: cloud-ide-operator-role
subjects:
- kind: ServiceAccountname: cloud-ide-operator-sanamespace: cloud-ide-apps

serviceaccount

apiVersion: v1
kind: ServiceAccount
metadata:name: cloud-ide-operator-sanamespace: cloud-ide-apps

6.5 Web部署

在部署web时需要修改配置文件中的mysql、redis以及grpc的ip地址,之后通过configmap将配置文件挂载到pod中

创建configmap

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

部署deploy

apiVersion: apps/v1
kind: Deployment
metadata:labels:app: cloud-ide-web-deployname: cloud-ide-webnamespace: cloud-ide-apps
spec:replicas: 3selector:matchLabels:app: cloud-ide-webtemplate:metadata:labels:app: cloud-ide-webspec:containers:- name: webimage: registry.cn-hangzhou.aliyuncs.com/mangohow-apps/cloud-ide-webserver:v1.0ports:- containerPort: 8088volumeMounts:- name: confmountPath: /app/confvolumes:- name: confconfigMap:name: cloud-ide-web-cm---
apiVersion: v1
kind: Service
metadata:labels:app: cloud-ide-web-svcname: cloud-ide-web-svcnamespace: cloud-ide-apps
spec:ports:- port: 8088protocol: TCPtargetPort: 8088nodePort: 30088selector:app: cloud-ide-webtype: CluserIP

6.6 前端部署

将前端文件编译后,通过nginx来部署。nginx的配置文件在部署文件夹中,适当修改即可。

 

踩坑记录:

        在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,当场气死。

相关文章:

k8s编程operator实战之云编码平台——⑤项目完成、部署

文章目录1、效果展示2、保存用户状态和访问用户服务实现方案2.1 如何保存用户的状态2.1.1 解决保留安装的插件问题2.2 如何访问到用户在工作空间中启动的http服务2.2.1 code-server如何帮我们实现了用户程序的代理3、Operator功能实现3.1 使用KubeBuilder创建项目3.1.1 完善kin…...

C语言杂记(指针篇)

指针篇 指针就是地址&#xff0c;地址就是指针 指针变量就是存放地址的变量 *号只有定义的时候表示定义指针变量&#xff0c;其他表示从地址里面取内容 通过指针的方法使main函数中的data1和data2发生数据交换。 #include <stdio.h> void chang_data(int *data1,int *da…...

ES window 系统环境下连接问题

环境问题&#xff1a;&#xff08;我采用的版本是 elasticsearch-7.9.3&#xff09;注意 开始修正之前的配置&#xff1a;前提&#xff1a;elasticsearch.yml增加或者修正一下配置&#xff1a;xpack.security.enabled: truexpack.license.self_generated.type: basicxpack.secu…...

hexo部署github搭建个人博客 完整详细带图版(更新中)

文章目录0. 前置内容1. hexo创建个人博客2. GitHub创建仓库3. hexo部署到GitHub4. 常用命令newcleangenerateserverdeploy5. 添加插件5.1 主题5.2 博客基本信息5.3 创建新的菜单5.4 添加搜索功能5.5 添加阅读时间字数提示5.6 打赏功能5.7 切换主题5.8 添加不蒜子统计5.9 添加百…...

SpringBoot集成DruidDataSource实现监控 SQL 性能

一、快速入门 1.1 基本概念 我们都使用过连接池&#xff0c;比如C3P0、DBCP、hikari、Druid&#xff0c;虽然 HikariCP 的速度稍快&#xff0c;但 Druid 能够提供强大的监控和扩展功能。Druid DataSource 是阿里巴巴开发的号称为监控而生的数据库连接池&#xff0c;它不仅可以…...

maven镜像源及代理配置

在公司使用网络一般需要设置代理&#xff0c; 我在idea中创建springboot工程时&#xff0c;发现依赖下载不了&#xff0c;原以为只要浏览器设置代理&#xff0c;其他的网络访问都会走代理&#xff0c;经过查资料设置了以下几个地方后工程创建正常&#xff0c;在此记录给大家参考…...

【Java面试篇】Spring中@Transactional注解事务失效的常见场景

文章目录Transactional注解的失效场景☁️前言&#x1f340;前置知识&#x1f341;场景一&#xff1a;Transactional应用在非 public 修饰的方法上&#x1f341;场景二&#xff1a; propagation 属性设置错误&#x1f341;场景三&#xff1a;rollbackFor属性设置错误&#x1f3…...

【C】分配内存的函数

#include <stdlib.h>//分配所需的内存空间&#xff0c;并返回一个指向它的指针。 void *malloc(size_t size);//分配所需的内存空间&#xff0c;并返回一个指向它的指针。并且calloc负责把这块内存空间用字节0填//充&#xff0c;而malloc并不负责把分配的内存空间清零 vo…...

IDEA 断点总是进入class文件没有进入源文件解决

前言 idea 断点总是进入class文件没有进入源文件解决 问题 在源文件里打了断点&#xff0c;断点模式启动时却进入了class文件里的断点&#xff0c;而没有进入到java源文件里的断点。 比如&#xff1a;我在 A.java 里打了断点&#xff0c;调试时却进入到了 jar 包里的 A.clas…...

【flink】 flink入门教程demo 初识flink

文章目录通俗解释什么是flink及其应用场景flink处理流程及核心APIflink代码快速入门flink重要概念什么是flink&#xff1f; 刚接触这个词的同学 可能会觉得比较难懂&#xff0c;网上搜教程 也是一套一套的官话&#xff0c; 如果大家熟悉stream流&#xff0c;那或许会比较好理解…...

LeetCode 1487. 保证文件名唯一

【LetMeFly】1487.保证文件名唯一 力扣题目链接&#xff1a;https://leetcode.cn/problems/making-file-names-unique/ 给你一个长度为 n 的字符串数组 names 。你将会在文件系统中创建 n 个文件夹&#xff1a;在第 i 分钟&#xff0c;新建名为 names[i] 的文件夹。 由于两个…...

详细剖析|袋鼠云数栈前端框架Antd 3.x 升级 4.x 的踩坑之路

袋鼠云数栈从2016年发布第⼀个版本开始&#xff0c;就始终坚持着以技术为核⼼、安全为底线、提效为⽬标、中台为战略的思想&#xff0c;坚定不移地⾛国产化信创路线&#xff0c;不断推进产品功能迭代、技术创新、服务细化和性能升级。 在数栈过去的产品迭代中受限于当前组件的…...

【C++PrimerPlus】第三章 处理数据

文章目录前言内容目录3.1 简单变量3.1.2 变量名3.1.2 整形3.1.3 整形short,int,long,long long3.1.4 无符号类型3.1.5 选择整形类型3.1.6 整形字面值3.1.7 C如何确定常量的类型3.1.8 char类型&#xff1a;字符和小整数3.1.9 bool类型3.2 const修饰符3.3浮点数3.3.1 书写浮点数3…...

【基础算法】单链表的OJ练习(1) # 反转链表 # 合并两个有序链表 #

文章目录前言反转链表合并两个有序链表写在最后前言 上一章讲解了单链表 -> 传送门 <- &#xff0c;后面几章就对单链表进行一些简单的题目练习&#xff0c;目的是为了更好的理解单链表的实现以及加深对某些函数接口的熟练度。 本章带来了两个题目。一是反转链表&#x…...

离散数学笔记(1)命题逻辑

文章目录1.命题符号化及联结词基本概念本节题型2.命题公式及分类基本概念本节题型1.命题符号化及联结词 基本概念 命题的定义&#xff1a;能够判断真假的陈述句称为命题。 备注&#xff1a;感叹句、疑问句、祈使句和类似于xy>5之类真值不唯一的句子都不是命题。 真值的真假…...

IDEA Android 网格布局(GridLayout)示例(计算器界面布局)

网格布局(GridLayout&#xff09; 示例程序效果&#xff08;实现类似vivo手机自带计算器UI&#xff09; 真机和模拟器运行效果&#xff1a; 简述&#xff1a; GridLayout(网格布局)和TableLayout&#xff08;表格布局&#xff09;有类似的地方&#xff0c;通俗来讲可以理解为…...

【蓝桥杯嵌入式】拓展板之数码管显示

文章目录硬件电路连接方式函数实现文章福利硬件电路 通过上述原理图&#xff0c;可知拓展板上的数码管是一个共阴数码管&#xff0c;也就是说某段数码管接上高电平时&#xff0c;就会点亮。   上述原理图还给出一个提示&#xff0c;即&#xff1a;三个数码管分别与三个74HC59…...

Web Spider案例 网洛克 第三题 AAEncode加密 练习(七)

声明 此次案例只为学习交流使用&#xff0c;抓包内容、敏感网址、数据接口均已做脱敏处理&#xff0c;切勿用于其他非法用途&#xff1b; 文章目录声明一、资源推荐二、逆向目标三、抓包分析 & 下断分析逆向3.1 抓包分析3.2 下断分析逆向拿到混淆JS代码3.3 AAEncode解决方…...

【javaScript面试题】2023前端最新版javaScript模块,高频24问

&#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;博主收集的关于javaScript的面试题 目录 一、2023javaScript面试题精选 1.js的数据类型…...

Hadoop集群启动从节点没有DataNode

一、问题背景 之前启动hadoop集群的时候都没有问题&#xff0c;今天启动hadoop集群的时候&#xff0c;从节点的DataNode没有启动起来。 二、解决思路 遇见节点起不来的情况&#xff0c;可以去看看当前节点的日志文件 我进入当前从节点的hadoop安装目录的Logs文件下去查看日…...

FIFO IP Core

FIFO IP Core 先进先出的缓存器常常被用于数据的缓存&#xff0c;或者高速异步数据交互&#xff08;跨时钟信号传递&#xff09;和RAM和ROM的区别是没有地址线&#xff0c;无法指定地址 写时钟(Write Clock Domain)&#xff0c;读时钟写复位&#xff08;wr_rst)&#xff0c;读…...

从FPGA说起的深度学习(四)

这是新的系列教程&#xff0c;在本教程中&#xff0c;我们将介绍使用 FPGA 实现深度学习的技术&#xff0c;深度学习是近年来人工智能领域的热门话题。在本教程中&#xff0c;旨在加深对深度学习和 FPGA 的理解。用 C/C 编写深度学习推理代码高级综合 (HLS) 将 C/C 代码转换为硬…...

pytorch入门7--自动求导和神经网络

深度学习网上自学学了10多天了&#xff0c;看了很多大神的课总是很快被劝退。终于&#xff0c;遇到了一位对小白友好的刘二大人&#xff0c;先附上链接&#xff0c;需要者自取&#xff1a;https://b23.tv/RHlDxbc。 下面是课程笔记。 一、自动求导 举例说明自动求导。 torch中的…...

QT 之wayland 事件处理分析基于qt5wayland5.14.2

1. Qt wayland 初始化 接收鼠标/案件&#xff0c;触摸屏等事件事件 QWaylandNativeInterface : public QPlatformNativeInterface 在QWaylandNativeInterface 继承qpa 接口类QPlatformNativeInterface; 1.1 初始化鼠标&#xff1a; void *QWaylandNativeInterface::nativeR…...

【this 和 super 的区别】

在 Java 中&#xff0c;this 和 super 都是关键字&#xff0c;表示当前对象和父类对象。 this 关键字可以用于以下几种情况&#xff1a; 引用当前对象的成员变量&#xff0c;方法和构造方法&#xff0c;用于区分局部变量和成员变量重名的情况&#xff1b; 调用当前类的另外一…...

K8s:Monokle Desktop 一个集Yaml资源编写、项目管理、集群管理的 K8s IDE

写在前面 Monokle Desktop 是 kubeshop 推出的一个开源的 K8s IDE相关项目还有 Monokle CLI 和 Monokle Cloud相比其他的工具&#xff0c;Monokle Desktop 功能较全面&#xff0c;涉及 k8s 管理的整个生命周期博文内容&#xff1a;Monokle Desktop 下载安装&#xff0c;项目管理…...

自动化测试实战篇(8),jmeter并发测试登录接口,模拟从100到1000个用户同时登录测试服务器压力

首先进行使用jmeter进行并发测试之前就需要搞清楚线程和进程的区别还需要理解什么是并发、高并发、并行。还需要理解高并发中的以及老生常谈的&#xff0c;TCP三次握手协议和TCP四次握手协议**TCP三次握手协议指&#xff1a;****TCP四次挥手协议&#xff1a;**进入Jmeter&#…...

ATTCK v12版本战术实战研究—持久化(二)

一、前言前几期文章中&#xff0c;我们介绍了ATT&CK中侦察、资源开发、初始访问、执行战术、持久化战术的知识。那么从前文中介绍的相关持久化子技术来开展测试&#xff0c;进行更深一步的分析。本文主要内容是介绍攻击者在运用持久化子技术时&#xff0c;在相关的资产服务…...

python函数式编程

1 callable内建函数判断一个名字是否为一个可调用函数 >>> import math >>> x 1 >>> y math.sqrt >>> callable(x) False >>> callable(y) True 2 记录函数&#xff08;文档字符串&#xff09; >>> def square(x): …...

3.linux下安装mysql

1.安装前的环境准备 查看是否安装过mysql 首先检测Linux操作系统中是否安装了MySQL&#xff1a; # rpm -qa | grep -i mysql 卸载安装包 如果有信息出现&#xff0c;则进行删除&#xff0c;命令如下&#xff1a; # rpm -e --nodeps 包名 删除老版本mysql的开发头文件和…...

北京网站建设公司电话/网络营销的特点和优势

进程间通信一、管道创建管道父子进程的管道单向通信父子间的双向通信管道Shell中的管道通信匿名管道与命名管道管道特点二、消息队列不足三、共享内存四、信号量五、信号六、Socket创建Socket的系统调用通信方式TCP协议通信的Socket编程模型UDP协议通信的Socket编程模型本地进程…...

做盘石信用认证网站/网络销售推广公司

每日签到的功能&#xff0c;供大家参考&#xff0c;具体内容如下首次签到获得1个积分,第二次签到获得2个积分,第三次签到获得3个积分,以此类推但是签到必须每天连续积分才可以递增,如果有中断再次签到时获得积分仍然从1开始递增;user: id,username,count,point,sign_timesign.h…...

阜阳企业网站推广/快速网站轻松排名

【编者的话】曾经听到不少运维管理人员抱怨&#xff0c;Mesos何时可以为同一集群中的每套容器系统提供不同的IP地址&#xff1f;众所周知&#xff0c;在网络架构领域&#xff0c;没有哪种方案能够一劳永逸地适应全部具体场景。然而&#xff0c;Mesos 0.23.0 版本中做出大胆实践…...

汕头市门户网站建设/关键词查询工具有哪些

netstat -ntpl kill -9 PID...

可信的昆明网站建设/东莞市优速网络科技有限公司

pyltp的简介 语言技术平台(LTP)经过哈工大社会计算与信息检索研究中心 11 年的持续研发和推广&#xff0c; 是国内外最具影响力的中文处理基础平台。它提供的功能包括中文分词、词性标注、命名实体识别、依存句法分析、语义角色标注等。 pyltp 是 LTP 的 Python 封装&#xff0…...

金融类网站建设/廊坊seo优化排名

智慧民生围绕交通、医疗、教育、居住等市民最关心的、最直接的、最现实的热点问题&#xff0c;整合构建面向各类人群的信息化公共服务体系&#xff0c;实现信息化公共服务体系的普及化和无障碍化&#xff1b;充分释放信息化在持续提升交通、医卫、教育、社区等社会民生服务能力…...