Skip to content

Commit 1d2d1b8

Browse files
committed
Support for conversions between models of different versions
1 parent b0e7d99 commit 1d2d1b8

12 files changed

+2768
-1
lines changed

gen/KubernetesWatchGenerator/KubernetesWatchGenerator.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
<None Update="IKubernetes.Watch.cs.template">
2323
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2424
</None>
25+
<None Update="ModelOperators.cs.template">
26+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
27+
</None>
28+
<None Update="VersionConverter.cs.template">
29+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
30+
</None>
2531
</ItemGroup>
2632

2733
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using k8s.Versioning;
2+
3+
namespace k8s.Models
4+
{
5+
{{#.}}
6+
public partial class {{GetTuple . index="0"}}
7+
{
8+
public static explicit operator {{GetTuple . index="0"}}({{GetTuple . index="1"}} s) => VersionConverter.Mapper.Map<{{GetTuple . index="0"}}>(s);
9+
}
10+
public partial class {{GetTuple . index="1"}}
11+
{
12+
public static explicit operator {{GetTuple . index="1"}}({{GetTuple . index="0"}} s) => VersionConverter.Mapper.Map<{{GetTuple . index="1"}}>(s);
13+
}
14+
{{/.}}
15+
}

gen/KubernetesWatchGenerator/Program.cs

+47
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using System.Runtime.CompilerServices;
9+
using System.Text.RegularExpressions;
810
using System.Threading.Tasks;
911

1012
namespace KubernetesWatchGenerator
@@ -88,6 +90,7 @@ static async Task Main(string[] args)
8890
Helpers.Register(nameof(GetApiVersion), GetApiVersion);
8991
Helpers.Register(nameof(GetKind), GetKind);
9092
Helpers.Register(nameof(GetPlural), GetPlural);
93+
Helpers.Register(nameof(GetTuple), GetTuple);
9194

9295
// Generate the Watcher operations
9396
// We skip operations where the name of the class in the C# client could not be determined correctly.
@@ -125,8 +128,39 @@ static async Task Main(string[] args)
125128
.ToHashSet();
126129

127130
Render.FileToFile("ModelExtensions.cs.template", definitions, Path.Combine(outputDirectory, "ModelExtensions.cs"));
131+
132+
// generate version converter maps
133+
var allGeneratedModelClassNames = Directory
134+
.EnumerateFiles(Path.Combine(outputDirectory, "Models"))
135+
.Select(Path.GetFileNameWithoutExtension)
136+
.ToList();
137+
138+
var versionRegex = @"(^V|v)[0-9]+((alpha|beta)[0-9]+)?";
139+
var typePairs = allGeneratedModelClassNames
140+
.OrderBy(x => x)
141+
.Select(x => new {Version = Regex.Match(x, versionRegex).Value?.ToLower(), Kinda = Regex.Replace(x, versionRegex, string.Empty), Type = x})
142+
.Where(x => !string.IsNullOrEmpty(x.Version))
143+
.GroupBy(x => x.Kinda)
144+
.Where(x => x.Count() > 1)
145+
.SelectMany(x => x.SelectMany((value, index) => x.Skip(index + 1), (first, second) => new { first, second }))
146+
.OrderBy(x =>x.first.Kinda)
147+
.ThenBy(x => x.first.Version)
148+
.Select(x => (ITuple)Tuple.Create(x.first.Type, x.second.Type))
149+
.ToList();
150+
151+
var versionFile = File.ReadAllText(Path.Combine(outputDirectory, "..", "Versioning", "VersionConverter.cs"));
152+
var manualMaps = Regex.Matches(versionFile, @"\.CreateMap<(?<T1>.+?),\s?(?<T2>.+?)>")
153+
.Select(x => Tuple.Create(x.Groups["T1"].Value, x.Groups["T2"].Value))
154+
.ToList();
155+
var versionConverterPairs = typePairs.Except(manualMaps).ToList();
156+
157+
Render.FileToFile("VersionConverter.cs.template", versionConverterPairs, Path.Combine(outputDirectory, "VersionConverter.cs"));
158+
Render.FileToFile("ModelOperators.cs.template", typePairs, Path.Combine(outputDirectory, "ModelOperators.cs"));
159+
128160
}
129161

162+
163+
130164
static void ToXmlDoc(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
131165
{
132166
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is string)
@@ -153,6 +187,18 @@ static void ToXmlDoc(RenderContext context, IList<object> arguments, IDictionary
153187
}
154188
}
155189

190+
static void GetTuple(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
191+
{
192+
if (arguments != null && arguments.Count > 0 && arguments[0] is ITuple && options.TryGetValue("index", out var indexObj) && int.TryParse(indexObj?.ToString(), out var index))
193+
{
194+
var pair = (ITuple)arguments[0];
195+
var value = pair[index];
196+
context.Write(value.ToString());
197+
}
198+
}
199+
200+
201+
156202
static void GetClassName(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
157203
{
158204
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is SwaggerOperation)
@@ -309,6 +355,7 @@ static void GetMethodName(RenderContext context, IList<object> arguments, IDicti
309355
}
310356
}
311357

358+
312359
static string GetMethodName(SwaggerOperation watchOperation)
313360
{
314361
var tag = watchOperation.Tags[0];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using AutoMapper;
2+
using k8s.Models;
3+
4+
namespace k8s.Versioning
5+
{
6+
7+
8+
public static partial class VersionConverter
9+
{
10+
private static void AutoConfigurations(IMapperConfigurationExpression cfg)
11+
{
12+
{{#.}}
13+
cfg.CreateMap<{{GetTuple . index="0"}}, {{GetTuple . index="1"}}>().ReverseMap();
14+
{{/.}}
15+
}
16+
}
17+
18+
19+
}

src/KubernetesClient/Extensions.cs

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Reflection;
7+
using System.Text.RegularExpressions;
8+
using System.Threading.Tasks;
9+
using k8s.Models;
10+
using Microsoft.Rest;
11+
using Microsoft.Rest.TransientFaultHandling;
12+
using Newtonsoft.Json.Converters;
13+
using VersionConverter = k8s.Versioning.VersionConverter;
14+
15+
namespace k8s
16+
{
17+
public static class Extensions
18+
{
19+
20+
public static KubernetesEntityAttribute GetKubernetesTypeMetadata<T>(this T obj) where T : IKubernetesObject =>
21+
obj.GetType().GetKubernetesTypeMetadata();
22+
public static KubernetesEntityAttribute GetKubernetesTypeMetadata(this Type currentType)
23+
{
24+
var attr = currentType.GetCustomAttribute<KubernetesEntityAttribute>();
25+
if (attr == null)
26+
{
27+
throw new InvalidOperationException($"Custom resource must have {nameof(KubernetesEntityAttribute)} applied to it");
28+
}
29+
return attr;
30+
}
31+
public static T Initialize<T>(this T obj) where T : IKubernetesObject
32+
{
33+
var metadata = obj.GetKubernetesTypeMetadata();
34+
35+
obj.ApiVersion = !string.IsNullOrEmpty(metadata.Group) ? $"{metadata.Group}/{metadata.ApiVersion}" : metadata.ApiVersion;
36+
obj.Kind = metadata.Kind ?? obj.GetType().Name;
37+
if(obj is IMetadata<V1ObjectMeta> withMetadata && withMetadata.Metadata == null)
38+
withMetadata.Metadata = new V1ObjectMeta();
39+
return obj;
40+
}
41+
42+
internal static bool IsValidKubernetesName(this string value) => !Regex.IsMatch(value, "^[a-z0-9-]+$");
43+
44+
45+
}
46+
}

src/KubernetesClient/KubernetesClient.csproj

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
</PropertyGroup>
2828

2929
<ItemGroup>
30+
<PackageReference Include="AutoMapper" Version="7.0.1" Condition="'$(TargetFramework)' == 'net452'" />
31+
<PackageReference Include="AutoMapper" Version="9.0.0" Condition="'$(TargetFramework)' != 'net452'" />
3032
<PackageReference Include="Fractions" Version="4.0.1" />
3133
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="1.1.2" Condition="'$(TargetFramework)' != 'netstandard2.0' and '$(TargetFramework)' != 'netcoreapp2.1'" />
3234
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="3.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp2.1'" />
@@ -42,6 +44,7 @@
4244
</ItemGroup>
4345
<ItemGroup Condition="'$(TargetFramework)' == 'net452'">
4446
<Reference Include="System.Net.Http.WebRequest" />
45-
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
47+
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
4648
</ItemGroup>
49+
4750
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.RegularExpressions;
4+
5+
namespace k8s.Versioning
6+
{
7+
public class KubernetesVersionComparer : IComparer<string>
8+
{
9+
public static KubernetesVersionComparer Instance { get; private set; }
10+
static readonly Regex _kubernetesVersionRegex;
11+
12+
static KubernetesVersionComparer()
13+
{
14+
_kubernetesVersionRegex = new Regex(@"^v(?<major>[0-9]+)((?<stream>alpha|beta)(?<minor>[0-9]+))?$", RegexOptions.Compiled);
15+
Instance = new KubernetesVersionComparer();
16+
}
17+
18+
internal KubernetesVersionComparer()
19+
{
20+
}
21+
22+
public int Compare(string x, string y)
23+
{
24+
if (x == null || y == null)
25+
return StringComparer.CurrentCulture.Compare(x, y);
26+
var matchX = _kubernetesVersionRegex.Match(x);
27+
if(!matchX.Success)
28+
return StringComparer.CurrentCulture.Compare(x, y);
29+
var matchY = _kubernetesVersionRegex.Match(y);
30+
if(!matchY.Success)
31+
return StringComparer.CurrentCulture.Compare(x, y);
32+
var versionX = ExtractVersion(matchX);
33+
var versionY = ExtractVersion(matchY);
34+
return versionX.CompareTo(versionY);
35+
}
36+
37+
private Version ExtractVersion(Match match)
38+
{
39+
40+
var major = int.Parse(match.Groups["major"].Value);
41+
if (!Enum.TryParse<Stream>(match.Groups["stream"].Value, true, out var stream))
42+
stream = Stream.Final;
43+
int.TryParse(match.Groups["minor"].Value, out var minor);
44+
return new Version(major, (int)stream, minor);
45+
}
46+
47+
private enum Stream
48+
{
49+
Alpha = 1,
50+
Beta = 2,
51+
Final = 3
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)