added app icon, introduce kubernetesservice interface and logging

This commit is contained in:
2023-08-01 20:39:25 +02:00
parent ee6544c641
commit 12ef9680d0
9 changed files with 189 additions and 154 deletions

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,7 @@
<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>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DefineConstants>TRACE</DefineConstants> <DefineConstants>TRACE</DefineConstants>
@@ -28,7 +29,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,8 +16,8 @@ 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()

View File

@@ -0,0 +1,20 @@
// // 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);
}

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!;
@@ -98,9 +98,8 @@ 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);
@@ -111,7 +110,7 @@ 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)
{ {
throw new IOException("Copy command failed: no files found"); throw new IOException("Copy command failed: no files found");
@@ -119,7 +118,7 @@ public class KubernetesService
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)
{ {
@@ -129,7 +128,7 @@ public class KubernetesService
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(error);
} }
}); });

View File

@@ -11,154 +11,161 @@ 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> ParentCommand { get; }
public ReactiveCommand<Unit, Unit> OpenCommand { get; }
public MainWindowViewModel()
{
//TODO: use dependency injection to get the kubernetes service
IKubernetesService 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 : "/"; },
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;
}
}, RxApp.TaskpoolScheduler);
}, isFile, RxApp.MainThreadScheduler);
DownloadCommand = ReactiveCommand.CreateFromTask(async () => ParentCommand = ReactiveCommand.Create(() =>
{ {
await Observable.StartAsync(async () => { SelectedPath = SelectedPath![..SelectedPath!.LastIndexOf('/')];
var fileName = SelectedFile!.Name.Substring(SelectedFile!.Name.LastIndexOf('/') + 1, SelectedFile!.Name.Length - SelectedFile!.Name.LastIndexOf('/') - 1); if (SelectedPath!.Length == 0)
var saveFileName = await ApplicationHelper.SaveFile(".", fileName); {
if (saveFileName != null) SelectedPath = "/";
{ }
IsDownloadActive = true; }, isNotRoot, RxApp.MainThreadScheduler);
await kubernetesService.DownloadFile(SelectedNamespace, SelectedPod, SelectedFile, saveFileName);
IsDownloadActive = false;
}
}, RxApp.TaskpoolScheduler);
}, isFile, RxApp.MainThreadScheduler);
ParentCommand = ReactiveCommand.Create(() => // read the cluster contexts
{ _namespaces = this
SelectedPath = SelectedPath![..SelectedPath!.LastIndexOf('/')]; .WhenAnyValue(c => c.SelectedClusterContext)
if (SelectedPath!.Length == 0) .Throttle(TimeSpan.FromMilliseconds(10))
{ .Where(context => context != null)
SelectedPath = "/"; .Select(context =>
} {
}, isNotRoot, RxApp.MainThreadScheduler); kubernetesService.SwitchClusterContext(context!);
return kubernetesService.GetNamespaces();
})
.ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, x => x.Namespaces);
// read the cluster contexts // read the pods when the namespace changes
_namespaces = this _pods = this
.WhenAnyValue(c => c.SelectedClusterContext) .WhenAnyValue(c => c.SelectedNamespace)
.Throttle(TimeSpan.FromMilliseconds(10)) .Throttle(TimeSpan.FromMilliseconds(10))
.Where(context => context != null) .Where(ns => ns != null)
.Select(context => .Select(ns => kubernetesService.GetPods(ns!.Name))
{ .ObserveOn(RxApp.MainThreadScheduler)
kubernetesService.SwitchClusterContext(context!); .ToProperty(this, x => x.Pods);
return kubernetesService.GetNamespaces();
})
.ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, x => x.Namespaces);
// read the pods when the namespace changes // read the file information when the path changes
_pods = this _fileInformation = this
.WhenAnyValue(c => c.SelectedNamespace) .WhenAnyValue(c => c.SelectedPath, c => c.SelectedPod, c => c.SelectedNamespace)
.Throttle(TimeSpan.FromMilliseconds(10)) .Throttle(TimeSpan.FromMilliseconds(10))
.Where(ns => ns != null) .Select(x => x.Item3 == null || x.Item2 == null
.Select(ns => kubernetesService.GetPods(ns!.Name)) ? new List<FileInformation>()
.ObserveOn(RxApp.MainThreadScheduler) : kubernetesService.GetFiles(x.Item3!.Name, x.Item2!.Name, x.Item2!.Containers.First(),
.ToProperty(this, x => x.Pods); x.Item1))
.ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, x => x.FileInformation);
// read the file information when the path changes // reset the path when the pod or namespace changes
_fileInformation = this this.WhenAnyValue(c => c.SelectedPod, c => c.SelectedNamespace)
.WhenAnyValue(c => c.SelectedPath, c => c.SelectedPod, c => c.SelectedNamespace) .Subscribe(x => SelectedPath = "/");
.Throttle(TimeSpan.FromMilliseconds(10))
.Select(x => x.Item3 == null || x.Item2 == null
? new List<FileInformation>()
: kubernetesService.GetFiles(x.Item3!.Name, x.Item2!.Name, x.Item2!.Containers.First(),
x.Item1))
.ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, x => x.FileInformation);
// reset the path when the pod or namespace changes // load the cluster contexts when the view model is created
this.WhenAnyValue(c => c.SelectedPod, c => c.SelectedNamespace) var loadContexts = ReactiveCommand
.Subscribe(x => SelectedPath = "/"); .Create<Unit, IEnumerable<ClusterContext>>(_ => kubernetesService.GetClusterContexts());
_clusterContexts = loadContexts.Execute().ToProperty(
this, x => x.ClusterContexts, scheduler: RxApp.MainThreadScheduler);
// load the cluster contexts when the view model is created // select the current cluster context
var loadContexts = ReactiveCommand SelectedClusterContext = ClusterContexts
.Create<Unit, IEnumerable<ClusterContext>>(_ => kubernetesService.GetClusterContexts()); .FirstOrDefault(x => x.Name == kubernetesService.GetCurrentContext());
_clusterContexts = loadContexts.Execute().ToProperty( }
this, x => x.ClusterContexts, scheduler: RxApp.MainThreadScheduler);
// select the current cluster context
SelectedClusterContext = ClusterContexts
.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>