使用 .net + blazor 做一個 kubernetes 開原始檔系統

程式設計玩家發表於2022-05-06

背景

據我所知,目前 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。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
namespace WebApi
{
    public class Program
    {
        public static readonly string ConfigDir = "/root/k8s-config";

        ...
    }
}
View Code

 

建構函式主要建立 cluster json 檔案及目錄,並把 json 內容反序列化成 cluster list。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
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。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[HttpGet]
public IActionResult List()
{
    return Ok(_clusters);
}
View Code

 

獲取指定叢集的詳情資訊,並讀取 kubernetes 證書資訊進行展示。

使用 .net + blazor 做一個 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 指令獲取版本資訊。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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,並儲存到檔案。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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

 

更新叢集,主要是更新叢集證書及叢集資訊。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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

 

刪除叢集,主要是刪除叢集資訊,並清理叢集證書。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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 指令的輔助函式。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
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 獲取叢集的名稱空間列表。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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 列表,用於級聯下拉選單。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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 裡的容器列表,用於級聯下拉選單。

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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:

使用 .net + blazor 做一個 kubernetes 開原始檔系統
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 列合成。

因此,解析程式碼如下:

使用 .net + blazor 做一個 kubernetes 開原始檔系統
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 指令把檔案拷貝到指定容器中:

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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 指令把檔案從容器拷貝到伺服器,再把檔案讀取下載:

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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 的問題。因此在新版本獲取檔案列表的方法中,我也實現了該指令:

使用 .net + blazor 做一個 kubernetes 開原始檔系統
[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,在文章留言交流,或者在公眾號 - 跬步之巔留言交流。

相關文章