Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using DocMigrator.Yaml;
using Microsoft.Extensions.Logging;
using RackPeek.Domain.Resources;
using RackPeek.Domain.Resources.AccessPoints;
using RackPeek.Domain.Resources.Desktops;
using RackPeek.Domain.Resources.Firewalls;
using RackPeek.Domain.Resources.Laptops;
using RackPeek.Domain.Resources.Routers;
using RackPeek.Domain.Resources.Servers;
using RackPeek.Domain.Resources.Services;
using RackPeek.Domain.Resources.Switches;
using RackPeek.Domain.Resources.SystemResources;
using RackPeek.Domain.Resources.UpsUnits;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace RackPeek.Domain.Persistence.Yaml;

public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<YamlRoot>
{
public RackPeekConfigMigrationDeserializer(IServiceProvider serviceProvider,
ILogger<YamlMigrationDeserializer<YamlRoot>> logger) :
base(serviceProvider, logger,
new List<Func<IServiceProvider, Dictionary<object,object>, ValueTask>>{
EnsureSchemaVersionExists,
},
"version",
new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithCaseInsensitivePropertyMatching()
.WithTypeConverter(new StorageSizeYamlConverter())
.WithTypeConverter(new NotesStringYamlConverter())
.WithTypeDiscriminatingNodeDeserializer(options =>
{
options.AddKeyValueTypeDiscriminator<Resource>("kind", new Dictionary<string, Type>
{
{ Server.KindLabel, typeof(Server) },
{ Switch.KindLabel, typeof(Switch) },
{ Firewall.KindLabel, typeof(Firewall) },
{ Router.KindLabel, typeof(Router) },
{ Desktop.KindLabel, typeof(Desktop) },
{ Laptop.KindLabel, typeof(Laptop) },
{ AccessPoint.KindLabel, typeof(AccessPoint) },
{ Ups.KindLabel, typeof(Ups) },
{ SystemResource.KindLabel, typeof(SystemResource) },
{ Service.KindLabel, typeof(Service) }
});
}),
new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeConverter(new StorageSizeYamlConverter())
.WithTypeConverter(new NotesStringYamlConverter())
.ConfigureDefaultValuesHandling(
DefaultValuesHandling.OmitNull |
DefaultValuesHandling.OmitEmptyCollections
)) {}

#region Migrations

// Define migration functions here
public static ValueTask EnsureSchemaVersionExists(IServiceProvider serviceProvider, Dictionary<object, object> obj)
{
if (!obj.ContainsKey("version"))
{
obj["version"] = 0;
}

return ValueTask.CompletedTask;
}

#endregion
}
123 changes: 23 additions & 100 deletions RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
public sealed class YamlResourceCollection(
string filePath,
ITextFileStore fileStore,
ResourceCollection resourceCollection)
ResourceCollection resourceCollection,
RackPeekConfigMigrationDeserializer _deserializer)
: IResourceCollection
{
// Bump this when your YAML schema changes, and add a migration step below.
Expand Down Expand Up @@ -108,34 +109,35 @@
return;
}

var root = DeserializeRoot(yaml);
if (root == null)
{
// Keep behavior aligned with your previous code: if YAML is invalid, treat as empty.
resourceCollection.Resources.Clear();
return;
}

var version = _deserializer.GetSchemaVersion(yaml);

// Guard: config is newer than this app understands.
if (root.Version > CurrentSchemaVersion)
if (version > CurrentSchemaVersion)
{
throw new InvalidOperationException(
$"Config schema version {root.Version} is newer than this application supports ({CurrentSchemaVersion}).");
$"Config schema version {version} is newer than this application supports ({CurrentSchemaVersion}).");
}

YamlRoot? root;
// If older, backup first, then migrate step-by-step, then save.
if (root.Version < CurrentSchemaVersion)
if (version < CurrentSchemaVersion)
{
await BackupOriginalAsync(yaml);

root = await MigrateAsync(root);

root = await _deserializer.Deserialize(yaml);

// Ensure we persist the migrated root (with updated version)
await SaveRootAsync(root);
}

else
{
root = await _deserializer.Deserialize(yaml);
}

resourceCollection.Resources.Clear();
resourceCollection.Resources.AddRange(root.Resources ?? []);
if (root?.Resources != null)
{
resourceCollection.Resources.AddRange(root.Resources);
}
}

public Task AddAsync(Resource resource)
Expand Down Expand Up @@ -200,88 +202,8 @@
var backupPath = $"{filePath}.bak.{DateTime.UtcNow:yyyyMMddHHmmss}";
await fileStore.WriteAllTextAsync(backupPath, originalYaml);
}

private Task<YamlRoot> MigrateAsync(YamlRoot root)
{
// Step-by-step migrations until we reach CurrentSchemaVersion
while (root.Version < CurrentSchemaVersion)
{
root = root.Version switch
{
0 => MigrateV0ToV1(root),
_ => throw new InvalidOperationException(
$"No migration is defined from version {root.Version} to {root.Version + 1}.")
};
}

return Task.FromResult(root);
}

private YamlRoot MigrateV0ToV1(YamlRoot root)
{
// V0 -> V1 example migration:
// - Ensure 'kind' is normalized on all resources
// - Ensure tags collections aren’t null
if (root.Resources != null)
{
foreach (var r in root.Resources)
{
r.Kind = GetKind(r);
r.Tags ??= [];
}
}

root.Version = 1;
return root;
}

// ----------------------------
// YAML read/write
// ----------------------------

private YamlRoot? DeserializeRoot(string yaml)
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithCaseInsensitivePropertyMatching()
.WithTypeConverter(new StorageSizeYamlConverter())
.WithTypeConverter(new NotesStringYamlConverter())
.WithTypeDiscriminatingNodeDeserializer(options =>
{
options.AddKeyValueTypeDiscriminator<Resource>("kind", new Dictionary<string, Type>
{
{ Server.KindLabel, typeof(Server) },
{ Switch.KindLabel, typeof(Switch) },
{ Firewall.KindLabel, typeof(Firewall) },
{ Router.KindLabel, typeof(Router) },
{ Desktop.KindLabel, typeof(Desktop) },
{ Laptop.KindLabel, typeof(Laptop) },
{ AccessPoint.KindLabel, typeof(AccessPoint) },
{ Ups.KindLabel, typeof(Ups) },
{ SystemResource.KindLabel, typeof(SystemResource) },
{ Service.KindLabel, typeof(Service) }
});
})
.Build();

try
{
// If 'version' is missing, int defaults to 0 => treated as V0.
var root = deserializer.Deserialize<YamlRoot>(yaml);

// If YAML had only "resources:" previously, this will still work.
root ??= new YamlRoot { Version = 0, Resources = new List<Resource>() };
root.Resources ??= new List<Resource>();

return root;
}
catch (YamlException)
{
return null;
}
}

private async Task SaveRootAsync(YamlRoot root)

private async Task SaveRootAsync(YamlRoot? root)
{
var serializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
Expand All @@ -296,7 +218,7 @@
// Preserve ordering: version first, then resources
var payload = new OrderedDictionary
{
["version"] = root.Version,

Check warning on line 221 in RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
["resources"] = (root.Resources ?? new List<Resource>()).Select(SerializeResource).ToList()
};

Expand Down Expand Up @@ -349,10 +271,11 @@

return map;
}

}

public class YamlRoot
{
public int Version { get; set; } // <- NEW: YAML schema version
public int Version { get; set; }
public List<Resource>? Resources { get; set; }
}
6 changes: 4 additions & 2 deletions RackPeek.Domain/RackPeek.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2"/>
<PackageReference Include="YamlDotNet" Version="16.3.0"/>
<PackageReference Include="DocMigrator.Yaml" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>

</Project>
10 changes: 7 additions & 3 deletions RackPeek.Web.Viewer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using RackPeek.Domain.Persistence;
using RackPeek.Domain.Persistence.Yaml;
using Shared.Rcl;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace RackPeek.Web.Viewer;

Expand Down Expand Up @@ -33,15 +35,17 @@ public static async Task Main(string[] args)

var resources = new ResourceCollection();
builder.Services.AddSingleton(resources);

var yamlDir = builder.Configuration.GetValue<string>("RPK_YAML_DIR") ?? "config";
var yamlFilePath = $"{yamlDir}/config.yaml";
builder.Services.AddScoped<RackPeekConfigMigrationDeserializer>();

builder.Services.AddScoped<IResourceCollection>(sp =>
new YamlResourceCollection(
yamlFilePath,
sp.GetRequiredService<ITextFileStore>(),
sp.GetRequiredService<ResourceCollection>()));
sp.GetRequiredService<ResourceCollection>(),
sp.GetRequiredService<RackPeekConfigMigrationDeserializer>()));

builder.Services.AddYamlRepos();
builder.Services.AddCommands();
Expand All @@ -53,4 +57,4 @@ public static async Task Main(string[] args)

await builder.Build().RunAsync();
}
}
}
16 changes: 8 additions & 8 deletions RackPeek.Web.Viewer/RackPeek.Web.Viewer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" PrivateAssets="all"/>
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1"/>
<PackageReference Include="Spectre.Console.Testing" Version="0.54.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.3" PrivateAssets="all" />
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
<PackageReference Include="Spectre.Console.Testing" Version="0.54.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj"/>
<ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj" />
</ItemGroup>

<ItemGroup>
<_ContentIncludedByDefault Remove="Layout\MainLayout.razor"/>
<_ContentIncludedByDefault Remove="Layout\NavMenu.razor"/>
<_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json"/>
<_ContentIncludedByDefault Remove="Layout\MainLayout.razor" />
<_ContentIncludedByDefault Remove="Layout\NavMenu.razor" />
<_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" />
</ItemGroup>

</Project>
11 changes: 7 additions & 4 deletions RackPeek.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using RackPeek.Domain.Persistence;
using RackPeek.Domain.Persistence.Yaml;
using RackPeek.Web.Components;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using Shared.Rcl;

namespace RackPeek.Web;
Expand Down Expand Up @@ -46,8 +48,7 @@ public static async Task<WebApplication> BuildApp(WebApplicationBuilder builder)
}

builder.Services.AddScoped<ITextFileStore, PhysicalTextFileStore>();



builder.Services.AddScoped(sp =>
{
var nav = sp.GetRequiredService<NavigationManager>();
Expand All @@ -60,12 +61,14 @@ public static async Task<WebApplication> BuildApp(WebApplicationBuilder builder)

var resources = new ResourceCollection();
builder.Services.AddSingleton(resources);
builder.Services.AddScoped<RackPeekConfigMigrationDeserializer>();

builder.Services.AddScoped<IResourceCollection>(sp =>
new YamlResourceCollection(
yamlFilePath,
sp.GetRequiredService<ITextFileStore>(),
sp.GetRequiredService<ResourceCollection>()));
sp.GetRequiredService<ResourceCollection>(),
sp.GetRequiredService<RackPeekConfigMigrationDeserializer>()));

// Infrastructure
builder.Services.AddYamlRepos();
Expand Down Expand Up @@ -108,4 +111,4 @@ public static async Task Main(string[] args)
var app = await BuildApp(builder);
await app.RunAsync();
}
}
}
24 changes: 12 additions & 12 deletions RackPeek/RackPeek.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2"/>
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.2"/>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.2"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2"/>
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2"/>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.2"/>
<PackageReference Include="Spectre.Console" Version="0.54.0"/>
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1"/>
<PackageReference Include="Spectre.Console.Testing" Version="0.54.0"/>
<PackageReference Include="YamlDotNet" Version="16.3.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
<PackageReference Include="Spectre.Console" Version="0.54.0" />
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
<PackageReference Include="Spectre.Console.Testing" Version="0.54.0" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj"/>
<ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj" />
</ItemGroup>

</Project>
Loading
Loading