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

2023-11-17

背景
据我所知,目前 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 _clusters;

public ClustersController()
{
_clusters = new List()
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 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 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 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 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 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 ToFiles(string text)
{
var files = new List();

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 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 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 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 版本的代码。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 .net + blazor 做一个 kubernetes 开源文件系统 的相关文章

随机推荐

  • Gartner发布5G网络基础设施魔力象限报告

    昨天 国际权威分析机构Gartner发布了首份 通信运营商5G网络基础设施魔力象限 报告 Gartner通信运营商5G网络基础设施魔力象限 该魔力象限帮助通信服务提供商识别和评估其5G网络基础设施的网络设备提供商 这种端到端5G网络基础设施
  • 为什么样本方差里面要除以(n-1)而不是n?

    前段日子重新整理了一下这个问题的解答 跟大家分享一下 如果有什么错误的话希望大家能够提出来 我会及时改正的 话不多说进入正题 首先 我们来看一下样本方差的计算公式 刚开始接触这个公式的话可能会有一个疑问就是 为什么样本方差要除以 n 1 而
  • LIME(可解释性分析方法)

    目录 1 什么是LIME 2 思路 3 LIME在不同任务中的范式 待补充 1 什么是LIME 简单理解 对于分类任务 如下图所示 LIME可以列出分类结果 所依据特征对应给比重 对于图像分类任务 如下图所示 可以标记出 分类结果对应的特征
  • 西门子S7-200PLC基本入门编程

    1 PLC控制电动机的启保停电路 实验要求 按下启动开关时 电动机正常启动并且保持正常持续工作 按下停止按钮时电动机立刻停止工作 PLCI O分配表 输入量 输出量 I0 0 电动机启动按钮 Q0 0 电动机运行 I0 1 电动机停止按钮
  • Tomcat配置内存和远程debug端口

    配置内存 需要在catalina bat中添加JAVA OPTS参数 如下内容 SET JAVA OPTS Xms256m Xmx1024m XX MaxNewSize 256m XX MaxPermSize 428m Duser time
  • 【Matlab】常用函数汇总(二)

    Matlab 是矩阵实验室 Matrix Laboratory 的英文缩写 是用于科学与工程计算的工具 Matlab 提供了许多常用的数学函数 本文主要介绍 Matlab 与统计 排序 求和与乘积 以及随机数相关的函数 目录 1 统计函数
  • Python支持向量回归SVR拟合、预测回归数据和可视化准确性检查实例

    最近我们被客户要求撰写关于支持向量回归的研究报告 包括一些图形和统计输出 支持向量回归 SVR 是一种回归算法 它应用支持向量机 SVM 的类似技术进行回归分析 正如我们所知 回归数据包含连续的实数 为了拟合这种类型的数据 SVR模型在考虑
  • 软件工程学习日记(4)----面向数据流的设计方法

    用面向数据流的方法设计下列系统的软件结构 问题回顾 为方便储户 某银行拟开发计算机储蓄系统 储户填写的存款单或取款单由业务员输入系统 如果是存款 系统记录存款人姓名 住址 存款类型 存款日期 利率等信息 并印出存款单给储户 如果是取款 系统
  • 四元组与旋转矩阵

    转自 https blog csdn net linuxheik article details 49129927 引用 四元组与旋转矩阵 2011 09 22 17 13 39 分类 DirectX资料 举报 字号 订阅 下载LOFTER
  • halcon起步

    halcon起步 安装 软件介绍 安装 下载地址 管理员方式运行 选择安装 否 复制dll文件 D Program Files MVTec HALCON 12 0 bin x64 win64 重启计算机 软件介绍 打开药品识别例程 导出为c
  • vue3报错:‘xxxx‘is declared but its value is never read.Vetur(6133)

    原因 因为vue3不支持vetur了 解决办法 1 禁用或者删除vscode中的vetur扩展 2 下载Vue Language Features 3 重新打开项目 完美解决
  • 医学图像相关的数据集

    医学图像相关的数据集 1 Camelyon 乳腺病理 数据集获取 参考 博文地址 相关文章推荐 预处理
  • Qt QString字符串分割、截取的3种方法

    Qt QString字符串分割 截取 在做项目中不可避免的会使用到一串字符串中的一段字符 因此常常需要截取字符串 有两种方式可以解决这个问题 方法一 QString分割字符串 QString date dateEdit toString y
  • Log4Net使用实例(VS2008 App)

    准备工作 首先要去http logging apache org log4net 下载log4net的源代码 将log4net sln载入Visual Studio NET 编译后可以得到log4net dll 也可以直接在网上搜索下载别人
  • CI/CD(持续集成/持续交付/持续部署)

    CICD流程图 代码管理仓库gitlab gitlab是个私有的代码管理仓库 可以运行在企业内部的网络中 使企业开发人员可以保持代码的私有性 同时也方便自行管理代码 gitlab有很多CI功能 但是通常还是采用Jenkins 原因就是Jen
  • 淘宝用户日志数据集的用户行为分析与用户分群

    文章目录 数据集描述 一 数据清洗 1 读取并查看数据基本信息和数据的完整性 2 查看数据集中行的重复情况并删除 3 处理缺失值 4 合并month和day列组成时间类型的date列 5 划分子数据集 二 数据分析 1 访问量与访客量的情况
  • 嵌入式开发4(I.MX6U串口实验与ubuntu串口调试助手)

    在学习正点原子6UL嵌入式开发板的时候 串口UART是一个很重要的点 在以后的实验中会经常遇到 但是教学中是在windows环境下搭建ubuntu虚拟机来编译代码的 串口调试助手使用的是windows版本的 而我是安装了双系统 所以研究了一
  • visual studio中配置OpenCVsharp

    只能在线下载 每次新建项目就要下载一次 没找到离线下载的方式 很可恶 visual studio2019 C 语言 配置OpenCVsharp当前最新版 4 6 0 在浏览界面搜索OpenCVsharp 下载OpenCVsharp4和对应r
  • unity 路径

    IOS Application dataPath Application xxxxxxxx xxxx xxxx xxxx xxxxxxxxxxxx xxx app Data Application streamingAssetsPath A
  • 使用 .net + blazor 做一个 kubernetes 开源文件系统

    背景 据我所知 目前 kubernetes 本身或者其它第三方社区都没提供 kubernetes 的文件系统 也就是说要从 kubernetes 的容器中下载或上传文件 需要先进入容器查看目录结构 然后再通过 kubectl cp 指令把文