forked from dotnet/dotnet-operator-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathConversionWebhook.cs
More file actions
175 lines (150 loc) · 7.35 KB
/
Copy pathConversionWebhook.cs
File metadata and controls
175 lines (150 loc) · 7.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Versioning;
using System.Text.Json;
using k8s;
using k8s.Models;
using Microsoft.AspNetCore.Mvc;
namespace KubeOps.Operator.Web.Webhooks.Conversion;
/// <summary>
/// Base class for conversion webhooks. This class handles the conversion of
/// entities in their versions. Must be annotated with the <see cref="ConversionWebhookAttribute"/>.
/// </summary>
/// <typeparam name="TEntity">The target type (version) of the entity.</typeparam>
[RequiresPreviewFeatures(
"Conversion webhooks API is not yet stable, the way that conversion " +
"webhooks are implemented may change in the future based on user feedback.")]
[ApiController]
public abstract class ConversionWebhook<TEntity> : ControllerBase
where TEntity : IKubernetesObject<V1ObjectMeta>
{
private JsonSerializerOptions _serializerOptions = null!;
protected ConversionWebhook()
{
KubernetesJson.AddJsonOptions(c => _serializerOptions = c);
}
/// <summary>
/// The list of converters that are available for this webhook.
/// </summary>
protected abstract IEnumerable<IEntityConverter<TEntity>> Converters { get; }
private IEnumerable<(string To, string From, Func<object, object> Converter, Type FromType)> AvailableConversions =>
Converters
.SelectMany(c => new (string, string, Func<object, object>, Type)[]
{
(c.ToGroupVersion, c.FromGroupVersion, o => c.Convert(o), c.FromType),
(c.FromGroupVersion, c.ToGroupVersion, o => c.Revert((TEntity)o), c.ToType),
});
[HttpPost]
public IActionResult Convert([FromBody] ConversionRequest request)
{
try
{
var conversions = AvailableConversions.ToList();
// The conversion chain depends only on the source and desired versions, and the desired version is constant
// for the whole request, so the chain is cached by source apiVersion. A LIST response typically converts
// many objects that share the same stored version, so this avoids repeating the graph search (and the scans
// over the conversions list) for every object.
var paths = new Dictionary<string, (IReadOnlyList<Func<object, object>> Steps, Type SourceType)>();
var results = new List<object>();
for (var index = 0; index < request.Request.Objects.Length; index++)
{
var obj = request.Request.Objects[index];
if (obj["apiVersion"]?.GetValue<string>() is not { } sourceApiVersion)
{
return new ConversionResponse(
request.Request.Uid,
$"Object at index {index} does not contain a valid apiVersion.");
}
if (!paths.TryGetValue(sourceApiVersion, out var path))
{
if (!TryBuildConversionPath(
conversions,
sourceApiVersion,
request.Request.DesiredApiVersion,
out var steps,
out var sourceType))
{
return new ConversionResponse(
request.Request.Uid,
$"No conversion path exists from '{sourceApiVersion}' to " +
$"'{request.Request.DesiredApiVersion}' for object at index {index}.");
}
path = (steps, sourceType);
paths[sourceApiVersion] = path;
}
var converted = obj.Deserialize(path.SourceType, _serializerOptions)
?? throw new InvalidOperationException(
$"Object at index {index} could not be deserialized as '{path.SourceType}'.");
foreach (var step in path.Steps)
{
converted = step(converted);
}
results.Add(converted);
}
return new ConversionResponse(request.Request.Uid, results);
}
catch (Exception e)
{
return new ConversionResponse(request.Request.Uid, e.ToString());
}
}
/// <summary>
/// Builds a chain of registered converters that transforms an object from <paramref name="from"/> to
/// <paramref name="to"/>. Converters are registered as a hub-and-spoke set (every served version converts to and
/// from the storage/hub version), so the API server can legitimately request a conversion between two versions that
/// have no direct converter — e.g. an object persisted under a version that is no longer the storage version, read
/// at a third served version. In that case the conversion is composed through intermediate versions (a
/// breadth-first search picks the shortest chain), rather than the object being silently dropped.
/// </summary>
/// <param name="conversions">The directed conversion edges available for this webhook.</param>
/// <param name="from">The <c>apiVersion</c> the source object is encoded in.</param>
/// <param name="to">The requested <c>desiredAPIVersion</c>.</param>
/// <param name="steps">The ordered conversion functions to apply to the deserialized source object.</param>
/// <param name="sourceType">The CLR type the source object must be deserialized into before applying the steps.</param>
/// <returns><see langword="true"/> if a conversion chain exists; otherwise <see langword="false"/>.</returns>
private static bool TryBuildConversionPath(
IReadOnlyList<(string To, string From, Func<object, object> Converter, Type FromType)> conversions,
string from,
string to,
out IReadOnlyList<Func<object, object>> steps,
[NotNullWhen(true)] out Type? sourceType)
{
steps = [];
sourceType = null;
// Nothing to convert; also guards against walking an edge back to the source through the hub.
if (from == to)
{
return false;
}
// Breadth-first search over the directed conversion edges so the shortest chain is chosen. A direct converter,
// when one exists, is found as a single-hop path and therefore keeps the previous behaviour unchanged.
var queue = new Queue<List<(string To, string From, Func<object, object> Converter, Type FromType)>>();
var visited = new HashSet<string> { from };
foreach (var edge in conversions.Where(c => c.From == from))
{
queue.Enqueue([edge]);
}
while (queue.Count > 0)
{
var path = queue.Dequeue();
var last = path[^1];
if (last.To == to)
{
steps = [.. path.Select(e => e.Converter)];
sourceType = path[0].FromType;
return true;
}
if (!visited.Add(last.To))
{
continue;
}
foreach (var edge in conversions.Where(c => c.From == last.To))
{
queue.Enqueue([.. path, edge]);
}
}
return false;
}
}