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

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

使用方法
- 克隆代码,https://github.com/ErikXu/kubernetes-filesystem
- 安装 docker
- 执行 bash build.sh 指令
- 执行 bash pack.sh 指令
- 下载 kubectl 并保存到 /usr/local/bin/kubectl
- 执行 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"; ...
}
}
构造函数主要创建 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);
}
}
}
获取集群列表,直接返回 cluster json list。

[HttpGet]
public IActionResult List()
{
return Ok(_clusters);
}
获取指定集群的详情信息,并读取 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);
}
获取指定集群的版本信息,主要使用 .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);
}
创建集群,主要是上传集群证书,并把集群信息序列化成 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();
}
更新集群,主要是更新集群证书及集群信息。

[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();
}
删除集群,主要是删除集群信息,并清理集群证书。

[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();
}
使用 .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);
}
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);
}
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);
}
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);
}
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);
}
再看一下指令 "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;
}
上传文件主要是把文件上传到服务器,再使用 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 });
}
下载文件主要是使用 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);
}
一个小插曲
有个哥们提了一个 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);
}
但意料之外的是,kubectl debug 里获取到的文件列表和容器的文件列表不一致,因此,如果想使用旧版本的方式,请使用 d293e34 版本的代码。
Cluster.razor 和 File.razor
界面部分相对比较简单,基本就是 Bootstrap 和一些 http client 的调用,请大家自行去查阅即可,有问题可以留言讨论。
项目地址
https://github.com/ErikXu/kubernetes-filesystem
欢迎大家 star,提 pr,提 issue,在文章留言交流,或者在公众号 - 跬步之巅留言交流。
使用 .net + blazor 做一个 kubernetes 开源文件系统的更多相关文章
- 想做一个整合开源安全代码扫描工具的代码安全分析平台 - Android方向调研
想做一个整合开源安全代码扫描工具的代码安全分析平台 - Android方向调研 http://blog.csdn.net/testing_is_believing/article/details/22 ...
- 【炫丽】从0开始做一个WPF+Blazor对话小程序
大家好,我是沙漠尽头的狼. .NET是免费,跨平台,开源,用于构建所有应用的开发人员平台. 本文演示如何在WPF中使用Blazor开发漂亮的UI,为客户端开发注入新活力. 注 要使WPF支持Blazo ...
- 致敬学长!J20航模遥控器开源项目计划【开局篇】 | 先做一个开机界面 | MATLAB图像二值化 | Img2Lcd图片取模 | OLED显示图片
我们的开源宗旨:自由 协调 开放 合作 共享 拥抱开源,丰富国内开源生态,开展多人运动,欢迎加入我们哈~ 和一群志同道合的人,做自己所热爱的事! 项目开源地址:https://github.com/C ...
- 用python做一个搜索引擎(Pylucene)
什么是搜索引擎? 搜索引擎是“对网络信息资源进行搜集整理并提供信息查询服务的系统,包括信息搜集.信息整理和用户查询三部分”.如图1是搜索引擎的一般结构,信息搜集模块从网络采集信息到网络信息库之中(一般 ...
- 我发起了一个 .Net 开源 数据库 项目 SqlNet
大家好 , 我发起了一个 .Net 开源 数据库 项目 SqlNet . 项目计划 是 用 C# 写一个 关系数据库 . 可以先参考我之前写的 2 篇文章 : 谈谈数据库原理 https://w ...
- Kubernetes 学习笔记(二):本地部署一个 kubernetes 集群
前言 前面用到过的 minikube 只是一个单节点的 k8s 集群,这对于学习而言是不够的.我们需要有一个多节点集群,才能用到各种调度/监控功能.而且单节点只能是一个加引号的"集群&quo ...
- 微软云创益大赛获奖团队风采:做一个中国特色的.Net源代码社区
为了强化云技术,落地云应用,彰显云价值,微软(中国)携手中国计算机报举办了“微软Cloud OS第二届云创益大赛”.本届大赛历时111天,共吸引了6647位个人组选手回答了70,078道题,59支参赛 ...
- 招聘:有兴趣做一个与Android对等的操作系统么?
招聘:有兴趣做一个与Android对等的操作系统么? 前不久我发了一篇<八一八招聘的那些事儿>讲了我自己作为求职者对招聘的一些看法.那个时候我还在求职,对求职的结果还是挺满意的,五家公司面 ...
- fir.im Weekly - 如何做一个出色的程序员
做一个出色的程序员,困难而高尚.本期 fir.im Weekly 精选了一些实用的 iOS,Android 开发工具和源码分享,还有一些关于程序员的成长 Tips 和有意思有质量的线下活动~ How ...
随机推荐
- 在 Spring AOP 中,关注点和横切关注的区别是什么?
关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的 一个功能. 横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应 用,比如日志,安全和数据传输,几乎应用的每个 ...
- windows 添加路由表
route print 查看路由表 route add 192.168.4.0 mask 255.255.255.0 192.168.18.111 添加路由 rou ...
- vsftd及虚拟用户
临时需要搭建一个ftp,突然忘记怎么搞了,重新整一下,以后备用 vsftd及虚拟用户 1.安装vsftpd yum install vsftpd 2.添加用户(用于虚拟用户映射) adduser se ...
- 运筹学之"连通图"和"最小枝杈树"和"最短路线问题"
一.连通图 必须每个点都有关系 图1 不算连通图 图2含有圈v1,v2,v5,可优化 图3就是所需的连通图 注意:图>连通图>树 二.最小枝杈树 获取是所有节点的最小值,只要是连通图就好, ...
- 汽车中的低速can和高速can的区别
转自:https://zhuanlan.zhihu.com/p/33212308
- getch()函数的使用方法及其返回值问题
getch()函数依赖于头文件 conio.h .会在windows平台下从控制台无回显地取一个字符,并且返回读取到的字符. 然而,我在实际用这个函数才发现getch()这个函数并不简单. getch ...
- javascript新手实例1-DOM基本操作
学习javascript好多同学不知道怎么上手,跟着网上的新手教程做了一遍又觉得javascript很简单,但是真正自己用起来又觉得写不出什么东西,我觉得学习最好的方法就是跟着有趣的例子做,所以我们的 ...
- nginx开启gzip和缓存配置
# 开启gzip gzip on; # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩 gzip_min_length 1k; # gzip 压缩级别,1-10,数字越大压缩的越好,也越占用C ...
- 关于mui中一个页面有有多个页签进行切换的下拉刷新加搜索问题
此图是最近做的项目中的一页,用的是mui结合vue,用了mui后,觉得是真心难用啊,先不说其他的,就光这个下拉刷新就让人奔溃了,问题层出不穷,不过最后经过努力还是摆平了哈. 1.每次切换到新的标签,都 ...
- 预排序遍历算法(MPTT)
预排序遍历算法(MPTT) 算法详细: 对于所有的树的节点,都会有一个左值和一个右值,用于确定该节点的边界. 父节点的左值都会比子节点左值的小,右值都会比子节点的右值大. 没有父节点新增:即没有父节点 ...