• 使用 .net + blazor 做一个 kubernetes 开源文件系统


    背景

    据我所知,目前 kubernetes 本身或者其它第三方社区都没提供 kubernetes 的文件系统。也就是说要从 kubernetes 的容器中下载或上传文件,需要先进入容器查看目录结构,然后再通过 kubectl cp 指令把文件拷贝进或出容器。虽然说不太麻烦,但也不太方便。当时正好推出 .net 5 + blazor,就趁着这个机会使用 .net 5 + blazor 做一个 kubernetes 的开源文件系统。

     

    界面简介

    创建集群

    创建集群其实就是上传需要接管的 kubernetes 的 kubeconfig,并给集群取个帮助区分的名字:

     

    浏览、上传、下载文件

    创建完集群后,就可以方便地选择集群 -> 命名空间 -> Pod -> 容器,然后浏览容器目录,上传文件到容器,或者下载文件到本地:

     

    使用方法

    1. 克隆代码,https://github.com/ErikXu/kubernetes-filesystem
    2. 安装 docker
    3. 执行 bash build.sh 指令
    4. 执行 bash pack.sh 指令
    5. 下载 kubectl 并保存到 /usr/local/bin/kubectl
    6. 执行 bash run.sh 指令

     

    代码目录

    ├── build.sh                                  # 构建脚本
    ├── docker                                    # docker 目录
    │   └── Dockerfile                            # Dockerfile
    ├── pack.sh                                   # 打包脚本
    ├── publish.sh                                # 发布脚本
    ├── README_CN.md                              # 项目说明(中文)
    ├── README.md                                 # 项目说明
    ├── run.sh                                    # 运行脚本
    └── src                                       # 源码目录
        ├── Kubernetes.Filesystem.sln             # 解决方案
        ├── Web                                   # Web 项目
        │   ├── App.razor                         # 入口 APP
        │   ├── _Imports.razor                    # 引用文件
        │   ├── Pages
        │   │   ├── Cluster.razor                 # 集群管理页面
        │   │   └── File.razor                    # 文件管理页面
        │   ├── Shared
        │   │   ├── MainLayout.razor              # 布局文件
        │   │   ├── MainLayout.razor.css          # 布局样式文件
        │   │   ├── NavMenu.razor                 # 导航文件
        │   │   ├── NavMenu.razor.css             # 导航样式文件
        │   │   └── SurveyPrompt.razor            # 调查弹出框
        │   ├── Web.csproj                        # Web 项目文件
        │   └── wwwroot
        │       ├── css                           # 样式文件夹
        │       ├── favicon.ico                   # icon 文件
        │       └── index.html                    # html 入口页
        └── WebApi                                # WebApi 项目
            ├── appsettings.Development.json      # 开发环境配置文件
            ├── appsettings.json                  # 配置文件
            ├── Controllers                       # 控制器目录
            │   ├── ClustersController.cs         # 集群控制器
            │   ├── ContainersController.cs       # 容器控制器
            │   ├── FilesController.cs            # 文件控制器
            │   ├── NamespacesController.cs       # 命名空间控制器
            │   └── PodsController.cs             # Pod 控制器
            ├── Startup.cs                        # Startup 文件
            └── WebApi.csproj                     # WebApi 项目文件

     

    代码简析

    ClustersController

    ClustersController 主要对集群进行管理,集群信息使用 json 文件存储,路径为:/root/k8s-config。

    namespace WebApi
    {
        public class Program
        {
            public static readonly string ConfigDir = "/root/k8s-config";
    
            ...
        }
    }
    View Code

     

    构造函数主要创建 cluster json 文件及目录,并把 json 内容反序列化成 cluster list。

    private readonly string _configName = "cluster.json";
    private List<Cluster> _clusters;
    
    public ClustersController()
    {
        _clusters = new List<Cluster>()
        if (!Directory.Exists(Program.ConfigDir))
        {
            Directory.CreateDirectory(Program.ConfigDir);
        
        var configPath = Path.Combine(Program.ConfigDir, _configName);
        if (!System.IO.File.Exists(configPath))
        {
            var json = JsonConvert.SerializeObject(_clusters);
            System.IO.File.WriteAllText(configPath, json);
        }
        else
        {
            var json = System.IO.File.ReadAllText(configPath);
            if (!string.IsNullOrWhiteSpace(json))
            {
                _clusters = JsonConvert.DeserializeObject<List<Cluster>>(json);
            }
        }
    }
    View Code

     

    获取集群列表,直接返回 cluster json list。

    [HttpGet]
    public IActionResult List()
    {
        return Ok(_clusters);
    }
    View Code

     

    获取指定集群的详情信息,并读取 kubernetes 证书信息进行展示。

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(string id)
    {
        var cluster = _clusters.SingleOrDefault(n => n.Id == id);
        if (cluster == null)
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var certificatePath = Path.Combine(Program.ConfigDir, cluster.Name);
        var certificate = await System.IO.File.ReadAllTextAsync(certificatePath);
        cluster.Certificate = certificate;
    
        return Ok(cluster);
    }
    View Code

     

    获取指定集群的版本信息,主要使用 .net process + kubernetes 的证书执行 kubectl version --short 指令获取版本信息。

    [HttpGet("{id}/version")]
    public IActionResult GetVersion(string id)
    {
        var cluster = _clusters.SingleOrDefault(n => n.Id == id);
        if (cluster == null)
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var configPath = Path.Combine(Program.ConfigDir, cluster.Name.ToLower());
    
        var command = $"kubectl version --short --kubeconfig {configPath}";
    
        var (code, message) = ExecuteCommand(command);
    
        if (code != 0)
        {
            return StatusCode(StatusCodes.Status500InternalServerError, new { Message = message });
        }
    
        var lines = message.Split(Environment.NewLine);
    
        var version = new ClusterVersion
        {
            Client = lines[0].Replace("Client Version:", string.Empty).Trim(),
            Server = lines[1].Replace("Server Version:", string.Empty).Trim()
        };
    
        version.ClientNum = double.Parse(version.Client.Substring(1, 4));
        version.ServerNum = double.Parse(version.Server.Substring(1, 4));
    
        return Ok(version);
    }
    View Code

     

    创建集群,主要是上传集群证书,并把集群信息序列化成 json,并保存到文件。

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] Cluster cluster)
    {
        cluster.Name = cluster.Name.ToLower();
        if (_clusters.Any(n => n.Name.Equals(cluster.Name)))
        {
            return BadRequest(new { Message = "Cluster name is existed!" });
        }
    
        var certificatePath = Path.Combine(Program.ConfigDir, cluster.Name);
        await System.IO.File.WriteAllTextAsync(certificatePath, cluster.Certificate);
    
        cluster.Id = Guid.NewGuid().ToString();
        cluster.Certificate = string.Empty;
        _clusters.Add(cluster);
        var json = JsonConvert.SerializeObject(_clusters);
        var configPath = Path.Combine(Program.ConfigDir, _configName);
        await System.IO.File.WriteAllTextAsync(configPath, json);
    
        return Ok();
    }
    View Code

     

    更新集群,主要是更新集群证书及集群信息。

    [HttpPut("{id}")]
    public async Task<IActionResult> Update(string id, [FromBody] Cluster form)
    {
        var cluster = _clusters.SingleOrDefault(n => n.Id == id);
        if (cluster == null)
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var certificatePath = Path.Combine(Program.ConfigDir, cluster.Name);
        System.IO.File.Delete(certificatePath);
    
        cluster.Name = form.Name;
        certificatePath = Path.Combine(Program.ConfigDir, form.Name);
        await System.IO.File.WriteAllTextAsync(certificatePath, form.Certificate);
    
        var json = JsonConvert.SerializeObject(_clusters);
        var configPath = Path.Combine(Program.ConfigDir, _configName);
        await System.IO.File.WriteAllTextAsync(configPath, json);
    
        return Ok();
    }
    View Code

     

    删除集群,主要是删除集群信息,并清理集群证书。

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(string id)
    {
        var cluster = _clusters.SingleOrDefault(n => n.Id == id);
        if (cluster == null)
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        _clusters.Remove(cluster);
    
        var certificatePath = Path.Combine(Program.ConfigDir, cluster.Name);
        System.IO.File.Delete(certificatePath);
    
        var configPath = Path.Combine(Program.ConfigDir, _configName);
        var json = JsonConvert.SerializeObject(_clusters);
        await System.IO.File.WriteAllTextAsync(configPath, json);
    
        return NoContent();
    }
    View Code

     

    使用 .net process 执行 linux 指令的辅助函数。

    private static (int, string) ExecuteCommand(string command)
    {
        var escapedArgs = command.Replace("\"", "\\\"");
        var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "/bin/sh",
                Arguments = $"-c \"{escapedArgs}\"",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            }
        };
    
        process.Start();
        process.WaitForExit();
    
        var message = process.StandardOutput.ReadToEnd();
        if (process.ExitCode != 0)
        {
            message = process.StandardError.ReadToEnd();
        }
    
        return (process.ExitCode, message);
    }
    View Code

     

    NamespacesController

    NamespacesController 比较简单,主要是使用 kubernetes 证书 + kubernetes api 获取集群的命名空间列表。

    [HttpGet]
    public IActionResult List([FromQuery] string cluster)
    {
        var configPath = Path.Combine(Program.ConfigDir, cluster.ToLower());
        if (!System.IO.File.Exists(configPath))
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var config = KubernetesClientConfiguration.BuildConfigFromConfigFile(configPath);
        var client = new k8s.Kubernetes(config);
        var namespaces = client.ListNamespace().Items.Select(n => n.Metadata.Name);
        return Ok(namespaces);
    }
    View Code

     

    PodsController

    PodsController 也比较简单,主要是获取指定命名空间下的 pod 列表,用于级联下拉菜单。

    [HttpGet]
    public IActionResult List([FromQuery] string cluster, [FromQuery] string @namespace)
    {
        var configPath = Path.Combine(Program.ConfigDir, cluster.ToLower());
        if (!System.IO.File.Exists(configPath))
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var config = KubernetesClientConfiguration.BuildConfigFromConfigFile(configPath);
        var client = new k8s.Kubernetes(config);
        var pods = client.ListNamespacedPod(@namespace).Items.Select(n => n.Metadata.Name);
        return Ok(pods);
    }
    View Code

     

    ContainersController

    ContainersController 也比较简单,主要是获取指定 pod 里的容器列表,用于级联下拉菜单。

    [HttpGet]
    public IActionResult List([FromQuery] string cluster, [FromQuery] string @namespace, [FromQuery] string pod)
    {
        var configPath = Path.Combine(Program.ConfigDir, cluster.ToLower());
        if (!System.IO.File.Exists(configPath))
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var config = KubernetesClientConfiguration.BuildConfigFromConfigFile(configPath);
        var client = new k8s.Kubernetes(config);
        var specificPod = client.ListNamespacedPod(@namespace).Items.Where(n => n.Metadata.Name == pod).First();
        var containers = specificPod.Spec.Containers.Select(n => n.Name);
        return Ok(containers);
    }
    View Code

     

    FilesController

    FilesController 是最重要,同时也是稍微有点复杂的一个控制器。

    获取容器指定路径的文件列表,主要是调用 kubernetes api 的 exec 方法,执行指令 "ls -Alh --time-style long-iso {dir}" 获得文件内容信息。

    由于 exec 是交互式的,所以方法使用的是 web socket:

    public async Task<IActionResult> List([FromQuery] string cluster, [FromQuery] string @namespace, [FromQuery] string pod, [FromQuery] string container, [FromQuery] string dir)
    {
        var configPath = Path.Combine(Program.ConfigDir, cluster.ToLower());
        if (!System.IO.File.Exists(configPath))
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var config = KubernetesClientConfiguration.BuildConfigFromConfigFile(configPath);
        var client = new k8s.Kubernetes(config);
    
        var webSocket = await client.WebSocketNamespacedPodExecAsync(pod, @namespace, new string[] { "ls", "-Alh", "--time-style", "long-iso", dir }, container).ConfigureAwait(false);
        var demux = new StreamDemuxer(webSocket);
        demux.Start();
    
        var buff = new byte[4096];
        var stream = demux.GetStream(1, 1);
        stream.Read(buff, 0, 4096);
        var bytes = TrimEnd(buff);
        var text = System.Text.Encoding.Default.GetString(bytes).Trim();
    
        var files = ToFiles(text);
        return Ok(files);
    }
    View Code

     

    再看一下指令 "ls -Alh --time-style long-iso {dir}" 的一个例子:

    ~ ls -Alh --time-style long-iso /usr/bin
    total 107M
    lrwxrwxrwx.     1 root root        8    2019-02-21 10:47 ypdomainname -> hostname
    -rwxr-xr-x.     1 root root        62K  2018-10-30 17:55 ar
    drwxr-xr-x      8 root root        4.0K 2022-04-01 09:37 scripts

    第一行 total 开头的信息不太重要可以忽略,从第二行可以看出,每一行的格式是固定的。

    第 0 列:权限,l 开头表示是链接,- 开头表示是文件,d 开头表示是文件夹

    第 1 列:link 数量

    第 2 列:用户

    第 3 例:组

    第 4 列:文件(夹)大小

    第 5 列:日期

    第 6 列:时间

    第 7 列:文件(夹)名称

    如果记录类型为 l,则文件名称为 7,8,9 列合成。

    因此,解析代码如下:

    private static List<FileItem> ToFiles(string text)
    {
        var files = new List<FileItem>();
    
        var lines = text.Split(Environment.NewLine);
    
        foreach (var line in lines)
        {
            if (line.StartsWith("total"))
            {
                continue;
            }
            var trimLine = line.Trim();
            var array = trimLine.Split(" ").ToList().Where(n => !string.IsNullOrWhiteSpace(n)).ToList();
    
            var file = new FileItem
            {
                Permission = array[0],
                Links = array[1],
                Owner = array[2],
                Group = array[3],
                Size = array[4],
                Date = array[5],
                Time = array[6],
                Name = array[7]
            };
    
            if (file.Permission.StartsWith("l"))
            {
                file.Name = $"{array[7]} {array[8]} {array[9]}";
            }
            files.Add(file);
        }
    
        return files;
    }
    View Code

     

    上传文件主要是把文件上传到服务器,再使用 kubectl cp 指令把文件拷贝到指定容器中:

    [HttpPost("upload")]
    public async Task<IActionResult> UploadFile([FromQuery] string cluster, [FromQuery] string @namespace, [FromQuery] string pod, [FromQuery] string container, [FromQuery] string dir, IFormFile file)
    {
        var configPath = Path.Combine(Program.ConfigDir, cluster.ToLower());
        if (!System.IO.File.Exists(configPath))
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var tmpPath = Path.Combine("/tmp", System.Guid.NewGuid().ToString());
        await using (var stream = System.IO.File.Create(tmpPath))
        {
            await file.CopyToAsync(stream);
        }
    
        var path = Path.Combine(dir, file.FileName);
        var command = $"kubectl cp {tmpPath} {pod}:{path} -c {container} -n {@namespace} --kubeconfig {configPath}";
        var (code, message) = ExecuteCommand(command);
    
        System.IO.File.Delete(tmpPath);
    
        if (code == 0)
        {
            return Ok();
        }
    
        return StatusCode(StatusCodes.Status500InternalServerError, new { Message = message });
    }
    View Code

     

    下载文件主要是使用 kubectl cp 指令把文件从容器拷贝到服务器,再把文件读取下载:

    [HttpGet("download")]
    public async Task<IActionResult> DownloadFile([FromQuery] string cluster, [FromQuery] string @namespace, [FromQuery] string pod, [FromQuery] string container, [FromQuery] string path)
    {
        var configPath = Path.Combine(Program.ConfigDir, cluster.ToLower());
        if (!System.IO.File.Exists(configPath))
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var tmpPath = Path.Combine("/tmp", System.Guid.NewGuid().ToString());
        var command = $"kubectl cp {pod}:{path} {tmpPath} -c {container} -n {@namespace} --kubeconfig {configPath}";
        var (code, message) = ExecuteCommand(command);
    
        if (code != 0)
        {
            return StatusCode(StatusCodes.Status500InternalServerError, new { Message = message });
        }
    
        var memory = new MemoryStream();
        using (var stream = new FileStream(tmpPath, FileMode.Open))
        {
            await stream.CopyToAsync(memory);
        }
        memory.Position = 0;
    
        var contentType = GetContentType(tmpPath);
    
        System.IO.File.Delete(tmpPath);
    
        return File(memory, contentType);
    }
    View Code

     

    一个小插曲

    有个哥们提了一个 issue 提到 kubernetes 在 1.20 引入了一个新指令 "kubectl debug",目的是为了解决容器中未安装 bash 或者 sh 的问题。因此在新版本获取文件列表的方法中,我也实现了该指令:

    [HttpGet]
    public async Task<IActionResult> List([FromQuery] string cluster, [FromQuery] string @namespace, [FromQuery] string pod, [FromQuery] string container, [FromQuery] string dir)
    {
        var configPath = Path.Combine(Program.ConfigDir, cluster.ToLower());
        if (!System.IO.File.Exists(configPath))
        {
            return BadRequest(new { Message = "Cluster is not existed!" });
        }
    
        var command = $"kubectl version --short --kubeconfig {configPath}";
    
        var (code, message) = ExecuteCommand(command);
    
        if (code != 0)
        {
            return StatusCode(StatusCodes.Status500InternalServerError, new { Message = message });
        }
    
        var lines = message.Split(Environment.NewLine);
    
        var version = new ClusterVersion
        {
            Client = lines[0].Replace("Client Version:", string.Empty).Trim(),
            Server = lines[1].Replace("Server Version:", string.Empty).Trim()
        };
    
        version.ClientNum = double.Parse(version.Client.Substring(1, 4));
        version.ServerNum = double.Parse(version.Server.Substring(1, 4));
    
        var text = string.Empty;
        if (version.ClientNum >= 1.2 && version.ServerNum >= 1.2)
        {
            command = $"kubectl debug -it {pod} -n {@namespace} --image=centos --target={container} --kubeconfig {configPath} -- sh -c 'ls -Alh --time-style long-iso {dir}'";
            (code, message) = ExecuteCommand(command);
    
            if (code != 0)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, new { Message = message });
            }
    
            text = message;
        }
        else
        {
            var config = KubernetesClientConfiguration.BuildConfigFromConfigFile(configPath);
            var client = new k8s.Kubernetes(config);
    
            var webSocket = await client.WebSocketNamespacedPodExecAsync(pod, @namespace, new string[] { "ls", "-Alh", "--time-style", "long-iso", dir }, container).ConfigureAwait(false);
            var demux = new StreamDemuxer(webSocket);
            demux.Start();
    
            var buff = new byte[4096];
            var stream = demux.GetStream(1, 1);
            stream.Read(buff, 0, 4096);
            var bytes = TrimEnd(buff);
            text = System.Text.Encoding.Default.GetString(bytes).Trim();
        }
    
        var files = ToFiles(text);
        return Ok(files);
    }
    View Code

    但意料之外的是,kubectl debug 里获取到的文件列表和容器的文件列表不一致,因此,如果想使用旧版本的方式,请使用 d293e34 版本的代码。

     

    Cluster.razor 和 File.razor

    界面部分相对比较简单,基本就是 Bootstrap 和一些 http client 的调用,请大家自行去查阅即可,有问题可以留言讨论。

     

    项目地址

    https://github.com/ErikXu/kubernetes-filesystem

    欢迎大家 star,提 pr,提 issue,在文章留言交流,或者在公众号 - 跬步之巅留言交流。

  • 相关阅读:
    apache解决请求无法识别Authorization
    【Python自然语言处理】使用SVM、随机森林法、梯度法等多种方法对病人罹患癌症预测实战(超详细 附源码)
    【MySQL高级】MySQL的锁机制
    leetcode 215.数组中第k大的元素
    北大肖臻老师《区块链技术与应用》系列课程学习笔记[20]以太坊-智能合约-1
    Matplotlib:科研绘图利器(写论文、数据可视化必备)
    网页在移动端的适配中单位的选择
    git 分支管理进阶
    一起来部署项目-Linux基本命令
    CURL简单使用
  • 原文地址:https://www.cnblogs.com/Erik_Xu/p/16183765.html