Skip to content

Commit f6227e4

Browse files
authored
fix(transpiler): add validation for circular type references (#1141)
- Added `CircularTypeReferenceException` and `InvalidTypeException` to handle invalid or circular references. - Enhanced transpiler to detect circular types and fall back to opaque objects when necessary. - Updated test suite with comprehensive circular reference scenarios. closes #351
1 parent c016a27 commit f6227e4

7 files changed

Lines changed: 616 additions & 116 deletions

File tree

docs/docs/operator/building-blocks/entities.mdx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,12 @@ public class EntitySpec
152152
### Special Attributes
153153

154154
- `[Ignore]`: Excludes a property or entity from CRD generation
155-
- `[PreserveUnknownFields]`: Preserves unknown fields in the Kubernetes object
156-
- `[EmbeddedResource]`: Marks a property as an embedded Kubernetes resource
155+
- `[PreserveUnknownFields]`: Allows unknown fields on the annotated object (`x-kubernetes-preserve-unknown-fields: true`). The known fields are still transpiled and validated, so you keep a structural schema for what you model while permitting extra fields. Works the same whether placed on a property or on a class/type: if the type cannot be represented (it contains a circular reference or an otherwise non-transpilable member), it gracefully falls back to an opaque `type: object` with `x-kubernetes-preserve-unknown-fields: true` instead of failing — making this the recommended way to model complex, externally generated, or self-referencing types.
156+
- `[EmbeddedResource]`: Marks a property as an embedded Kubernetes resource. The property type is never traversed; the schema is always an opaque embedded `type: object`.
157+
158+
:::note
159+
The CRD transpiler maps property types recursively. A **circular type reference** that is not opted out via `[PreserveUnknownFields]` or `[Ignore]` cannot be represented as a finite OpenAPI schema and raises a descriptive `TranspilationFailedException` during generation. Annotate the offending property or type with `[PreserveUnknownFields]` or `[Ignore]`, or restructure the type to remove the cycle.
160+
:::
157161

158162
## Example with Multiple Attributes
159163

@@ -264,4 +268,4 @@ subresources:
264268

265269
:::note
266270
`[ScaleSubresource]` and the status subresource are controlled independently. A `Status` property activates `status: {}` regardless of `[ScaleSubresource]`, and `[ScaleSubresource]` adds `scale:` regardless of whether a `Status` property exists.
267-
:::
271+
:::

src/KubeOps.Transpiler/Crds.cs

Lines changed: 182 additions & 112 deletions
Large diffs are not rendered by default.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace KubeOps.Transpiler.Exceptions;
6+
7+
/// <summary>
8+
/// Raised when the CRD transpiler detects a circular type reference that cannot be represented as a
9+
/// finite OpenAPI schema. Derives from <see cref="InvalidOperationException"/> to preserve backwards
10+
/// compatibility for existing catch clauses.
11+
/// </summary>
12+
internal sealed class CircularTypeReferenceException : InvalidOperationException
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="CircularTypeReferenceException"/> class.
16+
/// </summary>
17+
public CircularTypeReferenceException()
18+
{
19+
}
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="CircularTypeReferenceException"/> class.
23+
/// </summary>
24+
/// <param name="message">The error message.</param>
25+
public CircularTypeReferenceException(string? message)
26+
: base(message)
27+
{
28+
}
29+
30+
/// <summary>
31+
/// Initializes a new instance of the <see cref="CircularTypeReferenceException"/> class.
32+
/// </summary>
33+
/// <param name="message">The error message.</param>
34+
/// <param name="innerException">The exception that caused this exception.</param>
35+
public CircularTypeReferenceException(string? message, Exception? innerException)
36+
: base(message, innerException)
37+
{
38+
}
39+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace KubeOps.Transpiler.Exceptions;
6+
7+
/// <summary>
8+
/// Raised when the CRD transpiler encounters a type it cannot map to an OpenAPI schema.
9+
/// </summary>
10+
internal sealed class InvalidTypeException : InvalidOperationException
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="InvalidTypeException"/> class.
14+
/// </summary>
15+
public InvalidTypeException()
16+
{
17+
}
18+
19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="InvalidTypeException"/> class.
21+
/// </summary>
22+
/// <param name="message">The error message.</param>
23+
public InvalidTypeException(string? message)
24+
: base(message)
25+
{
26+
}
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="InvalidTypeException"/> class.
30+
/// </summary>
31+
/// <param name="message">The error message.</param>
32+
/// <param name="innerException">The exception that caused this exception.</param>
33+
public InvalidTypeException(string? message, Exception? innerException)
34+
: base(message, innerException)
35+
{
36+
}
37+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace KubeOps.Transpiler.Exceptions;
6+
7+
/// <summary>
8+
/// Raised when an entity cannot be transpiled into a CRD. The message is prefixed with the affected
9+
/// entity; the concrete cause (for example a circular type reference or a non-transpilable type) is
10+
/// available via <see cref="Exception.InnerException"/>.
11+
/// </summary>
12+
public sealed class TranspilationFailedException : Exception
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="TranspilationFailedException"/> class.
16+
/// </summary>
17+
public TranspilationFailedException()
18+
{
19+
}
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="TranspilationFailedException"/> class.
23+
/// </summary>
24+
/// <param name="message">The error message.</param>
25+
public TranspilationFailedException(string? message)
26+
: base(message)
27+
{
28+
}
29+
30+
/// <summary>
31+
/// Initializes a new instance of the <see cref="TranspilationFailedException"/> class.
32+
/// </summary>
33+
/// <param name="message">The error message.</param>
34+
/// <param name="innerException">The exception that caused this exception.</param>
35+
public TranspilationFailedException(string? message, Exception? innerException)
36+
: base(message, innerException)
37+
{
38+
}
39+
}

0 commit comments

Comments
 (0)