feat: init

This commit is contained in:
2026-06-06 06:15:45 +03:30
commit a986de98b0
33 changed files with 2141 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
package driver
import (
"context"
"github.com/container-storage-interface/spec/lib/go/csi"
"github.com/KubelanCloud/kks-csi-plugin/pkg/csi/provisioner"
)
type ControllerServer struct {
d *Driver
}
func newControllerServer(d *Driver) *ControllerServer {
return &ControllerServer{d: d}
}
func (s *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
if req.GetName() == "" {
return nil, invalidArgument("volume name is required")
}
if req.GetVolumeCapabilities() == nil {
return nil, invalidArgument("volume capabilities are required")
}
capacity := int64(1 * 1024 * 1024 * 1024)
if req.GetCapacityRange() != nil {
if req.GetCapacityRange().GetRequiredBytes() > 0 {
capacity = req.GetCapacityRange().GetRequiredBytes()
} else if req.GetCapacityRange().GetLimitBytes() > 0 {
capacity = req.GetCapacityRange().GetLimitBytes()
}
}
vol, err := s.d.backend.CreateVolume(ctx, provisioner.CreateVolumeRequest{
Name: sanitizeVolumeName(req.GetName()),
SizeBytes: capacity,
})
if err != nil {
return nil, internalError(err)
}
return &csi.CreateVolumeResponse{
Volume: &csi.Volume{
VolumeId: vol.VolumeID,
CapacityBytes: vol.SizeBytes,
VolumeContext: vol.VolumeContext,
},
}, nil
}
func (s *ControllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
if req.GetVolumeId() == "" {
return nil, invalidArgument("volume id is required")
}
if err := s.d.backend.DeleteVolume(ctx, req.GetVolumeId()); err != nil {
return nil, internalError(err)
}
return &csi.DeleteVolumeResponse{}, nil
}
func (s *ControllerServer) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) {
if req.GetVolumeId() == "" {
return nil, invalidArgument("volume id is required")
}
if req.GetNodeId() == "" {
return nil, invalidArgument("node id is required")
}
pub, err := s.d.backend.PublishVolume(ctx, req.GetVolumeId(), req.GetNodeId())
if err != nil {
return nil, internalError(err)
}
return &csi.ControllerPublishVolumeResponse{
PublishContext: pub.PublishContext,
}, nil
}
func (s *ControllerServer) ControllerUnpublishVolume(ctx context.Context, req *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) {
if req.GetVolumeId() == "" {
return nil, invalidArgument("volume id is required")
}
nodeID := req.GetNodeId()
if nodeID == "" {
return &csi.ControllerUnpublishVolumeResponse{}, nil
}
if err := s.d.backend.UnpublishVolume(ctx, req.GetVolumeId(), nodeID); err != nil {
return nil, internalError(err)
}
return &csi.ControllerUnpublishVolumeResponse{}, nil
}
func (s *ControllerServer) ValidateVolumeCapabilities(ctx context.Context, req *csi.ValidateVolumeCapabilitiesRequest) (*csi.ValidateVolumeCapabilitiesResponse, error) {
_ = ctx
if req.GetVolumeId() == "" {
return nil, invalidArgument("volume id is required")
}
if req.GetVolumeCapabilities() == nil {
return nil, invalidArgument("volume capabilities are required")
}
return &csi.ValidateVolumeCapabilitiesResponse{
Confirmed: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{
VolumeCapabilities: req.GetVolumeCapabilities(),
},
}, nil
}
func (s *ControllerServer) ListVolumes(ctx context.Context, req *csi.ListVolumesRequest) (*csi.ListVolumesResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("ListVolumes is not supported")
}
func (s *ControllerServer) GetCapacity(ctx context.Context, req *csi.GetCapacityRequest) (*csi.GetCapacityResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("GetCapacity is not supported")
}
func (s *ControllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("CreateSnapshot is not supported")
}
func (s *ControllerServer) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("DeleteSnapshot is not supported")
}
func (s *ControllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("ListSnapshots is not supported")
}
func (s *ControllerServer) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("ControllerExpandVolume is not supported")
}
func (s *ControllerServer) ControllerGetVolume(ctx context.Context, req *csi.ControllerGetVolumeRequest) (*csi.ControllerGetVolumeResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("ControllerGetVolume is not supported")
}
func (s *ControllerServer) ControllerGetCapabilities(ctx context.Context, req *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) {
_ = ctx
_ = req
return &csi.ControllerGetCapabilitiesResponse{
Capabilities: []*csi.ControllerServiceCapability{
controllerCapability(csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME),
controllerCapability(csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME),
},
}, nil
}
func (s *ControllerServer) ControllerModifyVolume(ctx context.Context, req *csi.ControllerModifyVolumeRequest) (*csi.ControllerModifyVolumeResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("ControllerModifyVolume is not supported")
}
+149
View File
@@ -0,0 +1,149 @@
package driver
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"github.com/container-storage-interface/spec/lib/go/csi"
"github.com/KubelanCloud/kks-csi-plugin/config"
"github.com/KubelanCloud/kks-csi-plugin/pkg/csi/client"
"github.com/KubelanCloud/kks-csi-plugin/pkg/csi/provisioner"
"go.uber.org/zap"
"google.golang.org/grpc"
)
type Driver struct {
cfg *config.Config
log *zap.SugaredLogger
backend provisioner.Backend
storageID string
identity *IdentityServer
controller *ControllerServer
node *NodeServer
}
func Run(ctx context.Context, cfg *config.Config, logger *zap.Logger) error {
if err := cfg.Validate(); err != nil {
return err
}
if logger == nil {
logger = zap.NewNop()
}
log := logger.Sugar()
backend := client.NewBackend(cfg.Client)
defer backend.Close()
cluster, err := backend.ClusterInfo(ctx)
if err != nil {
return fmt.Errorf("resolve storage info from csi server: %w", err)
}
d := &Driver{
cfg: cfg,
log: log,
backend: backend,
storageID: cluster.StorageID,
identity: newIdentityServer(cfg),
}
switch cfg.Driver.Mode {
case config.CSIModeController, config.CSIModeAll:
d.controller = newControllerServer(d)
case config.CSIModeNode:
}
switch cfg.Driver.Mode {
case config.CSIModeNode, config.CSIModeAll:
d.node = newNodeServer(d)
case config.CSIModeController:
}
endpoint, err := parseEndpoint(cfg.Driver.Endpoint)
if err != nil {
return err
}
if err := ensureSocketDir(endpoint); err != nil {
return err
}
listener, err := net.Listen(endpoint.network, endpoint.address)
if err != nil {
return fmt.Errorf("listen on %s: %w", cfg.Driver.Endpoint, err)
}
defer listener.Close()
server := grpc.NewServer()
csi.RegisterIdentityServer(server, d.identity)
if d.controller != nil {
csi.RegisterControllerServer(server, d.controller)
}
if d.node != nil {
csi.RegisterNodeServer(server, d.node)
}
log.Infof(
"starting csi client name=%s endpoint=%s mode=%s node_id=%s server=%s storage_id=%s",
cfg.Driver.Name,
cfg.Driver.Endpoint,
cfg.Driver.Mode,
cfg.Driver.NodeID,
cfg.Client.ServerURL,
cluster.StorageID,
)
errCh := make(chan error, 1)
go func() {
errCh <- server.Serve(listener)
}()
select {
case <-ctx.Done():
log.Info("shutting down csi client")
server.GracefulStop()
return nil
case err := <-errCh:
if err != nil {
return fmt.Errorf("csi grpc server stopped: %w", err)
}
return nil
}
}
type endpoint struct {
network string
address string
}
func parseEndpoint(raw string) (endpoint, error) {
if strings.HasPrefix(raw, "unix://") {
return endpoint{
network: "unix",
address: strings.TrimPrefix(raw, "unix://"),
}, nil
}
if strings.HasPrefix(raw, "tcp://") {
return endpoint{
network: "tcp",
address: strings.TrimPrefix(raw, "tcp://"),
}, nil
}
return endpoint{}, fmt.Errorf("unsupported endpoint %q (use unix:// or tcp://)", raw)
}
func ensureSocketDir(ep endpoint) error {
if ep.network != "unix" {
return nil
}
if err := os.MkdirAll(filepath.Dir(ep.address), 0o755); err != nil {
return fmt.Errorf("create socket directory: %w", err)
}
if err := os.Remove(ep.address); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove stale socket: %w", err)
}
return nil
}
+82
View File
@@ -0,0 +1,82 @@
package driver
import (
"context"
"github.com/container-storage-interface/spec/lib/go/csi"
"github.com/KubelanCloud/kks-csi-plugin/config"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
)
type IdentityServer struct {
cfg *config.Config
}
func newIdentityServer(cfg *config.Config) *IdentityServer {
return &IdentityServer{cfg: cfg}
}
func (s *IdentityServer) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
_ = ctx
_ = req
return &csi.GetPluginInfoResponse{
Name: s.cfg.Driver.Name,
VendorVersion: "v0.1.0",
}, nil
}
func (s *IdentityServer) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) {
_ = ctx
_ = req
return &csi.GetPluginCapabilitiesResponse{
Capabilities: []*csi.PluginCapability{
{
Type: &csi.PluginCapability_Service_{
Service: &csi.PluginCapability_Service{
Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,
},
},
},
},
}, nil
}
func (s *IdentityServer) Probe(ctx context.Context, req *csi.ProbeRequest) (*csi.ProbeResponse, error) {
_ = ctx
_ = req
return &csi.ProbeResponse{Ready: wrapperspb.Bool(true)}, nil
}
func controllerCapability(t csi.ControllerServiceCapability_RPC_Type) *csi.ControllerServiceCapability {
return &csi.ControllerServiceCapability{
Type: &csi.ControllerServiceCapability_Rpc{
Rpc: &csi.ControllerServiceCapability_RPC{Type: t},
},
}
}
func nodeCapability(t csi.NodeServiceCapability_RPC_Type) *csi.NodeServiceCapability {
return &csi.NodeServiceCapability{
Type: &csi.NodeServiceCapability_Rpc{
Rpc: &csi.NodeServiceCapability_RPC{Type: t},
},
}
}
func invalidArgument(msg string) error {
return status.Error(codes.InvalidArgument, msg)
}
func notFound(msg string) error {
return status.Error(codes.NotFound, msg)
}
func internalError(err error) error {
return status.Errorf(codes.Internal, "%v", err)
}
func unimplemented(msg string) error {
return status.Error(codes.Unimplemented, msg)
}
+120
View File
@@ -0,0 +1,120 @@
//go:build linux
package driver
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
mount "k8s.io/mount-utils"
utilexec "k8s.io/utils/exec"
)
const defaultLinuxFsType = "ext4"
func newMounter() *mount.SafeFormatAndMount {
return mount.NewSafeFormatAndMount(mount.New(""), utilexec.New())
}
func scsiHostRescan() {
scsiPath := "/sys/class/scsi_host/"
entries, err := os.ReadDir(scsiPath)
if err != nil {
return
}
for _, entry := range entries {
name := filepath.Join(scsiPath, entry.Name(), "scan")
_ = os.WriteFile(name, []byte("- - -"), 0o666)
}
}
func findDiskByLUN(lun int) (string, error) {
scsiHostRescan()
sysPath := "/sys/bus/scsi/devices"
entries, err := os.ReadDir(sysPath)
if err != nil {
return "", fmt.Errorf("read %s: %w", sysPath, err)
}
for _, entry := range entries {
name := entry.Name()
parts := strings.Split(name, ":")
if len(parts) < 4 {
continue
}
deviceLUN, err := strconv.Atoi(parts[3])
if err != nil || deviceLUN != lun {
continue
}
vendorPath := filepath.Join(sysPath, name, "vendor")
vendorBytes, err := os.ReadFile(vendorPath)
if err != nil {
continue
}
vendor := strings.TrimSpace(string(vendorBytes))
if vendor != "QEMU" && strings.ToUpper(vendor) != "MSFT" {
continue
}
blockDir := filepath.Join(sysPath, name, "block")
devices, err := os.ReadDir(blockDir)
if err != nil || len(devices) == 0 {
continue
}
devName := devices[0].Name()
for _, devLinkPath := range []string{"/dev/disk/by-id/", "/dev/disk/by-path/"} {
if link, err := findDiskLink(devLinkPath, devName); err == nil {
return link, nil
}
}
return "/dev/" + devName, nil
}
return "", fmt.Errorf("failed to find disk by lun %d", lun)
}
func findDiskLink(devLinkPath, devName string) (string, error) {
entries, err := os.ReadDir(devLinkPath)
if err != nil {
return "", err
}
for _, entry := range entries {
linkPath := filepath.Join(devLinkPath, entry.Name())
target, err := os.Readlink(linkPath)
if err != nil {
continue
}
if strings.HasSuffix(target, devName) {
return linkPath, nil
}
}
return "", fmt.Errorf("device %s not found under %s", devName, devLinkPath)
}
func formatAndMount(source, target, fsType string, options []string, mounter *mount.SafeFormatAndMount) error {
return mounter.FormatAndMount(source, target, fsType, options)
}
func cleanupMountPoint(target string, mounter *mount.SafeFormatAndMount) error {
return mount.CleanupMountPoint(target, mounter, true)
}
func bindMount(source, target string) error {
out, err := exec.Command("mount", "--bind", source, target).CombinedOutput()
if err != nil {
return fmt.Errorf("bind mount %s -> %s: %w: %s", source, target, err, strings.TrimSpace(string(out)))
}
return nil
}
func isMounted(path string) bool {
out, err := exec.Command("findmnt", "-n", path).CombinedOutput()
return err == nil && strings.TrimSpace(string(out)) != ""
}
+39
View File
@@ -0,0 +1,39 @@
//go:build !linux
package driver
import (
"fmt"
mount "k8s.io/mount-utils"
utilexec "k8s.io/utils/exec"
)
const defaultLinuxFsType = "ext4"
func newMounter() *mount.SafeFormatAndMount {
return mount.NewSafeFormatAndMount(mount.New(""), utilexec.New())
}
func findDiskByLUN(lun int) (string, error) {
return "", fmt.Errorf("scsi volume mount is only supported on linux (lun %d)", lun)
}
func formatAndMount(source, target, fsType string, options []string, mounter *mount.SafeFormatAndMount) error {
return mounter.FormatAndMount(source, target, fsType, options)
}
func cleanupMountPoint(target string, mounter *mount.SafeFormatAndMount) error {
return mount.CleanupMountPoint(target, mounter, true)
}
func bindMount(source, target string) error {
m := newMounter()
return m.Mount(source, target, "", []string{"bind"})
}
func isMounted(path string) bool {
m := newMounter()
notMnt, err := m.IsLikelyNotMountPoint(path)
return err == nil && !notMnt
}
+165
View File
@@ -0,0 +1,165 @@
package driver
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/container-storage-interface/spec/lib/go/csi"
"github.com/KubelanCloud/kks-csi-plugin/pkg/csi/provisioner"
)
type NodeServer struct {
d *Driver
}
func newNodeServer(d *Driver) *NodeServer {
return &NodeServer{d: d}
}
func (s *NodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) {
_ = ctx
if req.GetVolumeId() == "" {
return nil, invalidArgument("volume id is required")
}
if req.GetStagingTargetPath() == "" {
return nil, invalidArgument("staging target path is required")
}
if isBlockVolume(req.GetVolumeCapability()) {
return nil, unimplemented("block volumes are not supported")
}
lun, err := lunFromPublishContext(req.GetPublishContext())
if err != nil {
return nil, invalidArgument(err.Error())
}
device, err := findDiskByLUN(lun)
if err != nil {
return nil, internalError(err)
}
if err := os.MkdirAll(req.GetStagingTargetPath(), 0o755); err != nil {
return nil, internalError(fmt.Errorf("create staging path: %w", err))
}
fsType := defaultLinuxFsType
options := []string{}
if mnt := req.GetVolumeCapability().GetMount(); mnt != nil {
if mnt.FsType != "" {
fsType = mnt.FsType
}
options = append(options, mnt.MountFlags...)
}
mounter := newMounter()
if err := formatAndMount(device, req.GetStagingTargetPath(), fsType, options, mounter); err != nil {
return nil, internalError(fmt.Errorf("format and mount %s: %w", device, err))
}
return &csi.NodeStageVolumeResponse{}, nil
}
func (s *NodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) {
_ = ctx
if req.GetVolumeId() == "" {
return nil, invalidArgument("volume id is required")
}
if req.GetStagingTargetPath() == "" {
return nil, invalidArgument("staging target path is required")
}
mounter := newMounter()
if err := cleanupMountPoint(req.GetStagingTargetPath(), mounter); err != nil {
return nil, internalError(err)
}
return &csi.NodeUnstageVolumeResponse{}, nil
}
func (s *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
_ = ctx
if req.GetVolumeId() == "" {
return nil, invalidArgument("volume id is required")
}
if req.GetTargetPath() == "" {
return nil, invalidArgument("target path is required")
}
if req.GetStagingTargetPath() == "" {
return nil, invalidArgument("staging target path is required")
}
if err := os.MkdirAll(filepath.Dir(req.GetTargetPath()), 0o755); err != nil {
return nil, internalError(err)
}
if err := bindMount(req.GetStagingTargetPath(), req.GetTargetPath()); err != nil {
return nil, internalError(err)
}
return &csi.NodePublishVolumeResponse{}, nil
}
func (s *NodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) {
_ = ctx
if req.GetVolumeId() == "" {
return nil, invalidArgument("volume id is required")
}
if req.GetTargetPath() == "" {
return nil, invalidArgument("target path is required")
}
mounter := newMounter()
if err := cleanupMountPoint(req.GetTargetPath(), mounter); err != nil {
return nil, internalError(err)
}
return &csi.NodeUnpublishVolumeResponse{}, nil
}
func (s *NodeServer) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("NodeGetVolumeStats is not supported")
}
func (s *NodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) {
_ = ctx
_ = req
return nil, unimplemented("NodeExpandVolume is not supported")
}
func (s *NodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
_ = ctx
_ = req
return &csi.NodeGetCapabilitiesResponse{
Capabilities: []*csi.NodeServiceCapability{
nodeCapability(csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME),
},
}, nil
}
func (s *NodeServer) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) {
_ = ctx
_ = req
return &csi.NodeGetInfoResponse{
NodeId: s.d.cfg.Driver.NodeID,
}, nil
}
func lunFromPublishContext(publishContext map[string]string) (int, error) {
lunStr := strings.TrimSpace(publishContext[provisioner.PublishContextLUN])
if lunStr == "" {
return 0, fmt.Errorf("publish context %q is required", provisioner.PublishContextLUN)
}
lun, err := strconv.Atoi(lunStr)
if err != nil {
return 0, fmt.Errorf("invalid publish context %q: %w", provisioner.PublishContextLUN, err)
}
return lun, nil
}
func isBlockVolume(capability *csi.VolumeCapability) bool {
return capability != nil && capability.GetBlock() != nil
}
+7
View File
@@ -0,0 +1,7 @@
package driver
import "github.com/KubelanCloud/kks-csi-plugin/pkg/csi/volume"
func sanitizeVolumeName(name string) string {
return volume.SanitizeName(name)
}