Skip to content

Commit abf6950

Browse files
authored
Proposal: user defined model version explicit converter (#1211)
* introduce user defined model version explicit convertor * bump ver
1 parent 2959174 commit abf6950

18 files changed

+424
-271
lines changed

kubernetes-client.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "workerServiceDependencyInje
7171
EndProject
7272
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cp", "examples\cp\cp.csproj", "{CC41E248-2139-427E-8DD4-B047A8924FD2}"
7373
EndProject
74+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KubernetesClient.ModelConverter", "src\KubernetesClient.ModelConverter\KubernetesClient.ModelConverter.csproj", "{F1C8276A-8A60-4362-96B8-A006314446AE}"
75+
EndProject
7476
Global
7577
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7678
Debug|Any CPU = Debug|Any CPU
@@ -453,6 +455,18 @@ Global
453455
{CC41E248-2139-427E-8DD4-B047A8924FD2}.Release|x64.Build.0 = Release|Any CPU
454456
{CC41E248-2139-427E-8DD4-B047A8924FD2}.Release|x86.ActiveCfg = Release|Any CPU
455457
{CC41E248-2139-427E-8DD4-B047A8924FD2}.Release|x86.Build.0 = Release|Any CPU
458+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
459+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
460+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Debug|x64.ActiveCfg = Debug|Any CPU
461+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Debug|x64.Build.0 = Debug|Any CPU
462+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Debug|x86.ActiveCfg = Debug|Any CPU
463+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Debug|x86.Build.0 = Debug|Any CPU
464+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
465+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Release|Any CPU.Build.0 = Release|Any CPU
466+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Release|x64.ActiveCfg = Release|Any CPU
467+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Release|x64.Build.0 = Release|Any CPU
468+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Release|x86.ActiveCfg = Release|Any CPU
469+
{F1C8276A-8A60-4362-96B8-A006314446AE}.Release|x86.Build.0 = Release|Any CPU
456470
EndGlobalSection
457471
GlobalSection(SolutionProperties) = preSolution
458472
HideSolutionNode = FALSE
@@ -489,6 +503,7 @@ Global
489503
{C0759F88-A010-4DEF-BD3B-E183D3328FFC} = {B70AFB57-57C9-46DC-84BE-11B7DDD34B40}
490504
{05DC8884-AC54-4603-AC25-AE9D9F24E7AE} = {B70AFB57-57C9-46DC-84BE-11B7DDD34B40}
491505
{CC41E248-2139-427E-8DD4-B047A8924FD2} = {B70AFB57-57C9-46DC-84BE-11B7DDD34B40}
506+
{F1C8276A-8A60-4362-96B8-A006314446AE} = {3D1864AA-1FFC-4512-BB13-46055E410F73}
492507
EndGlobalSection
493508
GlobalSection(ExtensibilityGlobals) = postSolution
494509
SolutionGuid = {049A763A-C891-4E8D-80CF-89DD3E22ADC7}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("KubernetesClient")]
4+
[assembly: InternalsVisibleTo("KubernetesClient.Classic")]
5+
[assembly: InternalsVisibleTo("KubernetesClient.Basic")]
6+
[assembly: InternalsVisibleTo("KubernetesClient.Tests")]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using static k8s.Models.ModelVersionConverter;
2+
3+
namespace k8s.ModelConverter.AutoMapper;
4+
5+
public class AutoMapperModelVersionConverter : IModelVersionConverter
6+
{
7+
public static IModelVersionConverter Instance { get; } = new AutoMapperModelVersionConverter();
8+
9+
private AutoMapperModelVersionConverter()
10+
{
11+
}
12+
13+
public TTo Convert<TFrom, TTo>(TFrom from)
14+
{
15+
return VersionConverter.Mapper.Map<TTo>(from);
16+
}
17+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.RegularExpressions;
4+
5+
namespace k8s.ModelConverter.AutoMapper;
6+
7+
internal class KubernetesVersionComparer : IComparer<string>
8+
{
9+
public static KubernetesVersionComparer Instance { get; } = new KubernetesVersionComparer();
10+
private static readonly Regex KubernetesVersionRegex = new Regex(@"^v(?<major>[0-9]+)((?<stream>alpha|beta)(?<minor>[0-9]+))?$", RegexOptions.Compiled);
11+
12+
internal KubernetesVersionComparer()
13+
{
14+
}
15+
16+
public int Compare(string x, string y)
17+
{
18+
if (x == null || y == null)
19+
{
20+
return StringComparer.CurrentCulture.Compare(x, y);
21+
}
22+
23+
var matchX = KubernetesVersionRegex.Match(x);
24+
if (!matchX.Success)
25+
{
26+
return StringComparer.CurrentCulture.Compare(x, y);
27+
}
28+
29+
var matchY = KubernetesVersionRegex.Match(y);
30+
if (!matchY.Success)
31+
{
32+
return StringComparer.CurrentCulture.Compare(x, y);
33+
}
34+
35+
var versionX = ExtractVersion(matchX);
36+
var versionY = ExtractVersion(matchY);
37+
return versionX.CompareTo(versionY);
38+
}
39+
40+
private Version ExtractVersion(Match match)
41+
{
42+
var major = int.Parse(match.Groups["major"].Value);
43+
if (!Enum.TryParse<Stream>(match.Groups["stream"].Value, true, out var stream))
44+
{
45+
stream = Stream.Final;
46+
}
47+
48+
_ = int.TryParse(match.Groups["minor"].Value, out var minor);
49+
return new Version(major, (int)stream, minor);
50+
}
51+
52+
private enum Stream
53+
{
54+
Alpha = 1,
55+
Beta = 2,
56+
Final = 3,
57+
}
58+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// WARNING: DO NOT LEAVE COMMENTED CODE IN THIS FILE. IT GETS SCANNED BY GEN PROJECT SO IT CAN EXCLUDE ANY MANUALLY DEFINED MAPS
2+
3+
using AutoMapper;
4+
#if NET6_0_OR_GREATER
5+
using AutoMapper.Internal;
6+
#endif
7+
using k8s.Models;
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Linq;
11+
using System.Reflection;
12+
13+
namespace k8s.ModelConverter.AutoMapper;
14+
15+
/// <summary>
16+
/// Provides mappers that converts Kubernetes models between different versions
17+
/// </summary>
18+
internal static partial class VersionConverter
19+
{
20+
static VersionConverter()
21+
{
22+
UpdateMappingConfiguration(expression => { });
23+
}
24+
25+
public static IMapper Mapper { get; private set; }
26+
internal static MapperConfiguration MapperConfiguration { get; private set; }
27+
28+
/// <summary>
29+
/// Two level lookup of model types by Kind and then Version
30+
/// </summary>
31+
internal static Dictionary<string, Dictionary<string, Type>> KindVersionsMap { get; private set; }
32+
33+
public static Type GetTypeForVersion<T>(string version)
34+
{
35+
return GetTypeForVersion(typeof(T), version);
36+
}
37+
38+
public static Type GetTypeForVersion(Type type, string version)
39+
{
40+
return KindVersionsMap[type.GetKubernetesTypeMetadata().Kind][version];
41+
}
42+
43+
public static void UpdateMappingConfiguration(Action<IMapperConfigurationExpression> configuration)
44+
{
45+
MapperConfiguration = new MapperConfiguration(cfg =>
46+
{
47+
GetConfigurations(cfg);
48+
configuration(cfg);
49+
});
50+
Mapper = MapperConfiguration
51+
#if NET6_0_OR_GREATER
52+
.Internal()
53+
#endif
54+
.CreateMapper();
55+
KindVersionsMap = MapperConfiguration
56+
#if NET6_0_OR_GREATER
57+
.Internal()
58+
#endif
59+
.GetAllTypeMaps()
60+
.SelectMany(x => new[] { x.Types.SourceType, x.Types.DestinationType })
61+
.Where(x => x.GetCustomAttribute<KubernetesEntityAttribute>() != null)
62+
.Select(x =>
63+
{
64+
var attr = GetKubernetesEntityAttribute(x);
65+
return new { attr.Kind, attr.ApiVersion, Type = x };
66+
})
67+
.GroupBy(x => x.Kind)
68+
.ToDictionary(x => x.Key, kindGroup => kindGroup
69+
.GroupBy(x => x.ApiVersion)
70+
.ToDictionary(
71+
x => x.Key,
72+
versionGroup => versionGroup.Select(x => x.Type).Distinct().Single())); // should only be one type for each Kind/Version combination
73+
}
74+
75+
public static object ConvertToVersion(object source, string apiVersion)
76+
{
77+
if (source == null)
78+
{
79+
throw new ArgumentNullException(nameof(source));
80+
}
81+
82+
var type = source.GetType();
83+
var attr = GetKubernetesEntityAttribute(type);
84+
if (attr.ApiVersion == apiVersion)
85+
{
86+
return source;
87+
}
88+
89+
if (!KindVersionsMap.TryGetValue(attr.Kind, out var kindVersions))
90+
{
91+
throw new InvalidOperationException($"Version converter does not have any registered types for Kind `{attr.Kind}`");
92+
}
93+
94+
if (!kindVersions.TryGetValue(apiVersion, out var targetType) || !kindVersions.TryGetValue(attr.ApiVersion, out var sourceType) ||
95+
MapperConfiguration
96+
#if NET6_0_OR_GREATER
97+
.Internal()
98+
#endif
99+
.FindTypeMapFor(sourceType, targetType) == null)
100+
{
101+
throw new InvalidOperationException($"There is no conversion mapping registered for Kind `{attr.Kind}` from ApiVersion {attr.ApiVersion} to {apiVersion}");
102+
}
103+
104+
return Mapper.Map(source, sourceType, targetType);
105+
}
106+
107+
private static KubernetesEntityAttribute GetKubernetesEntityAttribute(Type type)
108+
{
109+
if (type == null)
110+
{
111+
throw new ArgumentNullException(nameof(type));
112+
}
113+
114+
var attr = type.GetCustomAttribute<KubernetesEntityAttribute>();
115+
if (attr == null)
116+
{
117+
throw new InvalidOperationException($"Type {type} does not have {nameof(KubernetesEntityAttribute)}");
118+
}
119+
120+
return attr;
121+
}
122+
123+
internal static void GetConfigurations(IMapperConfigurationExpression cfg)
124+
{
125+
AutoConfigurations(cfg);
126+
ManualConfigurations(cfg);
127+
}
128+
129+
private static void ManualConfigurations(IMapperConfigurationExpression cfg)
130+
{
131+
cfg.AllowNullCollections = true;
132+
cfg.DisableConstructorMapping();
133+
cfg
134+
#if NET6_0_OR_GREATER
135+
.Internal()
136+
#endif
137+
.ForAllMaps((typeMap, opt) =>
138+
{
139+
if (!typeof(IKubernetesObject).IsAssignableFrom(typeMap.Types.DestinationType))
140+
{
141+
return;
142+
}
143+
144+
var metadata = typeMap.Types.DestinationType.GetKubernetesTypeMetadata();
145+
opt.ForMember(nameof(IKubernetesObject.ApiVersion), x => x.Ignore());
146+
opt.ForMember(nameof(IKubernetesObject.Kind), x => x.Ignore());
147+
opt.AfterMap((from, to) =>
148+
{
149+
var obj = (IKubernetesObject)to;
150+
obj.ApiVersion = !string.IsNullOrEmpty(metadata.Group) ? $"{metadata.Group}/{metadata.ApiVersion}" : metadata.ApiVersion;
151+
obj.Kind = metadata.Kind;
152+
});
153+
});
154+
155+
cfg.CreateMap<V1Subject, V1beta2Subject>()
156+
.ForMember(dest => dest.Group, opt => opt.Ignore())
157+
.ForMember(dest => dest.ServiceAccount, opt => opt.Ignore())
158+
.ForMember(dest => dest.User, opt => opt.Ignore())
159+
.ReverseMap();
160+
161+
cfg.CreateMap<V1Subject, V1beta3Subject>()
162+
.ForMember(dest => dest.Group, opt => opt.Ignore())
163+
.ForMember(dest => dest.ServiceAccount, opt => opt.Ignore())
164+
.ForMember(dest => dest.User, opt => opt.Ignore())
165+
.ReverseMap();
166+
167+
cfg.CreateMap<V1HorizontalPodAutoscalerSpec, V2HorizontalPodAutoscalerSpec>()
168+
.ForMember(dest => dest.Metrics, opt => opt.Ignore())
169+
.ForMember(dest => dest.Behavior, opt => opt.Ignore())
170+
.ReverseMap();
171+
172+
173+
cfg.CreateMap<V1HorizontalPodAutoscalerStatus, V2HorizontalPodAutoscalerStatus>()
174+
.ForMember(dest => dest.Conditions, opt => opt.Ignore())
175+
.ForMember(dest => dest.CurrentMetrics, opt => opt.Ignore())
176+
.ReverseMap();
177+
178+
cfg.CreateMap<V1alpha1ResourceClaim, V1ResourceClaim>()
179+
.ForMember(dest => dest.Name, opt => opt.Ignore())
180+
.ReverseMap();
181+
182+
cfg.CreateMap<V1beta2LimitedPriorityLevelConfiguration, V1beta3LimitedPriorityLevelConfiguration>()
183+
.ForMember(dest => dest.NominalConcurrencyShares, opt => opt.Ignore())
184+
.ReverseMap();
185+
}
186+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
4+
<RootNamespace>k8s.ModelConverter</RootNamespace>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="Generator" />
9+
<AdditionalFiles Include="..\..\swagger.json" Generator="versionconverterautomap,version" />
10+
<ProjectReference Include="..\LibKubernetesGenerator\LibKubernetesGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\KubernetesClient.Models\KubernetesClient.Models.csproj" />
15+
<PackageReference Include="AutoMapper" Version="12.0.0" />
16+
</ItemGroup>
17+
</Project>

src/KubernetesClient.Models/KubernetesClient.Models.csproj

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66

77
<ItemGroup>
88
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="Generator" />
9-
<AdditionalFiles Include="..\..\swagger.json" Generator="model,modelext,versionconverter,version" />
9+
<AdditionalFiles Include="..\..\swagger.json" Generator="model,modelext,version,versionconverterstub" />
1010
<ProjectReference Include="..\LibKubernetesGenerator\LibKubernetesGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
1111
</ItemGroup>
1212

1313
<ItemGroup>
1414
<PackageReference Include="System.Text.Json" Version="7.0.2" />
15-
<PackageReference Include="AutoMapper" Version="12.0.0" Condition="'$(TargetFramework)' != 'netstandard2.0'" />
16-
<PackageReference Include="AutoMapper" Version="10.1.1" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
1715
<PackageReference Include="Fractions" Version="7.2.1" />
1816
<PackageReference Include="YamlDotNet" Version="13.0.1" />
1917
</ItemGroup>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace k8s.Models;
2+
3+
public static class ModelVersionConverter
4+
{
5+
public interface IModelVersionConverter
6+
{
7+
TTo Convert<TFrom, TTo>(TFrom from);
8+
}
9+
10+
public static IModelVersionConverter Converter { get; set; }
11+
12+
internal static TTo Convert<TFrom, TTo>(TFrom from)
13+
{
14+
if (Converter == null)
15+
{
16+
throw new InvalidOperationException("Converter is not set");
17+
}
18+
19+
return Converter.Convert<TFrom, TTo>(from);
20+
}
21+
}

0 commit comments

Comments
 (0)