3 Commits

10 changed files with 242 additions and 163 deletions

View File

@@ -17,11 +17,8 @@
<StreamGeometry x:Key="arrow_download_regular">M12.25,39.5 L35.75,39.5 C36.4403559,39.5 37,40.0596441 37,40.75 C37,41.3972087 36.5081253,41.9295339 35.8778052,41.9935464 L35.75,42 L12.25,42 C11.5596441,42 11,41.4403559 11,40.75 C11,40.1027913 11.4918747,39.5704661 12.1221948,39.5064536 L12.25,39.5 L35.75,39.5 L12.25,39.5 Z M23.6221948,6.00645361 L23.75,6 C24.3972087,6 24.9295339,6.49187466 24.9935464,7.12219476 L25,7.25 L25,31.54 L30.6466793,25.8942911 C31.1348346,25.4061358 31.9262909,25.4061358 32.4144462,25.8942911 C32.9026016,26.3824465 32.9026016,27.1739027 32.4144462,27.6620581 L24.6362716,35.4402327 C24.1481163,35.928388 23.35666,35.928388 22.8685047,35.4402327 L15.0903301,27.6620581 C14.6021747,27.1739027 14.6021747,26.3824465 15.0903301,25.8942911 C15.5784855,25.4061358 16.3699417,25.4061358 16.858097,25.8942911 L22.5,31.536 L22.5,7.25 C22.5,6.60279131 22.9918747,6.0704661 23.6221948,6.00645361 L23.75,6 L23.6221948,6.00645361 Z</StreamGeometry> <StreamGeometry x:Key="arrow_download_regular">M12.25,39.5 L35.75,39.5 C36.4403559,39.5 37,40.0596441 37,40.75 C37,41.3972087 36.5081253,41.9295339 35.8778052,41.9935464 L35.75,42 L12.25,42 C11.5596441,42 11,41.4403559 11,40.75 C11,40.1027913 11.4918747,39.5704661 12.1221948,39.5064536 L12.25,39.5 L35.75,39.5 L12.25,39.5 Z M23.6221948,6.00645361 L23.75,6 C24.3972087,6 24.9295339,6.49187466 24.9935464,7.12219476 L25,7.25 L25,31.54 L30.6466793,25.8942911 C31.1348346,25.4061358 31.9262909,25.4061358 32.4144462,25.8942911 C32.9026016,26.3824465 32.9026016,27.1739027 32.4144462,27.6620581 L24.6362716,35.4402327 C24.1481163,35.928388 23.35666,35.928388 22.8685047,35.4402327 L15.0903301,27.6620581 C14.6021747,27.1739027 14.6021747,26.3824465 15.0903301,25.8942911 C15.5784855,25.4061358 16.3699417,25.4061358 16.858097,25.8942911 L22.5,31.536 L22.5,7.25 C22.5,6.60279131 22.9918747,6.0704661 23.6221948,6.00645361 L23.75,6 L23.6221948,6.00645361 Z</StreamGeometry>
<StreamGeometry x:Key="arrow_right_regular">M15.2685,4.20949 C14.97,3.92233 14.4952,3.93153 14.208,4.23005 C13.9209,4.52857 13.9301,5.00335 14.2286,5.29051 L22.5028,13.25 L3.75,13.25 C3.33579,13.25 3,13.5858 3,13.9999982 C3,14.4142 3.33579,14.75 3.75,14.75 L22.5018,14.75 L14.2286,22.7085 C13.9301,22.9957 13.9209,23.4705 14.208,23.769 C14.4952,24.0675 14.97,24.0767 15.2685,23.7896 L24.6965,14.7202 C25.1054,14.3268 25.1054,13.6723 24.6965,13.2788 L15.2685,4.20949 Z</StreamGeometry> <StreamGeometry x:Key="arrow_right_regular">M15.2685,4.20949 C14.97,3.92233 14.4952,3.93153 14.208,4.23005 C13.9209,4.52857 13.9301,5.00335 14.2286,5.29051 L22.5028,13.25 L3.75,13.25 C3.33579,13.25 3,13.5858 3,13.9999982 C3,14.4142 3.33579,14.75 3.75,14.75 L22.5018,14.75 L14.2286,22.7085 C13.9301,22.9957 13.9209,23.4705 14.208,23.769 C14.4952,24.0675 14.97,24.0767 15.2685,23.7896 L24.6965,14.7202 C25.1054,14.3268 25.1054,13.6723 24.6965,13.2788 L15.2685,4.20949 Z</StreamGeometry>
<StreamGeometry x:Key="arrow_sync_circle_regular">M12,2 C17.5228,2 22,6.47715 22,12 C22,17.5228 17.5228,22 12,22 C6.47715,22 2,17.5228 2,12 C2,6.47715 6.47715,2 12,2 Z M12,3.5 C7.30558,3.5 3.5,7.30558 3.5,12 C3.5,16.6944 7.30558,20.5 12,20.5 C16.6944,20.5 20.5,16.6944 20.5,12 C20.5,7.30558 16.6944,3.5 12,3.5 Z M16.75,12 C17.1296833,12 17.4434889,12.2821653 17.4931531,12.6482323 L17.5,12.75 L17.5,15.75 C17.5,16.1642 17.1642,16.5 16.75,16.5 C16.3703167,16.5 16.0565111,16.2178347 16.0068469,15.8517677 L16,15.75 L16,15 C15.0881,16.2143 13.6362,17 11.9999,17 C10.4748,17 9.09587,16.316 8.17857,15.237 C7.91028,14.9214 7.94862,14.4481 8.2642,14.1798 C8.57979,13.9115 9.05311,13.9499 9.3214,14.2655 C9.96322,15.0204 10.9293,15.5 11.9999,15.5 C13.32553,15.5 14.4803167,14.7625672 15.0742404,13.6746351 L15.1633,13.5 L14,13.5 C13.5858,13.5 13.25,13.1642 13.25,12.75 C13.25,12.3703167 13.5321653,12.0565111 13.8982323,12.0068469 L14,12 L16.75,12 Z M11.9999,7 C13.5368,7 14.9041,7.66036 15.8268,8.77062 C16.0915,9.08918 16.0479,9.56205 15.7294,9.8268 C15.4108,10.0916 14.9379,10.0479 14.6732,9.72938 C14.0368,8.96361 13.093,8.5 11.9999,8.5 C10.5754318,8.5 9.34895806,9.35140335 8.80281957,10.5730172 L8.72948,10.75 L10,10.75 C10.4142,10.75 10.75,11.0858 10.75,11.5 C10.75,11.8796833 10.4678347,12.1934889 10.1017677,12.2431531 L10,12.25 L7.25,12.25 C6.8703075,12.25 6.55650958,11.9678347 6.50684668,11.6017677 L6.5,11.5 L6.5,8.25 C6.5,7.83579 6.83579,7.5 7.25,7.5 C7.6296925,7.5 7.94349042,7.78215688 7.99315332,8.14823019 L8,8.25 L8,8.99955 C8.9121,7.78531 10.364,7 11.9999,7 Z</StreamGeometry> <StreamGeometry x:Key="arrow_sync_circle_regular">M12,2 C17.5228,2 22,6.47715 22,12 C22,17.5228 17.5228,22 12,22 C6.47715,22 2,17.5228 2,12 C2,6.47715 6.47715,2 12,2 Z M12,3.5 C7.30558,3.5 3.5,7.30558 3.5,12 C3.5,16.6944 7.30558,20.5 12,20.5 C16.6944,20.5 20.5,16.6944 20.5,12 C20.5,7.30558 16.6944,3.5 12,3.5 Z M16.75,12 C17.1296833,12 17.4434889,12.2821653 17.4931531,12.6482323 L17.5,12.75 L17.5,15.75 C17.5,16.1642 17.1642,16.5 16.75,16.5 C16.3703167,16.5 16.0565111,16.2178347 16.0068469,15.8517677 L16,15.75 L16,15 C15.0881,16.2143 13.6362,17 11.9999,17 C10.4748,17 9.09587,16.316 8.17857,15.237 C7.91028,14.9214 7.94862,14.4481 8.2642,14.1798 C8.57979,13.9115 9.05311,13.9499 9.3214,14.2655 C9.96322,15.0204 10.9293,15.5 11.9999,15.5 C13.32553,15.5 14.4803167,14.7625672 15.0742404,13.6746351 L15.1633,13.5 L14,13.5 C13.5858,13.5 13.25,13.1642 13.25,12.75 C13.25,12.3703167 13.5321653,12.0565111 13.8982323,12.0068469 L14,12 L16.75,12 Z M11.9999,7 C13.5368,7 14.9041,7.66036 15.8268,8.77062 C16.0915,9.08918 16.0479,9.56205 15.7294,9.8268 C15.4108,10.0916 14.9379,10.0479 14.6732,9.72938 C14.0368,8.96361 13.093,8.5 11.9999,8.5 C10.5754318,8.5 9.34895806,9.35140335 8.80281957,10.5730172 L8.72948,10.75 L10,10.75 C10.4142,10.75 10.75,11.0858 10.75,11.5 C10.75,11.8796833 10.4678347,12.1934889 10.1017677,12.2431531 L10,12.25 L7.25,12.25 C6.8703075,12.25 6.55650958,11.9678347 6.50684668,11.6017677 L6.5,11.5 L6.5,8.25 C6.5,7.83579 6.83579,7.5 7.25,7.5 C7.6296925,7.5 7.94349042,7.78215688 7.99315332,8.14823019 L8,8.25 L8,8.99955 C8.9121,7.78531 10.364,7 11.9999,7 Z</StreamGeometry>
<StreamGeometry x:Key="document_one_page_regular">M17.7499 2.00097C18.9408 2.00097 19.9156 2.92613 19.9947 4.09692L19.9999 4.25097V19.749C19.9999 20.9399 19.0748 21.9147 17.904 21.9938L17.7499 21.999H6.25006C5.0592 21.999 4.08442 21.0739 4.00525 19.9031L4.00006 19.749V4.25097C4.00006 3.0601 4.92522 2.08532 6.09601 2.00616L6.25006 2.00097H17.7499ZM17.7499 3.50097H6.25006C5.87037 3.50097 5.55657 3.78312 5.50691 4.1492L5.50006 4.25097V19.749C5.50006 20.1287 5.78222 20.4425 6.14829 20.4922L6.25006 20.499H17.7499C18.1296 20.499 18.4434 20.2169 18.4931 19.8508L18.4999 19.749V4.25097C18.4999 3.87127 18.2178 3.55748 17.8517 3.50782L17.7499 3.50097Z M7 15.75C7 15.3358 7.33578 15 7.75 15H16.25C16.6642 15 17 15.3358 17 15.75C17 16.1642 16.6642 16.5 16.25 16.5H7.75C7.33578 16.5 7 16.1642 7 15.75Z M7 7.75C7 7.33579 7.33578 7 7.75 7H16.25C16.6642 7 17 7.33579 17 7.75C17 8.16422 16.6642 8.5 16.25 8.5H7.75C7.33578 8.5 7 8.16422 7 7.75Z M7 11.75C7 11.3358 7.33578 11 7.75 11H16.25C16.6642 11 17 11.3358 17 11.75C17 12.1642 16.6642 12.5 16.25 12.5H7.75C7.33578 12.5 7 12.1642 7 11.75Z</StreamGeometry>
<StreamGeometry x:Key="arrow_sync_regular">M7.74944331,5.18010908 C8.0006303,5.50946902 7.93725859,5.9800953 7.60789865,6.23128229 C5.81957892,7.59514774 4.75,9.70820889 4.75,12 C4.75,15.7359812 7.57583716,18.8119527 11.2066921,19.2070952 L10.5303301,18.5303301 C10.2374369,18.2374369 10.2374369,17.7625631 10.5303301,17.4696699 C10.7965966,17.2034034 11.2132603,17.1791973 11.5068718,17.3970518 L11.5909903,17.4696699 L13.5909903,19.4696699 C13.8572568,19.7359365 13.8814629,20.1526002 13.6636084,20.4462117 L13.5909903,20.5303301 L11.5909903,22.5303301 C11.298097,22.8232233 10.8232233,22.8232233 10.5303301,22.5303301 C10.2640635,22.2640635 10.2398575,21.8473998 10.4577119,21.5537883 L10.5303301,21.4696699 L11.280567,20.7208479 C6.78460951,20.3549586 3.25,16.5902554 3.25,12 C3.25,9.23526399 4.54178532,6.68321165 6.6982701,5.03856442 C7.02763004,4.78737743 7.49825632,4.85074914 7.74944331,5.18010908 Z M13.4696699,1.46966991 C13.7625631,1.76256313 13.7625631,2.23743687 13.4696699,2.53033009 L12.7204313,3.27923335 C17.2159137,3.64559867 20.75,7.4100843 20.75,12 C20.75,14.6444569 19.5687435,17.0974104 17.5691913,18.7491089 C17.2498402,19.0129038 16.7771069,18.9678666 16.513312,18.6485156 C16.2495171,18.3291645 16.2945543,17.8564312 16.6139054,17.5926363 C18.2720693,16.2229363 19.25,14.1922015 19.25,12 C19.25,8.26436254 16.4246828,5.18861329 12.7943099,4.7930139 L13.4696699,5.46966991 C13.7625631,5.76256313 13.7625631,6.23743687 13.4696699,6.53033009 C13.1767767,6.8232233 12.701903,6.8232233 12.4090097,6.53033009 L10.4090097,4.53033009 C10.1161165,4.23743687 10.1161165,3.76256313 10.4090097,3.46966991 L12.4090097,1.46966991 C12.701903,1.1767767 13.1767767,1.1767767 13.4696699,1.46966991 Z</StreamGeometry> <StreamGeometry x:Key="arrow_sync_regular">M7.74944331,5.18010908 C8.0006303,5.50946902 7.93725859,5.9800953 7.60789865,6.23128229 C5.81957892,7.59514774 4.75,9.70820889 4.75,12 C4.75,15.7359812 7.57583716,18.8119527 11.2066921,19.2070952 L10.5303301,18.5303301 C10.2374369,18.2374369 10.2374369,17.7625631 10.5303301,17.4696699 C10.7965966,17.2034034 11.2132603,17.1791973 11.5068718,17.3970518 L11.5909903,17.4696699 L13.5909903,19.4696699 C13.8572568,19.7359365 13.8814629,20.1526002 13.6636084,20.4462117 L13.5909903,20.5303301 L11.5909903,22.5303301 C11.298097,22.8232233 10.8232233,22.8232233 10.5303301,22.5303301 C10.2640635,22.2640635 10.2398575,21.8473998 10.4577119,21.5537883 L10.5303301,21.4696699 L11.280567,20.7208479 C6.78460951,20.3549586 3.25,16.5902554 3.25,12 C3.25,9.23526399 4.54178532,6.68321165 6.6982701,5.03856442 C7.02763004,4.78737743 7.49825632,4.85074914 7.74944331,5.18010908 Z M13.4696699,1.46966991 C13.7625631,1.76256313 13.7625631,2.23743687 13.4696699,2.53033009 L12.7204313,3.27923335 C17.2159137,3.64559867 20.75,7.4100843 20.75,12 C20.75,14.6444569 19.5687435,17.0974104 17.5691913,18.7491089 C17.2498402,19.0129038 16.7771069,18.9678666 16.513312,18.6485156 C16.2495171,18.3291645 16.2945543,17.8564312 16.6139054,17.5926363 C18.2720693,16.2229363 19.25,14.1922015 19.25,12 C19.25,8.26436254 16.4246828,5.18861329 12.7943099,4.7930139 L13.4696699,5.46966991 C13.7625631,5.76256313 13.7625631,6.23743687 13.4696699,6.53033009 C13.1767767,6.8232233 12.701903,6.8232233 12.4090097,6.53033009 L10.4090097,4.53033009 C10.1161165,4.23743687 10.1161165,3.76256313 10.4090097,3.46966991 L12.4090097,1.46966991 C12.701903,1.1767767 13.1767767,1.1767767 13.4696699,1.46966991 Z</StreamGeometry>
</Style.Resources> </Style.Resources>
<Style Selector="PathIcon.loading"> <Style Selector="PathIcon.loading">

View File

@@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Genos
- Font Author: Copyright 2011 The Genos Project Authors (https://github.com/googlefonts/genos)
- Font Source: http://fonts.gstatic.com/s/genos/v10/SlGNmQqPqpUOYTYjacb0Hc91fTwVA0_orUK6K7ZsAg.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -8,6 +8,8 @@
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Platforms>AnyCPU</Platforms> <Platforms>AnyCPU</Platforms>
<ApplicationIcon>Assets/app.ico</ApplicationIcon>
<Version>0.0.2</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DefineConstants>TRACE</DefineConstants> <DefineConstants>TRACE</DefineConstants>
@@ -28,7 +30,9 @@
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.1" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.1" />
<PackageReference Include="KubernetesClient" Version="11.0.44" /> <PackageReference Include="KubernetesClient" Version="11.0.44" />
<PackageReference Include="Serilog" Version="3.0.1" /> <PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -16,10 +16,10 @@ class Program
{ {
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
//.Filter.ByIncludingOnly(Matching.WithProperty("Area", LogArea.Control)) //.Filter.ByIncludingOnly(Matching.WithProperty("Area", LogArea.Control))
.MinimumLevel.Verbose() .MinimumLevel.Information()
.WriteTo.Console() .WriteTo.Async(a => a.File("app.log"))
.CreateLogger(); .CreateLogger();
BuildAvaloniaApp() BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args); .StartWithClassicDesktopLifetime(args);
} }
@@ -30,7 +30,7 @@ class Program
} }
finally finally
{ {
// This block is optional. // This block is optional.
// Use the finally-block if you need to clean things up or similar // Use the finally-block if you need to clean things up or similar
Log.CloseAndFlush(); Log.CloseAndFlush();
} }

View File

@@ -0,0 +1,23 @@
// // Copyright (c) Vector Informatik GmbH. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using K8sFileBrowser.Models;
namespace K8sFileBrowser.Services;
public interface IKubernetesService
{
IEnumerable<ClusterContext> GetClusterContexts();
string GetCurrentContext();
void SwitchClusterContext(ClusterContext clusterContext);
IEnumerable<Namespace> GetNamespaces();
IEnumerable<Pod> GetPods(string namespaceName);
IList<FileInformation> GetFiles(string namespaceName, string podName, string containerName, string path);
Task DownloadFile(Namespace? selectedNamespace, Pod? selectedPod, FileInformation selectedFile,
string? saveFileName, CancellationToken cancellationToken = default);
Task DownloadLog(Namespace? selectedNamespace, Pod? selectedPod,
string? saveFileName, CancellationToken cancellationToken = default);
}

View File

@@ -14,7 +14,7 @@ using static ICSharpCode.SharpZipLib.Core.StreamUtils;
namespace K8sFileBrowser.Services; namespace K8sFileBrowser.Services;
public class KubernetesService public class KubernetesService : IKubernetesService
{ {
private readonly K8SConfiguration _k8SConfiguration; private readonly K8SConfiguration _k8SConfiguration;
private IKubernetes _kubernetesClient = null!; private IKubernetes _kubernetesClient = null!;
@@ -73,7 +73,7 @@ public class KubernetesService
podName, namespaceName, containerName, podName, namespaceName, containerName,
new[] { "find", path, "-maxdepth", "1", "-exec", "stat", "-c", "%F|%n|%s|%Y", "{}", ";" }, new[] { "find", path, "-maxdepth", "1", "-exec", "stat", "-c", "%F|%n|%s|%Y", "{}", ";" },
true, true,
execResult.ParseFileInformationCallback, CancellationToken.None) (@in, @out, err) => execResult.ParseFileInformationCallback(@in, @out, err), CancellationToken.None)
.ConfigureAwait(false) .ConfigureAwait(false)
.GetAwaiter() .GetAwaiter()
.GetResult(); .GetResult();
@@ -98,11 +98,10 @@ public class KubernetesService
_kubernetesClient = kubernetesClient; _kubernetesClient = kubernetesClient;
} }
public async Task DownloadFile(Namespace? selectedNamespace, Pod? selectedPod, FileInformation selectedFile, public async Task DownloadFile(Namespace? selectedNamespace, Pod? selectedPod, FileInformation selectedFile,
string? saveFileName) string? saveFileName, CancellationToken cancellationToken = default)
{ {
Log.Information("{SelectedNamespace} - {SelectedPod} - {SelectedFile} - {SaveFileName}", Log.Information("{SelectedNamespace} - {SelectedPod} - {@SelectedFile} - {SaveFileName}",
selectedNamespace, selectedPod, selectedFile, saveFileName); selectedNamespace, selectedPod, selectedFile, saveFileName);
var handler = new ExecAsyncCallback(async (_, stdOut, stdError) => var handler = new ExecAsyncCallback(async (_, stdOut, stdError) =>
{ {
@@ -111,37 +110,57 @@ public class KubernetesService
await using var outputFileStream = File.OpenWrite(saveFileName!); await using var outputFileStream = File.OpenWrite(saveFileName!);
await using var tarInputStream = new TarInputStream(stdOut, Encoding.Default); await using var tarInputStream = new TarInputStream(stdOut, Encoding.Default);
var entry = await tarInputStream.GetNextEntryAsync(default); var entry = await tarInputStream.GetNextEntryAsync(cancellationToken);
if (entry == null) if (entry == null)
{ {
Log.Error("Copy command failed: no files found");
throw new IOException("Copy command failed: no files found"); throw new IOException("Copy command failed: no files found");
} }
var bytes = new byte[entry.Size]; var bytes = new byte[entry.Size];
ReadFully( tarInputStream, bytes ); ReadFully( tarInputStream, bytes );
await outputFileStream.WriteAsync(bytes, default); await outputFileStream.WriteAsync(bytes, cancellationToken);
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Copy command failed");
throw new IOException($"Copy command failed: {ex.Message}"); throw new IOException($"Copy command failed: {ex.Message}");
} }
using var streamReader = new StreamReader(stdError); using var streamReader = new StreamReader(stdError);
while (streamReader.EndOfStream == false) while (streamReader.EndOfStream == false)
{ {
var error = await streamReader.ReadToEndAsync(default); var error = await streamReader.ReadToEndAsync(cancellationToken);
Log.Error(error); Log.Error("Remote error: {Error}",error);
} }
}); });
// the kubectl uses also tar for copying files // the kubectl uses also tar for copying files
await _kubernetesClient.NamespacedPodExecAsync( await _kubernetesClient.NamespacedPodExecAsync(
selectedPod.Name, selectedPod?.Name,
selectedNamespace.Name, selectedNamespace?.Name,
selectedPod.Containers.First(), selectedPod?.Containers.First(),
new[] { "sh", "-c", $"tar cf - {selectedFile.Name}" }, new[] { "sh", "-c", $"tar cf - {selectedFile.Name}" },
false, false,
handler, handler,
default); cancellationToken);
}
public async Task DownloadLog(Namespace? selectedNamespace, Pod? selectedPod,
string? saveFileName, CancellationToken cancellationToken = default)
{
Log.Information("{SelectedNamespace} - {SelectedPod} - {SaveFileName}",
selectedNamespace, selectedPod, saveFileName);
// the kubectl uses also tar for copying files
var response = await _kubernetesClient.CoreV1.ReadNamespacedPodLogWithHttpMessagesAsync(
selectedPod?.Name,
selectedNamespace?.Name,
container: selectedPod?.Containers.First(),
follow: false , cancellationToken: cancellationToken)
.ConfigureAwait(false);
await using var outputFileStream = File.OpenWrite(saveFileName!);
var stream = response.Body;
await stream.CopyToAsync(outputFileStream, cancellationToken);
} }
} }

View File

@@ -11,154 +11,181 @@ namespace K8sFileBrowser.ViewModels;
public class MainWindowViewModel : ViewModelBase public class MainWindowViewModel : ViewModelBase
{ {
private readonly ObservableAsPropertyHelper<IEnumerable<ClusterContext>> _clusterContexts; private readonly ObservableAsPropertyHelper<IEnumerable<ClusterContext>> _clusterContexts;
public IEnumerable<ClusterContext> ClusterContexts => _clusterContexts.Value; public IEnumerable<ClusterContext> ClusterContexts => _clusterContexts.Value;
private ClusterContext? _selectedClusterContext; private ClusterContext? _selectedClusterContext;
public ClusterContext? SelectedClusterContext
public ClusterContext? SelectedClusterContext
{
get => _selectedClusterContext;
set => this.RaiseAndSetIfChanged(ref _selectedClusterContext, value);
}
private readonly ObservableAsPropertyHelper<IEnumerable<Namespace>> _namespaces;
public IEnumerable<Namespace> Namespaces => _namespaces.Value;
private Namespace? _selectedNamespace;
public Namespace? SelectedNamespace
{
get => _selectedNamespace;
set => this.RaiseAndSetIfChanged(ref _selectedNamespace, value);
}
private readonly ObservableAsPropertyHelper<IEnumerable<Pod>> _pods;
public IEnumerable<Pod> Pods => _pods.Value;
private Pod? _selectedPod;
public Pod? SelectedPod
{
get => _selectedPod;
set => this.RaiseAndSetIfChanged(ref _selectedPod, value);
}
private readonly ObservableAsPropertyHelper<IEnumerable<FileInformation>> _fileInformation;
public IEnumerable<FileInformation> FileInformation => _fileInformation.Value;
private FileInformation? _selectedFile;
public FileInformation? SelectedFile
{
get => _selectedFile;
set => this.RaiseAndSetIfChanged(ref _selectedFile, value);
}
private string? _selectedPath;
public string? SelectedPath
{
get => _selectedPath;
set => this.RaiseAndSetIfChanged(ref _selectedPath, value);
}
private bool _isDownloadActive;
public bool IsDownloadActive
{
get => _isDownloadActive;
set => this.RaiseAndSetIfChanged(ref _isDownloadActive, value);
}
public ReactiveCommand<Unit, Unit> DownloadCommand { get; }
public ReactiveCommand<Unit, Unit> DownloadLogCommand { get; }
public ReactiveCommand<Unit, Unit> ParentCommand { get; }
public ReactiveCommand<Unit, Unit> OpenCommand { get; }
public MainWindowViewModel()
{
//TODO: use dependency injection to get the kubernetes service
IKubernetesService kubernetesService = new KubernetesService();
var isSelectedPod = this
.WhenAnyValue(x => x.SelectedPod)
.Select(x => x != null);
var isFile = this
.WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive)
.Select(x => x is { Item1.Type: FileType.File, Item2: false });
var isDirectory = this
.WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive)
.Select(x => x is { Item1.Type: FileType.Directory, Item2: false });
var isNotRoot = this
.WhenAnyValue(x => x.SelectedPath, x => x.IsDownloadActive)
.Select(x => x.Item1 is not "/" && !x.Item2);
OpenCommand = ReactiveCommand.Create(() => { SelectedPath = SelectedFile != null ? SelectedFile!.Name : "/"; },
isDirectory, RxApp.MainThreadScheduler);
DownloadCommand = ReactiveCommand.CreateFromTask(async () =>
{ {
get => _selectedClusterContext; await Observable.StartAsync(async () =>
set => this.RaiseAndSetIfChanged(ref _selectedClusterContext, value); {
} var fileName = SelectedFile!.Name.Substring(SelectedFile!.Name.LastIndexOf('/') + 1,
SelectedFile!.Name.Length - SelectedFile!.Name.LastIndexOf('/') - 1);
private readonly ObservableAsPropertyHelper<IEnumerable<Namespace>> _namespaces; var saveFileName = await ApplicationHelper.SaveFile(".", fileName);
public IEnumerable<Namespace> Namespaces => _namespaces.Value; if (saveFileName != null)
private Namespace? _selectedNamespace;
public Namespace? SelectedNamespace
{
get => _selectedNamespace;
set => this.RaiseAndSetIfChanged(ref _selectedNamespace, value);
}
private readonly ObservableAsPropertyHelper<IEnumerable<Pod>> _pods;
public IEnumerable<Pod> Pods => _pods.Value;
private Pod? _selectedPod;
public Pod? SelectedPod
{
get => _selectedPod;
set => this.RaiseAndSetIfChanged(ref _selectedPod, value);
}
private readonly ObservableAsPropertyHelper<IEnumerable<FileInformation>> _fileInformation;
public IEnumerable<FileInformation> FileInformation => _fileInformation.Value;
private FileInformation? _selectedFile;
public FileInformation? SelectedFile
{
get => _selectedFile;
set => this.RaiseAndSetIfChanged(ref _selectedFile, value);
}
private string? _selectedPath;
public string? SelectedPath
{
get => _selectedPath;
set => this.RaiseAndSetIfChanged(ref _selectedPath, value);
}
private bool _isDownloadActive;
public bool IsDownloadActive
{
get => _isDownloadActive;
set => this.RaiseAndSetIfChanged(ref _isDownloadActive, value);
}
public ReactiveCommand<Unit, Unit> DownloadCommand { get; }
public ReactiveCommand<Unit, Unit> ParentCommand { get; }
public ReactiveCommand<Unit, Unit> OpenCommand { get; }
public MainWindowViewModel()
{
var kubernetesService = new KubernetesService();
var isFile = this
.WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive)
.Select(x => x is { Item1.Type: FileType.File, Item2: false });
var isDirectory = this
.WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive)
.Select(x => x is { Item1.Type: FileType.Directory, Item2: false });
var isNotRoot = this
.WhenAnyValue(x => x.SelectedPath, x => x.IsDownloadActive)
.Select(x => x.Item1 is not "/" && !x.Item2);
OpenCommand = ReactiveCommand.Create(() =>
{ {
SelectedPath = SelectedFile != null ? SelectedFile!.Name : "/"; IsDownloadActive = true;
}, isDirectory, RxApp.MainThreadScheduler); await kubernetesService.DownloadFile(SelectedNamespace, SelectedPod, SelectedFile, saveFileName);
IsDownloadActive = false;
DownloadCommand = ReactiveCommand.CreateFromTask(async () => }
}, RxApp.TaskpoolScheduler);
}, isFile, RxApp.MainThreadScheduler);
DownloadLogCommand = ReactiveCommand.CreateFromTask(async () =>
{
await Observable.StartAsync(async () =>
{
var fileName = SelectedPod?.Name + ".log";
var saveFileName = await ApplicationHelper.SaveFile(".", fileName);
if (saveFileName != null)
{ {
await Observable.StartAsync(async () => { IsDownloadActive = true;
var fileName = SelectedFile!.Name.Substring(SelectedFile!.Name.LastIndexOf('/') + 1, SelectedFile!.Name.Length - SelectedFile!.Name.LastIndexOf('/') - 1); await kubernetesService.DownloadLog(SelectedNamespace, SelectedPod, saveFileName);
var saveFileName = await ApplicationHelper.SaveFile(".", fileName); IsDownloadActive = false;
if (saveFileName != null) }
{ }, RxApp.TaskpoolScheduler);
IsDownloadActive = true; }, isSelectedPod, RxApp.MainThreadScheduler);
await kubernetesService.DownloadFile(SelectedNamespace, SelectedPod, SelectedFile, saveFileName);
IsDownloadActive = false;
}
}, RxApp.TaskpoolScheduler);
}, isFile, RxApp.MainThreadScheduler);
ParentCommand = ReactiveCommand.Create(() => ParentCommand = ReactiveCommand.Create(() =>
{ {
SelectedPath = SelectedPath![..SelectedPath!.LastIndexOf('/')]; SelectedPath = SelectedPath![..SelectedPath!.LastIndexOf('/')];
if (SelectedPath!.Length == 0) if (SelectedPath!.Length == 0)
{ {
SelectedPath = "/"; SelectedPath = "/";
} }
}, isNotRoot, RxApp.MainThreadScheduler); }, isNotRoot, RxApp.MainThreadScheduler);
// read the cluster contexts // read the cluster contexts
_namespaces = this _namespaces = this
.WhenAnyValue(c => c.SelectedClusterContext) .WhenAnyValue(c => c.SelectedClusterContext)
.Throttle(TimeSpan.FromMilliseconds(10)) .Throttle(TimeSpan.FromMilliseconds(10))
.Where(context => context != null) .Where(context => context != null)
.Select(context => .Select(context =>
{ {
kubernetesService.SwitchClusterContext(context!); kubernetesService.SwitchClusterContext(context!);
return kubernetesService.GetNamespaces(); return kubernetesService.GetNamespaces();
}) })
.ObserveOn(RxApp.MainThreadScheduler) .ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, x => x.Namespaces); .ToProperty(this, x => x.Namespaces);
// read the pods when the namespace changes // read the pods when the namespace changes
_pods = this _pods = this
.WhenAnyValue(c => c.SelectedNamespace) .WhenAnyValue(c => c.SelectedNamespace)
.Throttle(TimeSpan.FromMilliseconds(10)) .Throttle(TimeSpan.FromMilliseconds(10))
.Where(ns => ns != null) .Where(ns => ns != null)
.Select(ns => kubernetesService.GetPods(ns!.Name)) .Select(ns => kubernetesService.GetPods(ns!.Name))
.ObserveOn(RxApp.MainThreadScheduler) .ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, x => x.Pods); .ToProperty(this, x => x.Pods);
// read the file information when the path changes // read the file information when the path changes
_fileInformation = this _fileInformation = this
.WhenAnyValue(c => c.SelectedPath, c => c.SelectedPod, c => c.SelectedNamespace) .WhenAnyValue(c => c.SelectedPath, c => c.SelectedPod, c => c.SelectedNamespace)
.Throttle(TimeSpan.FromMilliseconds(10)) .Throttle(TimeSpan.FromMilliseconds(10))
.Select(x => x.Item3 == null || x.Item2 == null .Select(x => x.Item3 == null || x.Item2 == null
? new List<FileInformation>() ? new List<FileInformation>()
: kubernetesService.GetFiles(x.Item3!.Name, x.Item2!.Name, x.Item2!.Containers.First(), : kubernetesService.GetFiles(x.Item3!.Name, x.Item2!.Name, x.Item2!.Containers.First(),
x.Item1)) x.Item1))
.ObserveOn(RxApp.MainThreadScheduler) .ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, x => x.FileInformation); .ToProperty(this, x => x.FileInformation);
// reset the path when the pod or namespace changes // reset the path when the pod or namespace changes
this.WhenAnyValue(c => c.SelectedPod, c => c.SelectedNamespace) this.WhenAnyValue(c => c.SelectedPod, c => c.SelectedNamespace)
.Subscribe(x => SelectedPath = "/"); .Subscribe(x => SelectedPath = "/");
// load the cluster contexts when the view model is created // load the cluster contexts when the view model is created
var loadContexts = ReactiveCommand var loadContexts = ReactiveCommand
.Create<Unit, IEnumerable<ClusterContext>>(_ => kubernetesService.GetClusterContexts()); .Create<Unit, IEnumerable<ClusterContext>>(_ => kubernetesService.GetClusterContexts());
_clusterContexts = loadContexts.Execute().ToProperty( _clusterContexts = loadContexts.Execute().ToProperty(
this, x => x.ClusterContexts, scheduler: RxApp.MainThreadScheduler); this, x => x.ClusterContexts, scheduler: RxApp.MainThreadScheduler);
// select the current cluster context // select the current cluster context
SelectedClusterContext = ClusterContexts SelectedClusterContext = ClusterContexts
.FirstOrDefault(x => x.Name == kubernetesService.GetCurrentContext()); .FirstOrDefault(x => x.Name == kubernetesService.GetCurrentContext());
} }
} }

View File

@@ -7,7 +7,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="K8sFileBrowser.Views.MainWindow" x:Class="K8sFileBrowser.Views.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico" Icon="/Assets/app.ico"
Title="K8sFileBrowser"> Title="K8sFileBrowser">
<Design.DataContext> <Design.DataContext>
@@ -77,7 +77,7 @@
<TextBlock Text="{Binding SelectedPath}" VerticalAlignment="Center" Margin="10 0 0 0"></TextBlock> <TextBlock Text="{Binding SelectedPath}" VerticalAlignment="Center" Margin="10 0 0 0"></TextBlock>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" Margin="10" HorizontalAlignment="Right"> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" Margin="10" HorizontalAlignment="Right">
<Button Command="{Binding ParentCommand}" VerticalAlignment="Center" ToolTip.Tip="Go To Parent Directory"> <Button Command="{Binding ParentCommand}" VerticalAlignment="Center" ToolTip.Tip="Go To Parent Directory">
<PathIcon Data="{StaticResource arrow_curve_up_left_regular}"></PathIcon> <PathIcon Data="{StaticResource arrow_curve_up_left_regular}"></PathIcon>
</Button> </Button>
@@ -87,6 +87,9 @@
<Button Command="{Binding DownloadCommand}" VerticalAlignment="Center" ToolTip.Tip="Download File"> <Button Command="{Binding DownloadCommand}" VerticalAlignment="Center" ToolTip.Tip="Download File">
<PathIcon Data="{StaticResource arrow_download_regular}"></PathIcon> <PathIcon Data="{StaticResource arrow_download_regular}"></PathIcon>
</Button> </Button>
<Button Command="{Binding DownloadLogCommand}" VerticalAlignment="Center" ToolTip.Tip="Download Pod Log">
<PathIcon Data="{StaticResource document_one_page_regular}"></PathIcon>
</Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
<DataGrid Grid.Row="1" <DataGrid Grid.Row="1"