CSI学习笔记
从1.8版本开始,Kubernetes Storage SIG开始停止接受in-tree卷插件,并建议所有供应商实现out-of-tree卷插件。CSI是两种out-of-tree的K8S卷扩展机制之一,另外一个是Flexvolume。
Flexvolume的工作方式是,由名为flexvolume的in-tree插件来调用用户提供的卷插件。你需要事先把卷插件拷贝到所有K8S节点上,还要处理好卷插件升级的问题。
CSI出现更晚,也是未来的发展方向。它通过单一的接口描述存储供应商需要实现的逻辑,以保证它们提供的块、文件存储能够在容器工作负载上使用。CSI从设计上不和K8S绑定,期望能在任何容器编排系统中运行。
在K8S的1.13版本,它的CSI实现到达GA,兼容CSI v1.0和v0.3版本。
查看:https://kubernetes-csi.github.io/docs/drivers.html,其中列出了知名的CSI驱动实现。
- 独立于K8S版本进行演变,包括开发、测试、维护
- 不会影响K8S核心组件的运行
- 不会因为和K8S核心组件具有相同的特权,而导致安全隐患
- 不需要遵循K8S项目本身的开发规范
- 部署简单,不需要宿主机的root权限
- attach/mount时常常依赖于第三方工具,例如ceph需要rbd
整体架构上,CSI和FlexVolume没有太大区别,只是FlexVolume是通过命令行调用,而CSI则是通过gRPC调用。
上图中:
- Attach/Detach控制器:负责将卷附到节点,或者执行反向操作。例如将Ceph的RBD卷映射为某个节点上的块设备
- PV/PVC控制器:负责面向用户的PV/PVC接口
- Volume Manager:运行在Kubelet中,让卷对于Pod就绪
- Volume Plugins:插件化机制,对接各种存储解决方案
卷的整个生命周期,会牵涉到6个流程:
- Provision/Delete:PV控制器会监听API Server中的资源更新,主要监听PV / PVC / SC三类资源。当发现这些资源的CRU操作时,PV控制器会判断是否需要创建、删除、绑定(PV到PVC)、回收卷
- Attach/Detach:由AttachDetach控制器、节点上的VolumeManager负责
- AttachDetach控制器监听Pod/Node事件,如有必要,调用Volume Plugin执行Attach/Detach操作
- VolumeManager也能够触发Attach/Detach操作,但是它监听的是调度到本节点的Pod,判断是否需要调用Volume Plugin执行Attach/Detach操作
- Mount/Umount:将卷挂载给Pod
最初,只有VolumeManager能够Attach/Detach,但是节点宕掉后,它已经挂载的卷需要在其它节点进行Detach,然后再Attach,这个必须依赖外部才能完成,AttachDetach控制器因而产生。
现在,到底在何处进行Attach/Detach,取决于Volume Plugin,如果实现了Attach相关接口,则在AttachDetach中执行,否则,在Volume Manager上执行。
FlexVolume支持后面4个流程,Provision/Delete需要开发外置的Provisioner来实现。
如上图所述,在K8S核心存储之外,CSI引入两组外部组件:
- K8S团队提供:
- Node Driver registrar :一个Sidecar,向K8S注册CSI驱动
- External provisioner :一个Sidecar,监控PVC,调用匹配的CSI驱动的卷创建、删除接口
- External attacher:一个Sidecar,监控VolumeAttachment,调用CSI驱动相应的接口
- 第三方厂商提供:
- CSI Identity:提供插件信息、能力、探测插件状态
- CSI Controller:负责创建和管理卷
- CSI Node:在节点上完成和卷相关的功能,例如Publish/Unpublish、Stage/Unstage
Kubelet和CSI驱动的交互方式:
- Kubelet直接通过UDS,向CSI发起NodeStageVolume、NodePublishVolume等调用
- Kubelet通过插件注册机制来发现CSI驱动及其UDS套接字。这意味着CSI插件必须在任何节点上进行Kubelet插件注册
Master和CSI驱动的交互方式:
- K8S控制平面不直接和CSI驱动交互
- K8S控制平面组件仅仅通过K8S API进行相互交互
- CSI驱动必须监控相关K8S API资源,并触发对应的CSI操作,例如卷创建、Attach、快照生成…
细节参考:https://github.com/container-storage-interface/spec/blob/master/spec.md
术语 | 说明 |
Volume | 通过CSI来让CO管理的容器能够使用的存储单元 |
Block Volume | 在容器里呈现为块设备的卷 |
Mounted Volume | 利用某种文件系统挂载到容器的,表现为目录的卷 |
CO | 容器编排系统,使用CSI的RPC接口和CSI插件交互 |
SP | 存储提供商,即CSI插件的实现的提供者 |
RPC | 远程过程调用 |
Node | 用户工作负载在其上运行的节点,在CSI插件的视角,以节点ID唯一标识 |
Plugin | CSI插件,本质上是实现了CSI服务的gRPC端点 |
Plugin Supervisor | 管理CSI插件的进程,可以是CO |
Workload | 可以被CO调度的原子工作单元,可能是一个容器,或容器组 |
CSI规范的主要关注点是CO和Plugin之间的协议。CO应该同时支持中心化部署、headless部署的Plugin。几种可能的部署架构:
- 插件运行在所有节点上,在CO的Master上运行中心化的控制器,所有节点上运行Node Plugin:
12345678910111213141516171819CO "Master" Host+-------------------------------------------+| || +------------+ +------------+ || | CO | gRPC | Controller | || | +-----------> Plugin | || +------------+ +------------+ || |+-------------------------------------------+CO "Node" Host(s)+-------------------------------------------+| || +------------+ +------------+ || | CO | gRPC | Node | || | +-----------> Plugin | || +------------+ +------------+ || |+-------------------------------------------+ - Headless部署,在所有CO节点上运行Plugin,Controller Plugin、Node Plugin分开部署:
123456789101112131415CO "Node" Host(s)+-------------------------------------------+| || +------------+ +------------+ || | CO | gRPC | Controller | || | +--+--------> Plugin | || +------------+ | +------------+ || | || | || | +------------+ || | | Node | || +--------> Plugin | || +------------+ || |+-------------------------------------------+ - Headless部署,在所有CO节点上运行Plugin,Controller Plugin、Node Plugin合并部署:
12345678910CO "Node" Host(s)+-------------------------------------------+| || +------------+ +------------+ || | CO | gRPC | Controller | || | +-----------> Node | || +------------+ | Plugin | || +------------+ || |+-------------------------------------------+ - Headless部署,在所有CO节点上运行Plugin,只运行Node Plugin。GetPluginCapabilities调用不会报告CONTROLLER_SERVICE特性:
123456789CO "Node" Host(s)+-------------------------------------------+| || +------------+ +------------+ || | CO | gRPC | Node | || | +-----------> Plugin | || +------------+ +------------+ || |+-------------------------------------------+
状态说明:
状态 | 说明 |
CREATED | 卷已经创建,包括在底层存储中的对应物 |
NODE_READY | 卷已经被Attach到某个节点上 |
PUBLISHED | 卷已经挂载给容器 |
VOL_READY | 卷已经在节点上可用(deviceMount,在节点上进行了卷的全局性挂载),但是没有(bind)挂载给容器 |
一个动态提供的卷,从创建到销毁的状态流转:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
CreateVolume +------------+ DeleteVolume +------------->| CREATED +--------------+ | +---+----^---+ | | Controller | | Controller v +++ Publish | | Unpublish +++ |X| Volume | | Volume | | +-+ +---v----+---+ +-+ | NODE_READY | +---+----^---+ Node | | Node Publish | | Unpublish Volume | | Volume +---v----+---+ | PUBLISHED | +------------+ |
如果Node Plugin支持STAGE_UNSTAGE_VOLUME特性,则创建到销毁的状态流转:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
CreateVolume +------------+ DeleteVolume +------------->| CREATED +--------------+ | +---+----^---+ | | Controller | | Controller v +++ Publish | | Unpublish +++ |X| Volume | | Volume | | +-+ +---v----+---+ +-+ | NODE_READY | +---+----^---+ Node | | Node Stage | | Unstage Volume | | Volume +---v----+---+ | VOL_READY | +---+----^---+ Node | | Node Publish | | Unpublish Volume | | Volume +---v----+---+ | PUBLISHED | +------------+ |
预分配的卷,控制器将其发布到节点(ControllerPublishVolume),然后发布给容器(NodePublishVolume)的状态流转:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Controller Controller Publish Unpublish Volume +------------+ Volume +------------->+ NODE_READY +--------------+ | +---+----^---+ | | Node | | Node v +++ Publish | | Unpublish +++ |X| <-+ Volume | | Volume | | +++ | +---v----+---+ +-+ | | | PUBLISHED | | | +------------+ +----+ Validate Volume Capabilities |
通过capabilities API可以禁用掉某些生命周期状态。和这种卷的交互仅剩下NodePublishVolume/NodeUnpublishVolume:
1 2 3 4 5 6 7 8 9 10 |
+-+ +-+ |X| | | +++ +^+ | | Node | | Node Publish | | Unpublish Volume | | Volume +---v----+---+ | PUBLISHED | +------------+ |
CO和Plugin之间的接口,使用gRPC为载体。每个SP都需要提供:
- Node Plugin:对于任何SP提供的卷,想要Publish到的节点,都要运行此插件
- Controller Plugin:可以运行在任何地方,实现创建卷、创建快照等功能。可选
某些部署架构下,一个gRPC端点(可能由一个二进制文件提供)可以提供上述两组服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
syntax = "proto3"; package csi.v1; import "google/protobuf/descriptor.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; option go_package = "csi"; extend google.protobuf.EnumOptions { // 提示枚举是可选的,并且是试验API的一部分,未来可能废弃 bool alpha_enum = 1060; } extend google.protobuf.EnumValueOptions { // 提示枚举值是可选的,并且是试验API的一部分,未来可能废弃 bool alpha_enum_value = 1060; } extend google.protobuf.FieldOptions { // 提示某个字段可能包含敏感信息,应当小心处理,例如不要用日志记录 bool csi_secret = 1059; // 提示字段是可选的,并且是试验API的一部分,未来可能废弃 bool alpha_field = 1060; } extend google.protobuf.MessageOptions { // 提示消息是可选的,并且是试验API的一部分,未来可能废弃 bool alpha_message = 1060; } extend google.protobuf.MethodOptions { // 提示方法是可选的,并且是试验API的一部分,未来可能废弃 bool alpha_method = 1060; } extend google.protobuf.ServiceOptions { // 提示服务是可选的,并且是试验API的一部分,未来可能废弃 bool alpha_service = 1060; } |
1 2 3 4 5 6 7 8 9 10 11 |
// 让调用者(K8S组件、CSI Sidecar容器)能识别驱动,知晓它具有哪些可选特性 service Identity { rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse) {} rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse) {} rpc Probe (ProbeRequest) returns (ProbeResponse) {} } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
service Controller { rpc CreateVolume (CreateVolumeRequest) returns (CreateVolumeResponse) {} rpc DeleteVolume (DeleteVolumeRequest) returns (DeleteVolumeResponse) {} rpc ControllerPublishVolume (ControllerPublishVolumeRequest) returns (ControllerPublishVolumeResponse) {} rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest) returns (ControllerUnpublishVolumeResponse) {} rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest) returns (ValidateVolumeCapabilitiesResponse) {} rpc ListVolumes (ListVolumesRequest) returns (ListVolumesResponse) {} rpc GetCapacity (GetCapacityRequest) returns (GetCapacityResponse) {} rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest) returns (ControllerGetCapabilitiesResponse) {} rpc CreateSnapshot (CreateSnapshotRequest) returns (CreateSnapshotResponse) {} rpc DeleteSnapshot (DeleteSnapshotRequest) returns (DeleteSnapshotResponse) {} rpc ListSnapshots (ListSnapshotsRequest) returns (ListSnapshotsResponse) {} rpc ControllerExpandVolume (ControllerExpandVolumeRequest) returns (ControllerExpandVolumeResponse) {} rpc ControllerGetVolume (ControllerGetVolumeRequest) returns (ControllerGetVolumeResponse) { option (alpha_method) = true; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
service Node { rpc NodeStageVolume (NodeStageVolumeRequest) returns (NodeStageVolumeResponse) {} rpc NodeUnstageVolume (NodeUnstageVolumeRequest) returns (NodeUnstageVolumeResponse) {} rpc NodePublishVolume (NodePublishVolumeRequest) returns (NodePublishVolumeResponse) {} rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest) returns (NodeUnpublishVolumeResponse) {} rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest) returns (NodeGetVolumeStatsResponse) {} rpc NodeExpandVolume(NodeExpandVolumeRequest) returns (NodeExpandVolumeResponse) {} rpc NodeGetCapabilities (NodeGetCapabilitiesRequest) returns (NodeGetCapabilitiesResponse) {} rpc NodeGetInfo (NodeGetInfoRequest) returns (NodeGetInfoResponse) {} } |
通常情况下,由CO保证,针对同一个卷,在任何时刻,不会有大于1个的in-flight调用。
但是,如果CO崩溃,它会丢失状态信息,导致可能重复发送请求。Plugin应当尽可能优雅的处理这种场景,有可能应当返回ABORTED提示CO。
字符串最大128字节。map<string,string>最大4KiB。
规范中所有RPC接口都允许超时、在超时后可以重试。等待时间、超时重试间隔,由CO自行决定。
Plugin实现必须注意满足幂等要求。
取消一个调用的唯一方式,是执行一个反向操作,例如发起ControllerUnpublishVolume可以取消ControllerPublishVolume。
Plugin必须返回标准的gRPC状态码。状态code必须包含规范错误码。CO需要处理所有有效的错误码。
错误情形和对应gRPC代码如下:
例外条件 | gRPC代码 | 描述 |
缺少必须字段 | 3 INVALID_ARGUMENT | 提示请求中缺失必须字段,更多信息查看status.message字段 |
无效或不支持的字段 | 3 INVALID_ARGUMENT | 一个或多个字段不被插件支持,或者用法不对 |
访问被拒绝 | 7 PERMISSION_DENIED | 插件能够识别请求者的身份,但是此身份没有访问资格 |
对于目标卷存在未决的操作 | 10 ABORTED | 提示当前存在一个针对卷的、尚未完成的操作。通常CO会保证在同一时刻不会存在大于1个的、针对同一个卷的in-fight请求 |
RPC接口没实现 | 12 UNIMPLEMENTED | 插件没有实现接口,或者在当前配置下被禁用 |
身份验证失败 | 16 UNAUTHENTICATED | RPC请求没有携带有效的secret |
你可以通过标准的K8S API来使用CSI卷,包括PersistentVolumeClaim、PersistentVolume、StorageClass。
你可能需要开启Kubelet的标记 --allow-privileged=true,因为大部分CSI插件需要双向挂载传播,者有特权Pod才能做到这一点。
使用StorageClass,示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: fast-storage # CSI卷插件名字 provisioner: csi-driver.example.com parameters: type: pd-ssd # 保留前缀 # 仅仅支持预定义的若干Key,其它的被忽略,不会传递给CSI驱动 csi.storage.k8s.io/provisioner-secret-name: mysecret csi.storage.k8s.io/provisioner-secret-namespace: mynamespace |
然后在你的PVC中引用此StorageClass:
1 2 3 4 5 6 7 8 9 10 11 |
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-request-for-storage spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi storageClassName: fast-storage |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
apiVersion: v1 kind: PersistentVolume metadata: name: my-manually-created-pv spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain csi: # CSI驱动 driver: csi-driver.example.com # 底层卷名称 volumeHandle: existingVolumeName readOnly: false fsType: ext4 volumeAttributes: foo: bar controllerPublishSecretRef: name: mysecret1 namespace: mynamespace nodeStageSecretRef: name: mysecret2 namespace: mynamespace nodePublishSecretRef name: mysecret3 namespace: mynamespace |
细节参考:https://kubernetes-csi.github.io/docs/developing.html
K8S开发团队推荐了一套开发、部署、测试CSI驱动的途径,目标是减少样板代码、简化总体开发流程。该机制依赖于以下组件:
要基于推荐途径,实现一个CSI驱动,开发者需要:
- 创建一个容器化的应用程序,实现Identity、Node接口,可选的,实现Controller接口
- 基于csi-sanity进行单元测试
- 定义K8S编排配置,部署CSI驱动容器 + 适当的(K8S官方维护的)Sidecar容器
- 部署到K8S,进行端到端测试
开发CSI驱动的第一步是编写CSI规范中要求的gRPC服务。你至少需要实现:
- Identity:让调用者(K8S组件、CSI Sidecar容器)能识别驱动,知晓它具有哪些可选特性
- Node:必须实现NodePublishVolume、NodeUnpublishVolume、NodeGetCapabilities。这些方法能够将卷在节点的特定路径可用
CSI驱动不需要实现CSI规范的全集,你需要为驱动指定capabilities,声明它支持哪些CSI特性。
CSI的capabilities用于描述驱动支持的额外特性(对应CSI规范中的方法):
特性 | 说明 |
CONTROLLER_SERVICE | PluginCapability,整个Controller组件都是可选的,此特性提示CSI驱动支持Controller中的一个或多个方法 |
VOLUME_ACCESSIBILITY_CONSTRAINTS |
PluginCapability,提示该CSI插件生成的卷,不一定在任何节点上,都可以完全相同的访问。驱动可以返回额外的拓扑相关的信息给K8S,K8S可以利用这些信息:
|
VolumeExpansion | PluginCapability,提示CSI插件支持resize创建好的卷 |
CREATE_DELETE_VOLUME | ControllerServiceCapability,提示CSI插件支持动态的创建(Provision)和删除(Delete)卷 |
PUBLISH_UNPUBLISH_VOLUME |
ControllerServiceCapability,提示CSI插件支持ControllerPublishVolume、ControllerUnpublishVolume操作,这两个方法对应K8S卷的Attach/Detach操作 举例来说,这些操作可能将RBD卷映射为某个节点上的块设备 |
CREATE_DELETE_SNAPSHOT | ControllerServiceCapability。提示CSI插件支持创建快照,并且支持基于快照创建新的卷 |
CLONE_VOLUME | ControllerServiceCapability。提示CSI插件支持克隆卷 |
STAGE_UNSTAGE_VOLUME |
NodeServiceCapability。提示CSI驱动实现了NodeStageVolume、NodeUnstageVolume操作,这两个方法对应K8S卷设备的mount/umount操作 举例来说,这些操作可能将卷在节点上进行全局性的挂载 |
K8S API中包含两个CSI相关的对象。
该对象有两方面的价值:
- 简化CSI驱动的发现:你可以在安装CSI驱动时附带一个CSIDriver对象,这样K8S就可以轻易的发现此驱动(通过kubectl get csidriver)
- 定制K8S的行为:K8S具有一套和CSI驱动交互的默认规则,例如默认它会调用Attach/Detach操作。使用CSIDriver对象可以定制这一行为
对象示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
apiVersion: storage.k8s.io/v1 kind: CSIDriver metadata: # 和CSI驱动的全名一致 name: mycsidriver.example.com spec: # 提示K8S,此驱动需要Attach操作(因为驱动实现了ControllerPublishVolume方法), # 并且需要在Attach之后等待操作完成,然后再进行后续的Mount操作 # 默认值true attachRequired: true # 提示K8S,在挂载阶段,此驱动需要Pod的信息(名称、Pod的UID等) # Pod信息会在NodePublishVolume调用中作为volume_context传递: # "csi.storage.k8s.io/pod.name": pod.Name # "csi.storage.k8s.io/pod.namespace": pod.Namespace # "csi.storage.k8s.io/pod.uid": string(pod.UID) # "csi.storage.k8s.io/serviceAccount.name": pod.Spec.ServiceAccountName podInfoOnMount: true # 1.16中添加,到1.18为止beta # 此驱动支持的Volume Mode volumeLifecycleModes: - Persistent # 默认,常规PV/PVC机制 - Ephemeral # 内联临时存储(inline ephemeral volumes) |
列出所有CSI Driver:
1 2 3 |
kubectl get csidrivers.storage.k8s.io # NAME CREATED AT # hostpath.csi.k8s.io 2019-09-13T09:58:43Z |
CSI驱动产生的、节点相关的信息,存放在此对象中(而非Node对象)。价值:
- 映射K8S节点名称到CSI节点名称:CSI的GetNodeInfo调用返回的name,是存储系统引用节点使用的名字。在后续的ControllerPublishVolume调用中K8S使用该名字引用节点
- 为kubelet提供和kube-controller-manager、kube-scheduler交互的机制,不管CSI插件是否可用(注册到节点)
- 卷拓扑:CSI的GetNodeInfo调用会返回一系列标签(键值对),来识别节点的拓扑信息。K8S使用这些信息进行拓扑感知的卷创建。这些键值对存放在Node对象中,Kubelet会把键存放在CSINode中,供后续引用
对象示例:
1 2 3 4 5 6 7 8 9 |
apiVersion: storage.k8s.io/v1 kind: CSINode metadata: name: node1 spec: drivers: - name: mycsidriver.example.com nodeID: storageNodeID1 topologyKeys: ['mycsidriver.example.com/regions', "mycsidriver.example.com/zones"] |
将你的CSI驱动二进制程序,和不同的Sidecar一起编排,然后部署在node/controller模式下,实现不同的角色。
这些Sidecar由K8S官方维护,将通用的、样板逻辑抽取出来,降低开发CSI驱动的负担。
监控K8S API中的VolumeAttachment对象并进行处理(调用CSI驱动对应方法),VolumeAttachment对象表示一个将卷Attach/Detach到某个节点的期望。
监控PersistentVolumeClaim,调用CSI驱动的CreateVolume以创建新的卷。此Sidecar还负责删除卷、从快照创建卷、复制现有的卷。
一个新的PersistentVolumeClaim对象会触发卷的创建流程,如果PVC关联了SC,那么SC的provisioner字段指明谁负责实际的创建工作。
如果CSI驱动的Identity.GetPluginInfo返回的name,和上述provisioner匹配,则该CSI驱动负责创建,这一逻辑已经由external-provisioner实现好。
当卷创建好了,该Sidecar会创建一个PersistentVolume对象,代表新创建的卷。
当PVC删除后,它绑定的PersistentVolume的回收策略如果是delete,则该Sidecar会调用CSI驱动的DeleteVolume,清理掉卷。该Sidecar也会同时删除掉PersistentVolume对象。
在卷创建好后,还可以自动从数据源填充数据进去,参考VolumeContentSource。
external-provisioner支持Snapshot数据源。external-provisioner会通过SnapshotContent对象获取快照的信息,在CreateVolume调用中提供数据源字段,CSI驱动应该识别此字段,并且调用底层存储的接口,基于快照创建卷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: restore-pvc spec: storageClassName: csi-hostpath-sc # 数据源 dataSource: name: new-snapshot-test kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: - ReadWriteOnce resources: requests: storage: 10Gi |
类似的, 将数据源类型设置为PersistentVolumeClaim,可以克隆现有的PVC对应的卷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: cloned-pvc spec: storageClassName: my-csi-plugin dataSource: name: existing-src-pvc-name kind: PersistentVolumeClaim accessModes: - ReadWriteOnce resources: requests: storage: 10Gi |
在创建新卷时,external-provisioner会设置CreateVolumeRequest的 map<string, string> parameters字段,将它正在处理PVC的SC的parameters键值对存放到该parameters字段中。以 csi.storage.k8s.io/前缀的键是保留的,以此前缀开头的键值对,不作为opaque参数传递(也就是说会引发external-provisioner的特殊行为):
csi.storage.k8s.io/provisioner-secret-name
csi.storage.k8s.io/provisioner-secret-namespace
csi.storage.k8s.io/controller-publish-secret-name
csi.storage.k8s.io/controller-publish-secret-namespace
csi.storage.k8s.io/node-stage-secret-name
csi.storage.k8s.io/node-stage-secret-namespace
csi.storage.k8s.io/node-publish-secret-name
csi.storage.k8s.io/node-publish-secret-namespace
csi.storage.k8s.io/fstype
示例SC:
1 2 3 4 5 6 7 8 9 10 |
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: gold-example-storage provisioner: exampledriver.example.com parameters: disk-type: ssd csi.storage.k8s.io/fstype: ext4 csi.storage.k8s.io/provisioner-secret-name: mysecret csi.storage.k8s.io/provisioner-secret-namespace: mynamespace |
监控PersistentVolumeClaim的变更,如果容量变大,则触发CSI驱动的ControllerExpandVolume操作,增加底层卷大尺寸。
在1.17中卷快照功能到达Beta,原先的CSI external-snapshotter sidecar controller分裂为两个组件:
- snapshot-controller
- CSI external-snapshotter sidecar
snapshot-controller会监控VolumeSnapshot、VolumeSnapshotContent对象。它会创建VolumeSnapshotContent对象,此对象会触发sidecar进行快照相关操作。
snapshot-controller需要独立于CSI驱动部署,你需要安装CRD和控制器本身:
1 2 3 4 5 6 7 8 9 10 |
# 安装CRD kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-2.0/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-2.0/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-2.0/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml # RBAC kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-2.0/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml # 安装控制器 kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-2.0/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml |
external-snapshotter仅监控VolumeSnapshotContent,并调用CSI驱动的CreateSnapshot、DeleteSnapshot、ListSnapshots方法。
此sidecar探测CSI驱动的健康状况,并通过Liveness Probe机制上报给K8S,这样K8S可以探测到CSI驱动的故障,并尝试重启解决。
通过调用NodeGetInfo获取CSI驱动的信息,然后基于kubelet的插件注册机制,来注册到本地的kubelet。
已经在1.16废弃。
此Sidecar的目的是,自动注册包含CSI驱动信息的CSIDriver对象到K8S。由于已经废弃,CSI驱动提供商需要在安装清单中添加一个CSIDriver对象。
如果CSI驱动需要Secret信息(例如账号密码),而且此信息是驱动级别的(而非卷级别),应该使用K8S标准的密钥分发机制(卷挂载),直接注入到CSI驱动的Pod。
如果需要针对单个CSI操作、单个卷、单个存储池提供Secret,则可以通过CreateVolumeRequest、ControllerPublishVolumeRequest等操作的参数传递Secret。集群管理员需要创建相应的Secret对象,并在StorageClass、SnapshotClass对象中提供具有特定名字的parameters。
external-provisioner能够自动处理:
CreateVolumeRequest
DeleteVolumeRequest
ControllerPublishVolumeRequest
ControllerUnpublishVolumeRequest
ControllerExpandVolumeRequest
NodeStageVolumeRequest
NodePublishVolumeRequest
这些操作需要的Secret信息,它能够识别StorageClass.parameters中的以下名字:
csi.storage.k8s.io/provisioner-secret-name
csi.storage.k8s.io/provisioner-secret-namespace
csi.storage.k8s.io/controller-publish-secret-name
csi.storage.k8s.io/controller-publish-secret-namespace
csi.storage.k8s.io/node-stage-secret-name
csi.storage.k8s.io/node-stage-secret-namespace
csi.storage.k8s.io/node-publish-secret-name
csi.storage.k8s.io/node-publish-secret-namespace
csi.storage.k8s.io/controller-expand-secret-name
csi.storage.k8s.io/controller-expand-secret-namespace
示例一,Provision需要的通用Secret:
1 2 3 4 5 6 7 8 9 |
kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: fast-storage provisioner: csi-driver.team.example.com parameters: type: pd-ssd csi.storage.k8s.io/provisioner-secret-name: fast-storage-provision-key csi.storage.k8s.io/provisioner-secret-namespace: pd-ssd-credentials |
示例二,Per-volume Secret:
1 2 3 4 5 6 7 8 9 |
kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: fast-storage provisioner: csi-driver.team.example.com parameters: type: pd-ssd csi.storage.k8s.io/node-publish-secret-name: ${pvc.annotations['team.example.com/key']} csi.storage.k8s.io/node-publish-secret-namespace: ${pvc.namespace} |
某些存储系统,会暴露不是对任何K8S节点“等同可访问”的卷,也就是说,某些卷可能仅仅允许部分节点访问。集群中的节点可能被分段到rack、zone、region之类的组中,并且限定某些卷仅能够某些分组使用。
要启用该特性,需要设置external-provisioner的 --feature-gates=Topology=true参数。
为了让K8S这样的CO能够与上面的存储系统协作,CSI规范支持:
- 允许CSI驱动“不透明的”(opaquely)指定特定节点所在位置,例如节点A位于zone 1
- 允许K8S(的用户或组件)影响卷在何处创建,例如在zone 1或zone 2中创建卷
- 允许CSI驱动“不透明的”指定卷在何处,例如volume 1在zone 1或zone2的所有节点可用
K8S能够使用CSI拓扑进行智能调度。external-provisioner也能够使用CSI拓扑来确定Provision决策。
要在你的CSI驱动中实现拓扑特性,需要:
- PluginCapability需要支持VOLUME_ACCESSIBILITY_CONTRAINTS
- 驱动必须在NodeGetInfoResponse中设置好accessible_topology,这个信息会被K8S用来产生CSINode对象、为Node对象添加拓扑相关标签。
- 在CreateVolume时,拓扑信息会通过CreateVolumeRequest.accessibility_requirements传入,你的CSI驱动需要正确处理
在StorageClass对象中,volumeBindingMode可以取值:
- Immediate,则external-provisioner会传入集群中所有可用的拓扑信息
- WaitForFirstConsumer,则external-provisioner会等待调度器选择一个节点,该节点的拓扑信息会被设置为CreateVolumeRequest.accessibility_requirements.preferred的第一个元素。所有其它拓扑信息仍然包含在requisite、preferred字段中,以支持那些跨越多个拓扑的存储系统
(原始)块卷就是在容器中呈现为块设备的卷,而挂载卷(Mounted volume)则以特定类型的文件系统,挂载为容器中的一个目录。
CSI没有提供一个针对原始块卷的capability,CO会传递一个块卷创建请求给CSI驱动,如果你的驱动不支持创建快卷,则可以返回gRCP错误码InvalidArgument。
可以通过卷创建请求的VolumeCapabilities字段字段识别,K8S要你创建的是块卷还是挂载卷。该字段是一个数组,你创建的卷必须满足数组中所有元素指明的特性。如果一个VolumeCapability.AccessType == VolumeCapability_Block,则意味着请求的是块卷。
某些卷驱动,例如NFS,没有Attach(对应ControllerPublishVolume)的概念,但是K8S总是会执行Attach/Detach操作,即使CSI驱动没有实现相应的接口。
如果不经任何处理,调用必然会出错。一个变通方案是使用external-attacher,它能够直接向K8S响应以noop。这个方案的缺点是多了不必要的一层,而且还需要部署sidecar。
更好的方式是,使用CSIDriver对象,声明K8S应当和CSI驱动进行交互的方式:
1 2 3 4 5 6 7 |
apiVersion: storage.k8s.io/v1 kind: CSIDriver metadata: name: testcsidriver.example.com spec: # 跳过所有Attach/Detach操作 attachRequired: false |
此对象应该包含在你的CSI安装包的清单文件中。
CSI避免在规范中添加K8S特有的信息,以保持中立。
但是某些情况下,CSI驱动需要知道关于工作负载的信息(是哪个Pod需要使用卷),要获得这些信息,需要配置CSIDriver对象:
1 2 3 4 5 6 7 |
apiVersion: storage.k8s.io/v1 kind: CSIDriver metadata: name: testcsidriver.example.com spec: # 在挂载阶段,把Pod信息存放到NodePublishVolumeRequest.volume_context中 podInfoOnMount: true |
NodePublishVolumeRequest.volume_context中增加的字段包括:
csi.storage.k8s.io/pod.name: {pod.Name}
csi.storage.k8s.io/pod.namespace: {pod.Namespace}
csi.storage.k8s.io/pod.uid: {pod.UID}
csi.storage.k8s.io/serviceAccount.name: {pod.Spec.ServiceAccountName}
要支持在创建之后,改变卷的大小,你的驱动需要:
- 实现VolumeExpansion这一插件特性(capability)
- 实现控制器特性EXPAND_VOLUME,或者节点特性EXPAND_VOLUME,或者都实现
根据你的实现方式不同,卷扩容可能通过一次控制平面CSI RPC调用完成,也可能通过一次Node CSI RPC调用完成,也可以同时调用,分两步完成。
取决于插件特性VolumeExpansion,调用ControllerExpandVolume可以针对ONLINE或OFFLINE的卷执行:
- ONLINE,卷当前已经被publish(attach),或者在节点上可用
- OFFLINE,卷当前没有被publish,在节点上不可用
而NodeExpandVolume,则总是要求卷处于ONLINE状态。对于块存储,NodeExpandVolume典型情况下用来扩展它在节点上的文件系统的大小。
external-resizer这个sidecar由K8S提供,它负责监控PVC的变更,必要的话向CSI驱动发起ControllerExpandVolume调用。
在1.14 - 1.15中,卷扩展是Alpha特性,需要通过特性开关启用: --feature-gates=ExpandCSIVolumes=true
在1.14 - 1.15中,在线卷扩展需要通过特性开关启用: --feature-gates=ExpandInUsePersistentVolumes=true
创建PVC时,你可以指定PersistentVolumeClaim.DataSource字段,该字段对应CreateVolumeRequest.VolumeContentSource,VolumeContentSource提示CSI驱动,使用指定数据源来填充新创建的卷。
第一种用法是卷克隆,另一种是卷快照。
此特性1.16到达Beta。
传统的由CSI驱动提供的卷,仅仅和PV/PVC联用,是持久化的卷。
Pod内联卷(inline volume)用于支持临时存储用例。你可以在Pod Spec中直接指定CSI卷。在运行期间,内联卷遵循临时存储的生命周期(和Pod同步),CSI驱动在Pod创建、销毁期间,完成卷的管理操作。
配置示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
apiVersion: v1 kind: Pod metadata: name: some-pod spec: containers: ... volumes: - name: vol csi: driver: inline.storage.kubernetes.io volumeAttributes: foo: bar |
要支持内联卷,CSI驱动的逻辑需要变动。当K8S遇到声明了内联卷的Pod,它采用不同的处理方式。主要的一点是,驱动仅仅会:
- 在卷挂载期间,接收到NodePublish调用
- 在Pod删除、卷卸载时,接收到NodeUnpublish调用
CSIDriver对象的volumeLifecycleModes,必须包含Ephemeral。
1.15的特性开关 --feature-gates=CSIInlineVolume=true必须打开。
使用来自csi-test项目的sanity包,可以进行单元测试。
该包提供了一系列基础测试用例,任何CSI驱动都应该通过。例如,对于NodePublishVolume,如果没有提供卷,则应当失败。
你可以通过两种模式使用该包:
- 使用Go测试框架,将sanity包导入为依赖
- 使用命令行方式,针对你的驱动二进制文件进行测试
该库提供了一种简单的方式,验证CSI驱动遵循了规范。正如上文所述,对于Go语言开发的驱动,你可以将它引入为依赖。对于其它语言开发的驱动,则需要调用 csi-sanity命令对驱动的二进制文件进行测试。
sanity基于Ginkgo这一BDD测试框架,你可以在自己的TestXxx函数中调用sanity.Test:
1 2 3 4 5 6 7 8 9 10 |
func TestMyDriver(t *testing.T) { // 创建完整的驱动,及其环境 ... setup driver ... config := sanity.NewTestConfig() // 按需进行配置 cfg.Address = endpoint // 调用测试套件 sanity.Test(t, config) } |
另外一种用法,将自己的测试用例嵌入到Ginkgo测试套件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var _ = Describe("MyCSIDriver", func () { Context("Config A", func () { var config &sanity.Config BeforeEach(func() { // setup驱动和配置 }) AfterEach(func() { // 清理 }) Describe("CSI sanity", func() { sanity.GinkgoTest(config) }) }) Context("Config B", func () { // 基于另外一种配置进行测试 }) }) |
目前,CSI社区提供了端到端测试用例集,你可以将其导入自己的CSI驱动项目并执行。
未来CSI社区将会创建一套CSI一致性套件(CSI Conformance Suite),用于认证某个CSI驱动符合CSI Spec。
作为前提条件,你需要kubectl和1.13版本以上的K8S集群。
运行端到端测试的两种方式:
- 使用Kubernetes E2E Tests,通过参数提供DriverDefinition YAML文件。某些情况下,你无法使用这种测试途径,例如NFS的CSI插件,目前不支持动态Provisioning,因此必须跳过此步骤,你需要编写自己的testdriver
- 导入in-tree用例,使用go test执行
本节主要讨论第2种测试方式。
你可以使用K8S in-tree的存储E2E测试(1.14+)来测试自己的CSI驱动,参考NFS CSI plugin了解具体做法:测试文件位于test目录,主测试文件位于cmd目录。
你的驱动需要依赖1.14+版本的K8S,因为从1.14开始,才支持可拔插的E2E测试。
你还需要为CSI驱动实现testdriver,以说明如何创建测试用例。对于任何testdriver,以下函数是必须的:
-
GetDriverInfo() *testsuites.DriverInfo:返回DriverInfo,测试基于此方法寻找部署的CSI驱动,并且依据驱动的capability决定执行哪些用例。NFS驱动返回的信息如下:
1234567891011testsuites.DriverInfo{Name: "csi-nfsplugin",MaxFileSize: testpatterns.FileSizeLarge,SupportedFsType: sets.NewString("", // Default fsType),Capabilities: map[testsuites.Capability]bool{testsuites.CapPersistence: true,testsuites.CapExec: true,},}Hostpath驱动返回的信息如下:
1234567891011testsuites.DriverInfo{Name: "csi-hostpath",FeatureTag: "",MaxFileSize: testpatterns.FileSizeMedium,SupportedFsType: sets.NewString("", // Default fsType),Capabilities: map[testsuites.Capability]bool{testsuites.CapPersistence: true,},} - SkipUnsupportedTest(pattern testpatterns.TestPattern):提供你声明的、不支持的用例
- PrepareTest(f *framework.Framework) (*testsuites.PerTestConfig, func()):在此编写代码,setup你的CSI插件,在执行每个用例之前该方法会被调用
此外,取决于CSI驱动的特点, 你可能还需要实现testdriver的其它接口。例如对于NFS驱动,需要实现PreprovisionedVolumeTestDriver、PreprovisionedPVTestDriver,这样才能启动pre-provisioned测试
为CSI驱动实现了testdriver之后,你需要创建一个csi-volumes.go文件,在其中,利用testdriver来执行in-tree存储测试套件。
所有可用的in-tree存储测试套件参考:https://github.com/kubernetes/kubernetes/tree/master/test/e2e/storage/testsuites。
最后,在主测试文件中导入test包,初始化测试套件,运行测试:
1 2 3 4 5 6 7 8 9 |
var _ = utils.SIGDescribe("CSI Volumes", func() { testfiles.AddFileSource(testfiles.RootFileSource{Root: path.Join(framework.TestContext.RepoRoot, "../../deploy/kubernetes/")}) curDriver := NFSdriver() Context(testsuites.GetDriverNameWithFeatureTags(curDriver), func() { testsuites.DefineTestSuite(curDriver, CSITestSuites) }) }) |
尽管主测试文件会产生一个二进制文件,你也可以用go test来执行E2E测试:
1 |
go test -v <main test file> -ginkgo.v -ginkgo.progress --kubeconfig=<kubeconfig file> -timeout=0 |
典型情况下,一个CSI驱动部署为K8S的两组组件:Controller组件、Per-node组件。
可以部署在任何节点上,部署为Deployment或StatefulSet。该组件必须包含实现了CSI Controller服务的CSI驱动,附带1或多个sidecar。 Sidecar负责监控K8S对象,然后通过UDS(通过emptyDir在容器之间共享)调用CSI驱动的gRPC服务,Sidecar将CSI驱动公共逻辑提取出去,让你不必写样板代码。
你可以部署多个副本,实现HA,但是最好使用Leader选举机制,确保同一时刻只有一个活动的控制器。
通过DaemonSet部署到所有节点。该组件必须包含实现了CSI Node服务的CSI驱动,附带node-driver-registrar这个sidecar。
运行在节点上的Kubelet负责调用Node组件,进行mount/umount,并让卷对Pod可见。Kubelet同样通过UDS调用CSI驱动,UDS文件描述符位于hostPath卷中,在宿主机位置为 /var/lib/kubelet/<plugin name>/csi.sock
Node组件需要直接访问宿主机,这样才能保证块设备/文件系统挂载能够被Kubelet看见。因此,Node组件的挂载点必须设置为双向传播(Bidirectional)的,允许Kubelet能够看到Node组件容器创建的新挂载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
containers: - name: my-csi-driver ... volumeMounts: - name: socket-dir mountPath: /csi - name: mountpoint-dir mountPath: /var/lib/kubelet/pods # 挂载传播方向为双向,允许在容器之间,甚至容器和宿主之间传播挂载子树 # 这样CSI Node组件挂载的卷,在宿主机(Kubelet)命名空间可见 # 后者才有机会将卷绑定挂载给请求卷的容器 mountPropagation: "Bidirectional" - name: node-driver-registrar ... volumeMounts: - name: registration-dir mountPath: /registration volumes: # 供Kubelet到Driver的gRPC访问的UDS描述符 - name: socket-dir hostPath: path: /var/lib/kubelet/plugins/<driver-name> type: DirectoryOrCreate # CSI驱动在此卷中挂载卷 - name: mountpoint-dir hostPath: path: /var/lib/kubelet/pods type: Directory # node-driver-registrar通过此卷注册CSI驱动到Kubelet - name: registration-dir hostPath: path: /var/lib/kubelet/plugins_registry type: Directory |
为了启用K8S的挂载转播特性,底层运行时可能需要进行配置。对于Docker,你需要修改Systemd单元:
1 2 3 4 5 |
# 添加 MountFlags=shared # 或者移除 MountFlags=slave |
并重启Docker服务。
该项目的地址:https://github.com/kubernetes-csi/csi-driver-nfs,实现了CSI规范的最小子集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
func handle() { // 创建一个gRPC服务器,并运行 d := nfs.NewNFSdriver(nodeID, endpoint) d.Run() } // 创建CSI驱动 func NewNFSdriver(nodeID, endpoint string) *nfsDriver { glog.Infof("Driver: %v version: %v", driverName, version) n := &nfsDriver{ name: driverName, version: version, nodeID: nodeID, endpoint: endpoint, cap: map[csi.VolumeCapability_AccessMode_Mode]bool{}, } // 卷特性 vcam := []csi.VolumeCapability_AccessMode_Mode{ csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY, csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY, csi.VolumeCapability_AccessMode_MULTI_NODE_SINGLE_WRITER, csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, } n.AddVolumeCapabilityAccessModes(vcam) // 控制器特性,目前不支持控制器接口 n.AddControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{csi.ControllerServiceCapability_RPC_UNKNOWN}) return n } // 启动CSI驱动 func (n *nfsDriver) Run() { // Node 服务 n.ns = NewNodeServer(n, mount.New("")) s := NewNonBlockingGRPCServer() s.Start(n.endpoint, // Identity服务 NewDefaultIdentityServer(n), // Controller服务,实际上NFS驱动不支持该服务,因此代理给缺省实现 NewControllerServer(n), n.ns) s.Wait() } |
该服务需要在宿主机上执行挂载操作,因此驱动通过下面的调用为它提供了一个Mounter:
1 2 3 4 5 6 7 8 9 |
// 参数可以指定mount命令,默认使用 /bin/mount mounter := mount.New("") func NewNodeServer(n *nfsDriver, mounter mount.Interface) *nodeServer { return &nodeServer{ Driver: n, // 就是驱动对象nfsDriver,持续当前驱动的元数据 mounter: mounter, } } |
Node服务实现了CSI Spec的Node接口:
1 2 3 4 5 6 7 8 9 10 11 |
// 从Proto文件生成的Go接口 type NodeServer interface { NodeStageVolume(context.Context, *NodeStageVolumeRequest) (*NodeStageVolumeResponse, error) NodeUnstageVolume(context.Context, *NodeUnstageVolumeRequest) (*NodeUnstageVolumeResponse, error) NodePublishVolume(context.Context, *NodePublishVolumeRequest) (*NodePublishVolumeResponse, error) NodeUnpublishVolume(context.Context, *NodeUnpublishVolumeRequest) (*NodeUnpublishVolumeResponse, error) NodeGetVolumeStats(context.Context, *NodeGetVolumeStatsRequest) (*NodeGetVolumeStatsResponse, error) NodeExpandVolume(context.Context, *NodeExpandVolumeRequest) (*NodeExpandVolumeResponse, error) NodeGetCapabilities(context.Context, *NodeGetCapabilitiesRequest) (*NodeGetCapabilitiesResponse, error) NodeGetInfo(context.Context, *NodeGetInfoRequest) (*NodeGetInfoResponse, error) } |
代码比较简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
type nodeServer struct { Driver *nfsDriver mounter mount.Interface } func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { // 卷挂载到哪里 targetPath := req.GetTargetPath() // 启发式的判断是否挂载点(这个路径是否挂载了一个设备之类)。内部实现是 // 判断父目录和该路径是否对应了不同的设备,如果是,则认为它是挂载点 // 这个方法的效率高,但是不能时被所有类型的挂载点,特别是Linux绑定挂载、符号连接 // 如果调用者不关心这些特殊类型的挂载点,则可以用该函数,代替List()+遍历判断 notMnt, err := ns.mounter.IsLikelyNotMountPoint(targetPath) if err != nil { // 如果目录不存在,则创建目录,认定不是挂载点 if os.IsNotExist(err) { if err := os.MkdirAll(targetPath, 0750); err != nil { return nil, status.Error(codes.Internal, err.Error()) } notMnt = true } else { // 其它错误,出错 return nil, status.Error(codes.Internal, err.Error()) } } // 只有不是挂载点,才进行后续处理 if !notMnt { return &csi.NodePublishVolumeResponse{}, nil } // 获取 请求的卷特性 挂载选项 mo := req.GetVolumeCapability().GetMount().GetMountFlags() // 挂载为只读 if req.GetReadonly() { mo = append(mo, "ro") } // 卷上下文,包含NFS服务器的信息 s := req.GetVolumeContext()["server"] ep := req.GetVolumeContext()["share"] source := fmt.Sprintf("%s:%s", s, ep) // 在宿主机上执行挂载 err = ns.mounter.Mount(source, targetPath, "nfs", mo) if err != nil { if os.IsPermission(err) { return nil, status.Error(codes.PermissionDenied, err.Error()) } if strings.Contains(err.Error(), "invalid argument") { return nil, status.Error(codes.InvalidArgument, err.Error()) } return nil, status.Error(codes.Internal, err.Error()) } return &csi.NodePublishVolumeResponse{}, nil } func (ns *nodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) { // 如果目标路径存在,且是挂载点,则进行卸载 targetPath := req.GetTargetPath() notMnt, err := ns.mounter.IsLikelyNotMountPoint(targetPath) if err != nil { if os.IsNotExist(err) { return nil, status.Error(codes.NotFound, "Targetpath not found") } else { return nil, status.Error(codes.Internal, err.Error()) } } if notMnt { return nil, status.Error(codes.NotFound, "Volume not mounted") } // 卸载,并且删除挂载点对应目录 err = mount.CleanupMountPoint(req.GetTargetPath(), ns.mounter, false) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } return &csi.NodeUnpublishVolumeResponse{}, nil } // 从nfsDriver获取信息 func (ns *nodeServer) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { glog.V(5).Infof("Using default NodeGetInfo") return &csi.NodeGetInfoResponse{ NodeId: ns.Driver.nodeID, }, nil } // 从nfsDriver获取信息 func (ns *nodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { glog.V(5).Infof("Using default NodeGetCapabilities") return &csi.NodeGetCapabilitiesResponse{ Capabilities: []*csi.NodeServiceCapability{ { Type: &csi.NodeServiceCapability_Rpc{ Rpc: &csi.NodeServiceCapability_RPC{ Type: csi.NodeServiceCapability_RPC_UNKNOWN, }, }, }, }, }, nil } // 下面的方法都不支持 func (ns *nodeServer) NodeGetVolumeStats(ctx context.Context, in *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { return nil, status.Error(codes.Unimplemented, "") } func (ns *nodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { return &csi.NodeUnstageVolumeResponse{}, nil } func (ns *nodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { return &csi.NodeStageVolumeResponse{}, nil } func (ns *nodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { return nil, status.Error(codes.Unimplemented, "") } |
不管是三大服务的哪个,都需要持有nfsDriver的引用:
1 2 3 4 5 |
func NewDefaultIdentityServer(d *nfsDriver) *IdentityServer { return &IdentityServer{ Driver: d, } } |
Identity服务实现了CSI Spec的Identity接口:
1 2 3 4 5 |
type IdentityServer interface { GetPluginInfo(context.Context, *GetPluginInfoRequest) (*GetPluginInfoResponse, error) GetPluginCapabilities(context.Context, *GetPluginCapabilitiesRequest) (*GetPluginCapabilitiesResponse, error) Probe(context.Context, *ProbeRequest) (*ProbeResponse, error) } |
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
type IdentityServer struct { Driver *nfsDriver } // 从nfsDriver获取信息 func (ids *IdentityServer) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { glog.V(5).Infof("Using default GetPluginInfo") if ids.Driver.name == "" { return nil, status.Error(codes.Unavailable, "Driver name not configured") } if ids.Driver.version == "" { return nil, status.Error(codes.Unavailable, "Driver is missing version") } return &csi.GetPluginInfoResponse{ Name: ids.Driver.name, VendorVersion: ids.Driver.version, }, nil } // Noop func (ids *IdentityServer) Probe(ctx context.Context, req *csi.ProbeRequest) (*csi.ProbeResponse, error) { return &csi.ProbeResponse{}, nil } // 从nfsDriver获取信息 func (ids *IdentityServer) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) { glog.V(5).Infof("Using default capabilities") return &csi.GetPluginCapabilitiesResponse{ Capabilities: []*csi.PluginCapability{ { Type: &csi.PluginCapability_Service_{ Service: &csi.PluginCapability_Service{ Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, }, }, }, }, }, nil } |
Controller服务实现了CSI Spec的Controller接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type ControllerServer interface { CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error) DeleteVolume(context.Context, *DeleteVolumeRequest) (*DeleteVolumeResponse, error) ControllerPublishVolume(context.Context, *ControllerPublishVolumeRequest) (*ControllerPublishVolumeResponse, error) ControllerUnpublishVolume(context.Context, *ControllerUnpublishVolumeRequest) (*ControllerUnpublishVolumeResponse, error) ValidateVolumeCapabilities(context.Context, *ValidateVolumeCapabilitiesRequest) (*ValidateVolumeCapabilitiesResponse, error) ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error) GetCapacity(context.Context, *GetCapacityRequest) (*GetCapacityResponse, error) ControllerGetCapabilities(context.Context, *ControllerGetCapabilitiesRequest) (*ControllerGetCapabilitiesResponse, error) CreateSnapshot(context.Context, *CreateSnapshotRequest) (*CreateSnapshotResponse, error) DeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*DeleteSnapshotResponse, error) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error) ControllerExpandVolume(context.Context, *ControllerExpandVolumeRequest) (*ControllerExpandVolumeResponse, error) } |
代码就不贴了,都是简单的返回Unimplemented错误。
在nfsDriver的Run方法中,将上述三个服务组装为一个gRPC服务:
1 2 3 4 5 6 |
s := NewNonBlockingGRPCServer() s.Start(n.endpoint, NewDefaultIdentityServer(n), NewControllerServer(n), n.ns) s.Wait() |
gRPC服务由下面的接口表示:
1 2 3 4 5 6 7 8 9 10 |
type NonBlockingGRPCServer interface { // 启动gRPC服务,注册三个Server端 Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) // 阻塞,等待服务停止信号 Wait() // 优雅停止服务 Stop() // 强制停止服务 ForceStop() } |
它的实现是一个结构,引用一个grpc.Server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
type nonBlockingGRPCServer struct { wg sync.WaitGroup server *grpc.Server } // 在等待组上等待 func (s *nonBlockingGRPCServer) Wait() { s.wg.Wait() } // 等待组增加一个信号,然后启动gRPC服务 func (s *nonBlockingGRPCServer) Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) { s.wg.Add(1) go s.serve(endpoint, ids, cs, ns) return } // 下面的这些接口直接委托给grpc.Server func (s *nonBlockingGRPCServer) Stop() { s.server.GracefulStop() } func (s *nonBlockingGRPCServer) ForceStop() { s.server.Stop() } |
启动gRPC服务的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
func (s *nonBlockingGRPCServer) serve(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) { // 解析协议、地址 proto, addr, err := ParseEndpoint(endpoint) if err != nil { glog.Fatal(err.Error()) } // 如果是UDS套接字,尝试删除已存在的Socket文件 if proto == "unix" { addr = "/" + addr if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { glog.Fatalf("Failed to remove %s, error: %s", addr, err.Error()) } } // 监听 listener, err := net.Listen(proto, addr) if err != nil { glog.Fatalf("Failed to listen: %v", err) } // 注册日志拦截器 opts := []grpc.ServerOption{ grpc.UnaryInterceptor(logGRPC), } // 创建gRPC服务对象 server := grpc.NewServer(opts...) s.server = server // 注册三个服务 if ids != nil { csi.RegisterIdentityServer(server, ids) } if cs != nil { csi.RegisterControllerServer(server, cs) } if ns != nil { csi.RegisterNodeServer(server, ns) } glog.Infof("Listening for connections on address: %#v", listener.Addr()) // 在监听器上启动gRPC服务 server.Serve(listener) } // 日志拦截器 func logGRPC(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { glog.V(3).Infof("GRPC call: %s", info.FullMethod) glog.V(5).Infof("GRPC request: %s", protosanitizer.StripSecrets(req)) // 执行gRPC调用 resp, err := handler(ctx, req) if err != nil { glog.Errorf("GRPC error: %v", err) } else { glog.V(5).Infof("GRPC response: %s", protosanitizer.StripSecrets(resp)) } return resp, err } |
前文介绍过,端到端测试可以作为二进制文件运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package main import ( "flag" // 注册test包中的用例 _ "github.com/kubernetes-csi/csi-driver-nfs/test" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "k8s.io/kubernetes/test/e2e/framework" "testing" ) func init() { // e2e测试框架初始化 framework.HandleFlags() framework.AfterReadingAllFlags(&framework.TestContext) } func Test(t *testing.T) { flag.Parse() RegisterFailHandler(Fail) // 测试入口点,运行所有用例(Describes) RunSpecs(t, "CSI Suite") } func main() { Test(&testing.T{}) } |
按照约定,在名为test/csi-volumes.go的文件中定义测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package test import ( . "github.com/onsi/ginkgo" _ "github.com/onsi/gomega" "k8s.io/kubernetes/test/e2e/framework" "k8s.io/kubernetes/test/e2e/framework/testfiles" "k8s.io/kubernetes/test/e2e/storage/testsuites" "k8s.io/kubernetes/test/e2e/storage/utils" "path" ) // 需要进行哪些方面的测试 var CSITestSuites = []func() testsuites.TestSuite{ testsuites.InitVolumesTestSuite, testsuites.InitVolumeIOTestSuite, testsuites.InitVolumeModeTestSuite, testsuites.InitSubPathTestSuite, testsuites.InitProvisioningTestSuite, //testsuites.InitSnapshottableTestSuite, //testsuites.InitMultiVolumeTestSuite, } // 执行测试套件 var _ = utils.SIGDescribe("CSI Volumes", func() { // 安装CSI驱动到K8S所需的YAMLs testfiles.AddFileSource(testfiles.RootFileSource{Root: path.Join(framework.TestContext.RepoRoot, "../../deploy/kubernetes/")}) // NFS的TestDriver curDriver := NFSdriver() Context(testsuites.GetDriverNameWithFeatureTags(curDriver), func() { // 根据你的TestDriver决定最终需要进行哪些测试,如何进行 testsuites.DefineTestSuite(curDriver, CSITestSuites) }) }) |
test/nfs-testdriver.go中则定义了NFS CSI Driver的TestDriver,我们先看一下该接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 此接口,在测试套件运行过程中,代表被测试的CSI驱动 // 除了GetDriverInfo,所有测试方法都在测试期间调用,因此你可以在方法实现中调用 // framework.Skipf, framework.Fatal, Gomega assertions type TestDriver interface { // 驱动的静态元数据 GetDriverInfo() *DriverInfo // 如果Testpattern不适合此驱动,则跳过 // 在解析了测试套件参数,启动测试框架之前调用 SkipUnsupportedTest(testpatterns.TestPattern) // 在每个新的测试用例启动前调用 PrepareTest(f *framework.Framework) (*PerTestConfig, func()) } |
此外,NFS的测试驱动还实现了:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 表示支持PreprovisionedPV的TestDriver type PreprovisionedPVTestDriver interface { PreprovisionedVolumeTestDriver // 为预分配的卷创建PersistentVolumeSource + VolumeNodeAffinity GetPersistentVolumeSource(readOnly bool, fsType string, testVolume TestVolume) (*v1.PersistentVolumeSource, *v1.VolumeNodeAffinity) } // 具有预分配卷的TestDriver type PreprovisionedVolumeTestDriver interface { TestDriver // 创建指定类型的预分配好的卷 CreateVolume(config *PerTestConfig, volumeType testpatterns.TestVolType) TestVolume } |
以支持pre-provisioned测试。
下面看一下NFS的TestDriver的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
package test import ( "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/kubernetes/test/e2e/framework" "k8s.io/kubernetes/test/e2e/storage/testpatterns" "k8s.io/kubernetes/test/e2e/storage/testsuites" ) type nfsDriver struct { driverInfo testsuites.DriverInfo manifests []string } var NFSdriver = InitNFSDriver type nfsVolume struct { serverIP string serverPod *v1.Pod f *framework.Framework } // 初始化NFS的TestDriver, manifests为需要安装到K8S的清单文件 func initNFSDriver(name string, manifests ...string) testsuites.TestDriver { return &nfsDriver{ driverInfo: testsuites.DriverInfo{ Name: name, MaxFileSize: testpatterns.FileSizeLarge, SupportedFsType: sets.NewString( "", // Default fsType ), Capabilities: map[testsuites.Capability]bool{ testsuites.CapPersistence: true, testsuites.CapExec: true, }, }, manifests: manifests, } } func InitNFSDriver() testsuites.TestDriver { return initNFSDriver("nfs.csi.k8s.io", "csi-attacher-nfsplugin.yaml", "csi-attacher-rbac.yaml", "csi-nodeplugin-nfsplugin.yaml", "csi-nodeplugin-rbac.yaml") } var _ testsuites.TestDriver = &nfsDriver{} var _ testsuites.PreprovisionedVolumeTestDriver = &nfsDriver{} var _ testsuites.PreprovisionedPVTestDriver = &nfsDriver{} // TestDriver的静态信息 func (n *nfsDriver) GetDriverInfo() *testsuites.DriverInfo { return &n.driverInfo } // 跳过动态Provision测试 func (n *nfsDriver) SkipUnsupportedTest(pattern testpatterns.TestPattern) { if pattern.VolType == testpatterns.DynamicPV { framework.Skipf("NFS Driver does not support dynamic provisioning -- skipping") } } // PersistentVolumeSource代表PV的源,自然需要知道存储的位置、路径信息 // 这些信息从TestVolume得到,TestVolume在这里实际上持有的是NFS服务器的信息,而不是某个卷的信息 func (n *nfsDriver) GetPersistentVolumeSource(readOnly bool, fsType string, volume testsuites.TestVolume) (*v1.PersistentVolumeSource, *v1.VolumeNodeAffinity) { nv, _ := volume.(*nfsVolume) return &v1.PersistentVolumeSource{ CSI: &v1.CSIPersistentVolumeSource{ Driver: n.driverInfo.Name, VolumeHandle: "nfs-vol", VolumeAttributes: map[string]string{ "server": nv.serverIP, "share": "/", "readOnly": "true", }, }, }, nil } // 执行每个用例前的准备工作 func (n *nfsDriver) PrepareTest(f *framework.Framework) (*testsuites.PerTestConfig, func()) { config := &testsuites.PerTestConfig{ Driver: n, Prefix: "nfs", Framework: f, } return config, func() {} } // 创建一个测试卷(服务) func (n *nfsDriver) CreateVolume(config *testsuites.PerTestConfig, volType testpatterns.TestVolType) testsuites.TestVolume { f := config.Framework cs := f.ClientSet ns := f.Namespace switch volType { case testpatterns.InlineVolume: fallthrough case testpatterns.PreprovisionedPV: // 创建NFS服务器的Pod c := framework.VolumeTestConfig{ Namespace: ns.Name, Prefix: "nfs", ServerImage: "gcr.io/kubernetes-e2e-test-images/volume/nfs:1.0", ServerPorts: []int{2049}, ServerVolumes: map[string]string{"": "/exports"}, ServerReadyMessage: "NFS started", } config.ServerConfig = &c serverPod, serverIP := framework.CreateStorageServer(cs, c) return &nfsVolume{ serverIP: serverIP, serverPod: serverPod, f: f, } case testpatterns.DynamicPV: // Do nothing default: framework.Failf("Unsupported volType:%v is specified", volType) } return nil } // 删除测试卷(服务) func (v *nfsVolume) DeleteVolume() { framework.CleanUpVolumeServer(v.f, v.serverPod) } |
这里有一个华为CSI的例子,我们看看它是如何编排的。
Attacher属于Controller组件,因此部署为Deployment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
apiVersion: apps/v1 kind: Deployment metadata: name: external-attacher namespace: huawei spec: replicas: 1 selector: matchLabels: external-attacher: external-attacher template: metadata: labels: external-attacher: external-attacher spec: serviceAccountName: csi-attacher containers: - name: external-attacher image: quay.io/k8scsi/csi-attacher:v2.2.0 args: - "--v=5" - "--csi-address=$(ADDRESS)" - "--leader-election" - "--leader-election-namespace=$(MY_NAMESPACE)" - "--leader-election-identity=$(MY_NAME)" env: - name: MY_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: MY_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: ADDRESS value: /csi/csi.sock imagePullPolicy: "IfNotPresent" volumeMounts: - name: socket-dir mountPath: /csi volumes: - name: socket-dir hostPath: # 它的实现方式是,在每个节点上直接启动CSI驱动的二进制文件(提供gRPC服务) # 然后Sidecar通过UDS访问CSI驱动。而不是官方推荐的,以容器方式运行CSI驱动 path: /var/lib/kubelet/plugins/huawei.csi.driver type: Directory |
权限相关:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# Attacher must be able to work with PVs, nodes and VolumeAttachments kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: external-attacher-runner namespace: huawei rules: - apiGroups: [""] resources: ["persistentvolumes"] verbs: ["get", "list", "watch", "update"] - apiGroups: [""] resources: ["nodes"] verbs: ["get", "list", "watch"] - apiGroups: ["csi.storage.k8s.io"] resources: ["csinodeinfos"] verbs: ["get", "list", "watch"] - apiGroups: ["storage.k8s.io"] resources: ["volumeattachments"] verbs: ["get", "list", "watch", "update"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: csi-attacher-role namespace: huawei subjects: - kind: ServiceAccount name: csi-attacher namespace: huawei roleRef: kind: ClusterRole name: external-attacher-runner apiGroup: rbac.authorization.k8s.io --- # Attacher must be able to work with config map in current namespace # if (and only if) leadership election is enabled kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: external-attacher-cfg namespace: huawei rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "watch", "list", "delete", "update", "create"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: csi-attacher-role-cfg namespace: huawei subjects: - kind: ServiceAccount name: csi-attacher namespace: huawei roleRef: kind: Role name: external-attacher-cfg apiGroup: rbac.authorization.k8s.io --- apiVersion: v1 kind: ServiceAccount metadata: name: csi-attacher namespace: huawei |
StorageClass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# 文件系统 kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: fs provisioner: huawei parameters: volumeType: fs allocType: thin authClient: "*" backend: "OceanStorage-nas" --- # iSCSI块设备 apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: lun provisioner: huawei parameters: volumeType: lun allocType: thin |
这个不知道为什么要做成StatefulSet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
apiVersion: apps/v1 kind: StatefulSet metadata: name: external-provisioner namespace: huawei labels: app: external-provisioner spec: replicas: 1 serviceName: external-provisioner selector: matchLabels: app: external-provisioner template: metadata: labels: app: external-provisioner spec: serviceAccountName: csi-provisioner containers: - name: external-provisioner image: quay.io/k8scsi/csi-provisioner:v2.0.4 args: - "--provisioner=huawei" - "--csi-address=/csi/csi.sock" imagePullPolicy: "IfNotPresent" lifecycle: postStart: exec: command: ["/bin/sh", "-c", "touch /csi/provider_running"] preStop: exec: command: ["/bin/sh", "-c", "rm -f /csi/provider_running"] volumeMounts: - name: socket-dir mountPath: /csi volumes: - name: socket-dir hostPath: path: /var/lib/kubelet/plugins/huawei.csi.driver type: Directory |
权限相关:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: external-provisioner-runner namespace: huawei rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get", "list"] - apiGroups: [""] resources: ["persistentvolumes"] verbs: ["get", "list", "watch", "create", "delete"] - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "list", "watch", "update"] - apiGroups: ["storage.k8s.io"] resources: ["storageclasses"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["events"] verbs: ["list", "watch", "create", "update", "patch"] - apiGroups: ["snapshot.storage.k8s.io"] resources: ["volumesnapshots"] verbs: ["get", "list"] - apiGroups: ["snapshot.storage.k8s.io"] resources: ["volumesnapshotcontents"] verbs: ["get", "list"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: csi-provisioner-role namespace: huawei subjects: - kind: ServiceAccount name: csi-provisioner namespace: huawei roleRef: kind: ClusterRole name: external-provisioner-runner apiGroup: rbac.authorization.k8s.io --- # Provisioner must be able to work with endpoints in current namespace # if (and only if) leadership election is enabled kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: external-provisioner-cfg namespace: huawei rules: - apiGroups: [""] resources: ["endpoints"] verbs: ["get", "watch", "list", "delete", "update", "create"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: csi-provisioner-role-cfg namespace: huawei subjects: - kind: ServiceAccount name: csi-provisioner namespace: huawei roleRef: kind: Role name: external-provisioner-cfg apiGroup: rbac.authorization.k8s.io --- # https://raw.githubusercontent.com/kubernetes-csi/external-provisioner/1cd1c20a6d4b2fcd25c98a008385b436d61d46a4/deploy/kubernetes/rbac.yaml apiVersion: v1 kind: ServiceAccount metadata: name: csi-provisioner namespace: huawei |
该组件需要运行在所有节点,因此是DaemonSet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
apiVersion: apps/v1 kind: DaemonSet metadata: name: node-driver-registrar namespace: huawei spec: selector: matchLabels: app: node-driver-registrar template: metadata: labels: app: node-driver-registrar spec: serviceAccountName: csi-driver-registrar hostNetwork: true containers: - name: node-driver-registrar image: quay.io/k8scsi/csi-node-driver-registrar:v2.0.1 args: # Pod中CSI驱动的UDS路径 - "--csi-address=/csi/csi.sock" # 宿主机上CSI驱动的UDS路径,Kubelet使用该路径发起CSI操作 # 通常使用格式 /var/lib/kubelet/plugins/<drivername.example.com>/csi.sock - "--kubelet-registration-path=/var/lib/kubelet/plugins/huawei.csi.driver/csi.sock" imagePullPolicy: "IfNotPresent" lifecycle: preStop: exec: command: ["/bin/sh", "-c", "rm -rf /registration/huawei.csi.driver-reg.sock"] volumeMounts: - name: plugin-dir mountPath: /csi - name: registration-dir mountPath: /registration volumes: - name: registration-dir hostPath: path: /var/lib/kubelet/plugins_registry/ type: Directory - name: plugin-dir hostPath: path: /var/lib/kubelet/plugins/huawei.csi.driver/ type: Directory |
权限相关:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: driver-registrar-runner namespace: huawei rules: - apiGroups: [""] resources: ["events"] verbs: ["get", "list", "watch", "create", "update", "patch"] # The following permissions are only needed when running # driver-registrar without the --kubelet-registration-path # parameter, i.e. when using driver-registrar instead of # kubelet to update the csi.volume.kubernetes.io/nodeid # annotation. That mode of operation is going to be deprecated # and should not be used anymore, but is needed on older # Kubernetes versions. # - apiGroups: [""] # resources: ["nodes"] # verbs: ["get", "update", "patch"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: csi-driver-registrar-role namespace: huawei subjects: - kind: ServiceAccount name: csi-driver-registrar namespace: huawei roleRef: kind: ClusterRole name: driver-registrar-runner apiGroup: rbac.authorization.k8s.io --- apiVersion: v1 kind: ServiceAccount metadata: name: csi-driver-registrar namespace: huawei |
Leave a Reply