背景

据我所知,目前 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"; ...
}
}

构造函数主要创建 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 开源文件系统的更多相关文章

  1. 想做一个整合开源安全代码扫描工具的代码安全分析平台 - Android方向调研

    想做一个整合开源安全代码扫描工具的代码安全分析平台 - Android方向调研 http://blog.csdn.net/testing_is_believing/article/details/22 ...

  2. 【炫丽】从0开始做一个WPF+Blazor对话小程序

    大家好,我是沙漠尽头的狼. .NET是免费,跨平台,开源,用于构建所有应用的开发人员平台. 本文演示如何在WPF中使用Blazor开发漂亮的UI,为客户端开发注入新活力. 注 要使WPF支持Blazo ...

  3. 致敬学长!J20航模遥控器开源项目计划【开局篇】 | 先做一个开机界面 | MATLAB图像二值化 | Img2Lcd图片取模 | OLED显示图片

    我们的开源宗旨:自由 协调 开放 合作 共享 拥抱开源,丰富国内开源生态,开展多人运动,欢迎加入我们哈~ 和一群志同道合的人,做自己所热爱的事! 项目开源地址:https://github.com/C ...

  4. 用python做一个搜索引擎(Pylucene)

    什么是搜索引擎? 搜索引擎是“对网络信息资源进行搜集整理并提供信息查询服务的系统,包括信息搜集.信息整理和用户查询三部分”.如图1是搜索引擎的一般结构,信息搜集模块从网络采集信息到网络信息库之中(一般 ...

  5. 我发起了一个 .Net 开源 数据库 项目 SqlNet

    大家好 , 我发起了一个 .Net 开源 数据库 项目 SqlNet . 项目计划 是 用 C# 写一个 关系数据库 . 可以先参考我之前写的 2 篇文章 : 谈谈数据库原理    https://w ...

  6. Kubernetes 学习笔记(二):本地部署一个 kubernetes 集群

    前言 前面用到过的 minikube 只是一个单节点的 k8s 集群,这对于学习而言是不够的.我们需要有一个多节点集群,才能用到各种调度/监控功能.而且单节点只能是一个加引号的"集群&quo ...

  7. 微软云创益大赛获奖团队风采:做一个中国特色的.Net源代码社区

    为了强化云技术,落地云应用,彰显云价值,微软(中国)携手中国计算机报举办了“微软Cloud OS第二届云创益大赛”.本届大赛历时111天,共吸引了6647位个人组选手回答了70,078道题,59支参赛 ...

  8. 招聘:有兴趣做一个与Android对等的操作系统么?

    招聘:有兴趣做一个与Android对等的操作系统么? 前不久我发了一篇<八一八招聘的那些事儿>讲了我自己作为求职者对招聘的一些看法.那个时候我还在求职,对求职的结果还是挺满意的,五家公司面 ...

  9. fir.im Weekly - 如何做一个出色的程序员

    做一个出色的程序员,困难而高尚.本期 fir.im Weekly 精选了一些实用的 iOS,Android 开发工具和源码分享,还有一些关于程序员的成长 Tips 和有意思有质量的线下活动~ How ...

随机推荐

  1. 在 Spring AOP 中,关注点和横切关注的区别是什么?

    关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的 一个功能. 横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应 用,比如日志,安全和数据传输,几乎应用的每个 ...

  2. windows 添加路由表

    route print   查看路由表 route  add      192.168.4.0  mask 255.255.255.0        192.168.18.111   添加路由 rou ...

  3. vsftd及虚拟用户

    临时需要搭建一个ftp,突然忘记怎么搞了,重新整一下,以后备用 vsftd及虚拟用户 1.安装vsftpd yum install vsftpd 2.添加用户(用于虚拟用户映射) adduser se ...

  4. 运筹学之"连通图"和"最小枝杈树"和"最短路线问题"

    一.连通图 必须每个点都有关系 图1 不算连通图 图2含有圈v1,v2,v5,可优化 图3就是所需的连通图 注意:图>连通图>树 二.最小枝杈树 获取是所有节点的最小值,只要是连通图就好, ...

  5. 汽车中的低速can和高速can的区别

    转自:https://zhuanlan.zhihu.com/p/33212308

  6. getch()函数的使用方法及其返回值问题

    getch()函数依赖于头文件 conio.h .会在windows平台下从控制台无回显地取一个字符,并且返回读取到的字符. 然而,我在实际用这个函数才发现getch()这个函数并不简单. getch ...

  7. javascript新手实例1-DOM基本操作

    学习javascript好多同学不知道怎么上手,跟着网上的新手教程做了一遍又觉得javascript很简单,但是真正自己用起来又觉得写不出什么东西,我觉得学习最好的方法就是跟着有趣的例子做,所以我们的 ...

  8. nginx开启gzip和缓存配置

    # 开启gzip gzip on; # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩 gzip_min_length 1k; # gzip 压缩级别,1-10,数字越大压缩的越好,也越占用C ...

  9. 关于mui中一个页面有有多个页签进行切换的下拉刷新加搜索问题

    此图是最近做的项目中的一页,用的是mui结合vue,用了mui后,觉得是真心难用啊,先不说其他的,就光这个下拉刷新就让人奔溃了,问题层出不穷,不过最后经过努力还是摆平了哈. 1.每次切换到新的标签,都 ...

  10. 预排序遍历算法(MPTT)

    预排序遍历算法(MPTT) 算法详细: 对于所有的树的节点,都会有一个左值和一个右值,用于确定该节点的边界. 父节点的左值都会比子节点左值的小,右值都会比子节点的右值大. 没有父节点新增:即没有父节点 ...