Skip to content

Commit f1a3586

Browse files
authored
Support for conversions between models of different versions (#420)
1 parent a4ff002 commit f1a3586

12 files changed

+2832
-3
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

+48-2
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
@@ -101,6 +103,7 @@ static async Task Main(string[] args)
101103
Helpers.Register(nameof(GetApiVersion), GetApiVersion);
102104
Helpers.Register(nameof(GetKind), GetKind);
103105
Helpers.Register(nameof(GetPlural), GetPlural);
106+
Helpers.Register(nameof(GetTuple), GetTuple);
104107

105108
// Generate the Watcher operations
106109
// We skip operations where the name of the class in the C# client could not be determined correctly.
@@ -134,10 +137,40 @@ static async Task Main(string[] args)
134137
.Select(x => x.Class)
135138
.ToHashSet();
136139

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

172+
173+
141174
static void ToXmlDoc(RenderContext context, IList<object> arguments, IDictionary<string, object> options,
142175
RenderBlock fn, RenderBlock inverse)
143176
{
@@ -166,6 +199,18 @@ static void ToXmlDoc(RenderContext context, IList<object> arguments, IDictionary
166199
}
167200
}
168201

202+
static void GetTuple(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
203+
{
204+
if (arguments != null && arguments.Count > 0 && arguments[0] is ITuple && options.TryGetValue("index", out var indexObj) && int.TryParse(indexObj?.ToString(), out var index))
205+
{
206+
var pair = (ITuple)arguments[0];
207+
var value = pair[index];
208+
context.Write(value.ToString());
209+
}
210+
}
211+
212+
213+
169214
static void GetClassName(RenderContext context, IList<object> arguments, IDictionary<string, object> options,
170215
RenderBlock fn, RenderBlock inverse)
171216
{
@@ -343,6 +388,7 @@ static void GetMethodName(RenderContext context, IList<object> arguments, IDicti
343388
}
344389
}
345390

391+
346392
static string GetMethodName(SwaggerOperation watchOperation)
347393
{
348394
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

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
32+
public static T Initialize<T>(this T obj) where T : IKubernetesObject
33+
{
34+
var metadata = obj.GetKubernetesTypeMetadata();
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+
{
39+
withMetadata.Metadata = new V1ObjectMeta();
40+
}
41+
42+
return obj;
43+
}
44+
45+
internal static bool IsValidKubernetesName(this string value) => !Regex.IsMatch(value, "^[a-z0-9-]+$");
46+
47+
48+
}
49+
}

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'" />
@@ -41,6 +43,7 @@
4143
</ItemGroup>
4244
<ItemGroup Condition="'$(TargetFramework)' == 'net452'">
4345
<Reference Include="System.Net.Http.WebRequest" />
44-
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
46+
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
4547
</ItemGroup>
48+
4649
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
{
26+
return StringComparer.CurrentCulture.Compare(x, y);
27+
}
28+
29+
var matchX = _kubernetesVersionRegex.Match(x);
30+
if (!matchX.Success)
31+
{
32+
return StringComparer.CurrentCulture.Compare(x, y);
33+
}
34+
35+
var matchY = _kubernetesVersionRegex.Match(y);
36+
if (!matchY.Success)
37+
{
38+
return StringComparer.CurrentCulture.Compare(x, y);
39+
}
40+
41+
var versionX = ExtractVersion(matchX);
42+
var versionY = ExtractVersion(matchY);
43+
return versionX.CompareTo(versionY);
44+
}
45+
46+
private Version ExtractVersion(Match match)
47+
{
48+
var major = int.Parse(match.Groups["major"].Value);
49+
if (!Enum.TryParse<Stream>(match.Groups["stream"].Value, true, out var stream))
50+
{
51+
stream = Stream.Final;
52+
}
53+
54+
int.TryParse(match.Groups["minor"].Value, out var minor);
55+
return new Version(major, (int)stream, minor);
56+
}
57+
58+
private enum Stream
59+
{
60+
Alpha = 1,
61+
Beta = 2,
62+
Final = 3
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)