diff --git a/docs/docs/operator/building-blocks/entities.mdx b/docs/docs/operator/building-blocks/entities.mdx index 3e3339fc..99b14932 100644 --- a/docs/docs/operator/building-blocks/entities.mdx +++ b/docs/docs/operator/building-blocks/entities.mdx @@ -154,3 +154,77 @@ public class V1DemoEntity : CustomKubernetesEntity +{ + public class V1DemoEntitySpec + { + public int Replicas { get; set; } = 1; + } +} +``` + +Generated CRD: + +```yaml +subresources: + scale: + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas +``` + +### Example — scale and status subresources together + +When the entity inherits from `CustomKubernetesEntity`, both `status` and `scale` subresources are emitted. `labelSelectorPath` is optional and maps the serialized label selector that HPAs use for targeted scaling. + +```csharp +[KubernetesEntity(Group = "demo.kubeops.dev", ApiVersion = "v1", Kind = "DemoEntity")] +[ScaleSubresource(".spec.replicas", ".status.replicas", ".status.selector")] +public class V1DemoEntity : CustomKubernetesEntity +{ + public class V1DemoEntitySpec + { + public int Replicas { get; set; } = 1; + } + + public class V1DemoEntityStatus + { + public int Replicas { get; set; } + public required string Selector { get; init; } + } +} +``` + +Generated CRD: + +```yaml +subresources: + status: {} + scale: + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + labelSelectorPath: .status.selector +``` + +:::note +`[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. +::: diff --git a/src/KubeOps.Abstractions/Entities/Attributes/ScaleSubresourceAttribute.cs b/src/KubeOps.Abstractions/Entities/Attributes/ScaleSubresourceAttribute.cs new file mode 100644 index 00000000..eddec934 --- /dev/null +++ b/src/KubeOps.Abstractions/Entities/Attributes/ScaleSubresourceAttribute.cs @@ -0,0 +1,25 @@ +// 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. + +namespace KubeOps.Abstractions.Entities.Attributes; + +/// +/// Enables the scale subresource on a Custom Resource Definition, allowing +/// Kubernetes HorizontalPodAutoscalers (HPAs) to scale the resource. +/// +/// JSON path to desired replicas in spec, e.g. .spec.replicas. +/// JSON path to observed replicas in status, e.g. .status.replicas. +/// Optional JSON path to the label selector, e.g. .status.selector. +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class ScaleSubresourceAttribute( + string specReplicasPath, + string statusReplicasPath, + string? labelSelectorPath = null) : Attribute +{ + public string SpecReplicasPath => specReplicasPath; + + public string StatusReplicasPath => statusReplicasPath; + + public string? LabelSelectorPath => labelSelectorPath; +} diff --git a/src/KubeOps.Transpiler/Crds.cs b/src/KubeOps.Transpiler/Crds.cs index e56c2498..9ccd206c 100644 --- a/src/KubeOps.Transpiler/Crds.cs +++ b/src/KubeOps.Transpiler/Crds.cs @@ -68,14 +68,22 @@ public static V1CustomResourceDefinition Transpile(this MetadataLoadContext cont } var version = new V1CustomResourceDefinitionVersion { Name = meta.Version, Served = true, Storage = true }; - if - (type.GetProperty("Status") != null - || type.GetProperty("status") != null) + var hasStatus = type.GetProperty("status", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) != null; + var scaleAttr = type.GetCustomAttributeData(); + + if (hasStatus || scaleAttr != null) { version.Subresources = new() { - Scale = null, - Status = new(), + Status = hasStatus ? new() : null, + Scale = scaleAttr != null + ? new V1CustomResourceSubresourceScale + { + SpecReplicasPath = scaleAttr.GetCustomAttributeCtorArg(context, 0)!, + StatusReplicasPath = scaleAttr.GetCustomAttributeCtorArg(context, 1)!, + LabelSelectorPath = scaleAttr.GetCustomAttributeCtorArg(context, 2), + } + : null, }; } diff --git a/test/KubeOps.Transpiler.Test/Crds.Mlc.Scale.Test.cs b/test/KubeOps.Transpiler.Test/Crds.Mlc.Scale.Test.cs new file mode 100644 index 00000000..65564cac --- /dev/null +++ b/test/KubeOps.Transpiler.Test/Crds.Mlc.Scale.Test.cs @@ -0,0 +1,111 @@ +// 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 FluentAssertions; + +using k8s.Models; + +using KubeOps.Abstractions.Entities; +using KubeOps.Abstractions.Entities.Attributes; + +namespace KubeOps.Transpiler.Test; + +public sealed partial class CrdsMlcTest +{ + [Fact] + public void Should_Not_Add_Scale_SubResource_If_Absent() + { + var crd = _mlc.Transpile(typeof(EntityWithStatus)); + + var subresources = crd.Spec.Versions[0].Subresources; + subresources.Should().NotBeNull(); + subresources.Status.Should().NotBeNull(); + subresources.Scale.Should().BeNull(); + } + + [Fact] + public void Should_Add_Scale_SubResource_With_Required_Paths() + { + var crd = _mlc.Transpile(typeof(EntityWithScaleSubresource)); + + var subresources = crd.Spec.Versions[0].Subresources; + subresources.Should().NotBeNull(); + subresources.Scale.Should().NotBeNull(); + subresources.Scale.SpecReplicasPath.Should().Be(".spec.replicas"); + subresources.Scale.StatusReplicasPath.Should().Be(".status.replicas"); + subresources.Scale.LabelSelectorPath.Should().BeNull(); + } + + [Fact] + public void Should_Add_Scale_Without_Status_SubResource() + { + var crd = _mlc.Transpile(typeof(EntityWithScaleSubresource)); + + var subresources = crd.Spec.Versions[0].Subresources; + subresources.Should().NotBeNull(); + subresources.Status.Should().BeNull(); + } + + [Fact] + public void Should_Add_Scale_SubResource_With_Label_Selector_Path() + { + var crd = _mlc.Transpile(typeof(EntityWithScaleAndSelector)); + + var subresources = crd.Spec.Versions[0].Subresources; + subresources.Should().NotBeNull(); + subresources.Scale.Should().NotBeNull(); + subresources.Scale.SpecReplicasPath.Should().Be(".spec.replicas"); + subresources.Scale.StatusReplicasPath.Should().Be(".status.replicas"); + subresources.Scale.LabelSelectorPath.Should().Be(".status.selector"); + } + + [Fact] + public void Should_Add_Both_Scale_And_Status_SubResources() + { + var crd = _mlc.Transpile(typeof(EntityWithScaleAndStatus)); + + var subresources = crd.Spec.Versions[0].Subresources; + subresources.Should().NotBeNull(); + subresources.Scale.Should().NotBeNull(); + subresources.Scale.SpecReplicasPath.Should().Be(".spec.replicas"); + subresources.Scale.StatusReplicasPath.Should().Be(".status.replicas"); + subresources.Status.Should().NotBeNull(); + } + + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + [ScaleSubresource(".spec.replicas", ".status.replicas")] + public sealed class EntityWithScaleSubresource : CustomKubernetesEntity + { + public sealed class EntitySpec + { + public int Replicas { get; set; } + } + } + + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + [ScaleSubresource(".spec.replicas", ".status.replicas", ".status.selector")] + public sealed class EntityWithScaleAndSelector : CustomKubernetesEntity + { + public sealed class EntitySpec + { + public int Replicas { get; set; } + } + } + + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + [ScaleSubresource(".spec.replicas", ".status.replicas")] + public sealed class EntityWithScaleAndStatus + : CustomKubernetesEntity + { + public sealed class EntitySpec + { + public int Replicas { get; set; } + } + + public sealed class EntityStatus + { + public int Replicas { get; set; } + } + } +} diff --git a/test/KubeOps.Transpiler.Test/Crds.Mlc.Test.cs b/test/KubeOps.Transpiler.Test/Crds.Mlc.Test.cs index 6ff2ec7c..aceadca5 100644 --- a/test/KubeOps.Transpiler.Test/Crds.Mlc.Test.cs +++ b/test/KubeOps.Transpiler.Test/Crds.Mlc.Test.cs @@ -58,7 +58,7 @@ public void Should_Transpile_Entity_Type_Correctly(Type type, string? expectedTy bool? isNullable) { var crd = _mlc.Transpile(type); - var prop = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var prop = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; prop.Type.Should().Be(expectedType); prop.Format.Should().Be(expectedFormat); prop.Nullable.Should().Be(isNullable); @@ -76,7 +76,7 @@ public void Should_Transpile_Entity_Type_Correctly(Type type, string? expectedTy public void Should_Set_Correct_Array_Type(Type type, string expectedType, bool? isNullable) { var crd = _mlc.Transpile(type); - var prop = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"].Items as V1JSONSchemaProps; + var prop = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"].Items as V1JSONSchemaProps; prop!.Type.Should().Be(expectedType); prop.Nullable.Should().Be(isNullable); } @@ -87,7 +87,7 @@ public void Should_Set_Correct_Array_Type(Type type, string expectedType, bool? public void Should_Set_Correct_Dictionary_Additional_Properties_Type(Type type, string expectedType, bool? isNullable) { var crd = _mlc.Transpile(type); - var prop = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"].AdditionalProperties as V1JSONSchemaProps; + var prop = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"].AdditionalProperties as V1JSONSchemaProps; prop!.Type.Should().Be(expectedType); prop.Nullable.Should().Be(isNullable); } @@ -179,14 +179,14 @@ public void Should_Use_Correct_CRD() public void Should_Not_Add_Status_SubResource_If_Absent() { var crd = _mlc.Transpile(typeof(Entity)); - crd.Spec.Versions.First().Subresources?.Status?.Should().BeNull(); + crd.Spec.Versions[0].Subresources?.Status?.Should().BeNull(); } [Fact] public void Should_Add_Status_SubResource_If_Present() { var crd = _mlc.Transpile(typeof(EntityWithStatus)); - crd.Spec.Versions.First().Subresources.Status.Should().NotBeNull(); + crd.Spec.Versions[0].Subresources.Status.Should().NotBeNull(); } [Fact] @@ -204,7 +204,7 @@ public void Should_Set_Description_On_Class() { var crd = _mlc.Transpile(typeof(ClassDescriptionAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"]; specProperties.Description.Should().NotBe(""); } @@ -213,7 +213,7 @@ public void Should_Set_Description() { var crd = _mlc.Transpile(typeof(DescriptionAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.Description.Should().NotBe(""); } @@ -222,7 +222,7 @@ public void Should_Set_ExternalDocs() { var crd = _mlc.Transpile(typeof(ExtDocsAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.ExternalDocs.Url.Should().NotBe(""); } @@ -231,7 +231,7 @@ public void Should_Set_ExternalDocs_Description() { var crd = _mlc.Transpile(typeof(ExtDocsWithDescriptionAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.ExternalDocs.Description.Should().NotBe(""); } @@ -240,7 +240,7 @@ public void Should_Set_Items_Information() { var crd = _mlc.Transpile(typeof(ItemsAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.Type.Should().Be("array"); (specProperties.Items as V1JSONSchemaProps)?.Type?.Should().Be("string"); @@ -286,7 +286,7 @@ public void Should_Set_MultipleOf() { var crd = _mlc.Transpile(typeof(MultipleOfAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.MultipleOf.Should().Be(2); } @@ -296,7 +296,7 @@ public void Should_Set_Pattern() { var crd = _mlc.Transpile(typeof(PatternAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.Pattern.Should().Be(@"/\d*/"); } @@ -306,7 +306,7 @@ public void Should_Set_RangeMinimum() { var crd = _mlc.Transpile(typeof(RangeMinimumAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.Minimum.Should().Be(15); specProperties.ExclusiveMinimum.Should().BeTrue(); @@ -317,7 +317,7 @@ public void Should_Set_RangeMaximum() { var crd = _mlc.Transpile(typeof(RangeMaximumAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.Maximum.Should().Be(15); specProperties.ExclusiveMaximum.Should().BeTrue(); @@ -328,7 +328,7 @@ public void Should_Set_Required() { var crd = _mlc.Transpile(typeof(RequiredAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"]; specProperties.Required.Should().Contain("property"); } @@ -337,7 +337,7 @@ public void Should_Not_Contain_Ignored_Property() { var crd = _mlc.Transpile(typeof(IgnoreAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"]; specProperties.Properties.Should().NotContainKey("property"); } @@ -346,7 +346,7 @@ public void Should_Set_Preserve_Unknown_Fields() { var crd = _mlc.Transpile(typeof(PreserveUnknownFieldsAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.XKubernetesPreserveUnknownFields.Should().BeTrue(); } @@ -355,7 +355,7 @@ public void Should_Set_EmbeddedResource_Fields() { var crd = _mlc.Transpile(typeof(EmbeddedResourceAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.XKubernetesEmbeddedResource.Should().BeTrue(); } @@ -364,7 +364,7 @@ public void Should_Set_Preserve_Unknown_Fields_On_Dictionaries() { var crd = _mlc.Transpile(typeof(SimpleDictionaryEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.XKubernetesPreserveUnknownFields.Should().BeTrue(); } @@ -373,7 +373,7 @@ public void Should_Set_Preserve_Unknown_Fields_On_Classes() { var crd = _mlc.Transpile(typeof(UnknownFieldsEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"]; specProperties.XKubernetesPreserveUnknownFields.Should().BeTrue(); } @@ -382,7 +382,7 @@ public void Should_Set_Preserve_Unknown_Fields_On_System_Object() { var crd = _mlc.Transpile(typeof(EntityWithSystemObject)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"].Properties["obj"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"].Properties["obj"]; specProperties.XKubernetesPreserveUnknownFields.Should().BeTrue(); } @@ -391,7 +391,7 @@ public void Should_Set_Preserve_Unknown_Fields_On_ObjectLists() { var crd = _mlc.Transpile(typeof(UnknownFieldsListEntity)); - var specProperties = (V1JSONSchemaProps)crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"].Properties["propertyList"].Items; + var specProperties = (V1JSONSchemaProps)crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"].Properties["propertyList"].Items; specProperties.XKubernetesPreserveUnknownFields.Should().BeTrue(); } @@ -400,7 +400,7 @@ public void Should_Not_Set_Preserve_Unknown_Fields_On_Generic_Dictionaries() { var crd = _mlc.Transpile(typeof(DictionaryEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.XKubernetesPreserveUnknownFields.Should().BeNull(); } @@ -409,7 +409,7 @@ public void Should_Not_Set_Preserve_Unknown_Fields_On_KeyValuePair_Enumerable() { var crd = _mlc.Transpile(typeof(EnumerableKeyPairsEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.XKubernetesPreserveUnknownFields.Should().BeNull(); } @@ -418,7 +418,7 @@ public void Should_Not_Set_Properties_On_Dictionaries() { var crd = _mlc.Transpile(typeof(SimpleDictionaryEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.Properties.Should().BeNull(); } @@ -427,7 +427,7 @@ public void Should_Not_Set_Properties_On_Generic_Dictionaries() { var crd = _mlc.Transpile(typeof(DictionaryEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.Properties.Should().BeNull(); } @@ -436,7 +436,7 @@ public void Should_Not_Set_Properties_On_KeyValuePair_Enumerable() { var crd = _mlc.Transpile(typeof(EnumerableKeyPairsEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.Properties.Should().BeNull(); } @@ -445,7 +445,7 @@ public void Should_Set_AdditionalProperties_On_Dictionaries_For_Value_type() { var crd = _mlc.Transpile(typeof(DictionaryEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.AdditionalProperties.Should().NotBeNull(); } @@ -454,7 +454,7 @@ public void Should_Set_AdditionalProperties_On_KeyValuePair_For_Value_type() { var crd = _mlc.Transpile(typeof(EnumerableKeyPairsEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.AdditionalProperties.Should().NotBeNull(); } @@ -463,7 +463,7 @@ public void Should_Set_IntOrString() { var crd = _mlc.Transpile(typeof(IntstrOrStringEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.Properties.Should().BeNull(); specProperties.XKubernetesIntOrString.Should().BeTrue(); } @@ -473,7 +473,7 @@ public void Should_Use_PropertyName_From_JsonPropertyAttribute() { var crd = _mlc.Transpile(typeof(JsonPropNameAttrEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties; specProperties.Should().Contain(p => p.Key == "otherName"); } @@ -482,7 +482,7 @@ public void Must_Not_Contain_Ignored_TopLevel_Properties() { var crd = _mlc.Transpile(typeof(Entity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties; specProperties.Should().NotContainKeys("metadata", "apiVersion", "kind"); } @@ -490,7 +490,7 @@ public void Must_Not_Contain_Ignored_TopLevel_Properties() public void Should_Add_AdditionalPrinterColumns() { var crd = _mlc.Transpile(typeof(AdditionalPrinterColumnAttrEntity)); - var apc = crd.Spec.Versions.First().AdditionalPrinterColumns; + var apc = crd.Spec.Versions[0].AdditionalPrinterColumns; apc.Should().ContainSingle(def => def.JsonPath == ".property"); } @@ -498,7 +498,7 @@ public void Should_Add_AdditionalPrinterColumns() public void Should_Add_AdditionalPrinterColumns_With_Prio() { var crd = _mlc.Transpile(typeof(AdditionalPrinterColumnWideAttrEntity)); - var apc = crd.Spec.Versions.First().AdditionalPrinterColumns; + var apc = crd.Spec.Versions[0].AdditionalPrinterColumns; apc.Should().ContainSingle(def => def.JsonPath == ".property" && def.Priority == 1); } @@ -506,7 +506,7 @@ public void Should_Add_AdditionalPrinterColumns_With_Prio() public void Should_Add_AdditionalPrinterColumns_With_Name() { var crd = _mlc.Transpile(typeof(AdditionalPrinterColumnNameAttrEntity)); - var apc = crd.Spec.Versions.First().AdditionalPrinterColumns; + var apc = crd.Spec.Versions[0].AdditionalPrinterColumns; apc.Should().ContainSingle(def => def.JsonPath == ".property" && def.Name == "OtherName"); } @@ -514,7 +514,7 @@ public void Should_Add_AdditionalPrinterColumns_With_Name() public void Should_Add_GenericAdditionalPrinterColumns() { var crd = _mlc.Transpile(typeof(GenericAdditionalPrinterColumnAttrEntity)); - var apc = crd.Spec.Versions.First().AdditionalPrinterColumns; + var apc = crd.Spec.Versions[0].AdditionalPrinterColumns; apc.Should().NotBeNull(); apc.Should().ContainSingle(def => def.JsonPath == ".metadata.namespace" && def.Name == "Namespace"); @@ -534,170 +534,170 @@ public void Should_Correctly_Use_Entity_Scope_Attribute() public void Should_Correctly_Get_Enum_Value_From_JsonStringEnumMemberNameAttribute() { var crd = _mlc.Transpile(typeof(NamedEnumEntity)); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + var specProperties = crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["property"]; specProperties.EnumProperty.Should().BeEquivalentTo(["enumValue1", "enumValue2"]); } #region Test Entity Classes [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class StringTestEntity : CustomKubernetesEntity + private sealed class StringTestEntity : CustomKubernetesEntity { public string Property { get; set; } = string.Empty; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableStringTestEntity : CustomKubernetesEntity + private sealed class NullableStringTestEntity : CustomKubernetesEntity { public string? Property { get; set; } = string.Empty; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class IntTestEntity : CustomKubernetesEntity + private sealed class IntTestEntity : CustomKubernetesEntity { public int Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableIntTestEntity : CustomKubernetesEntity + private sealed class NullableIntTestEntity : CustomKubernetesEntity { public int? Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class LongTestEntity : CustomKubernetesEntity + private sealed class LongTestEntity : CustomKubernetesEntity { public long Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableLongTestEntity : CustomKubernetesEntity + private sealed class NullableLongTestEntity : CustomKubernetesEntity { public long? Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class FloatTestEntity : CustomKubernetesEntity + private sealed class FloatTestEntity : CustomKubernetesEntity { public float Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableFloatTestEntity : CustomKubernetesEntity + private sealed class NullableFloatTestEntity : CustomKubernetesEntity { public float? Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class DecimalTestEntity : CustomKubernetesEntity + private sealed class DecimalTestEntity : CustomKubernetesEntity { public decimal Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableDecimalTestEntity : CustomKubernetesEntity + private sealed class NullableDecimalTestEntity : CustomKubernetesEntity { public decimal? Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class DoubleTestEntity : CustomKubernetesEntity + private sealed class DoubleTestEntity : CustomKubernetesEntity { public double Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableDoubleTestEntity : CustomKubernetesEntity + private sealed class NullableDoubleTestEntity : CustomKubernetesEntity { public double? Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class BoolTestEntity : CustomKubernetesEntity + private sealed class BoolTestEntity : CustomKubernetesEntity { public bool Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableBoolTestEntity : CustomKubernetesEntity + private sealed class NullableBoolTestEntity : CustomKubernetesEntity { public bool? Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class DateTimeTestEntity : CustomKubernetesEntity + private sealed class DateTimeTestEntity : CustomKubernetesEntity { public DateTime Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableDateTimeTestEntity : CustomKubernetesEntity + private sealed class NullableDateTimeTestEntity : CustomKubernetesEntity { public DateTime? Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class DateTimeOffsetTestEntity : CustomKubernetesEntity + private sealed class DateTimeOffsetTestEntity : CustomKubernetesEntity { public DateTimeOffset Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableDateTimeOffsetTestEntity : CustomKubernetesEntity + private sealed class NullableDateTimeOffsetTestEntity : CustomKubernetesEntity { public DateTimeOffset? Property { get; set; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class V1ObjectMetaTestEntity : CustomKubernetesEntity + private sealed class V1ObjectMetaTestEntity : CustomKubernetesEntity { public V1ObjectMeta Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class ResourceQuantityTestEntity : CustomKubernetesEntity + private sealed class ResourceQuantityTestEntity : CustomKubernetesEntity { public ResourceQuantity Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class StringArrayEntity : CustomKubernetesEntity + private sealed class StringArrayEntity : CustomKubernetesEntity { public string[] Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableStringArrayEntity : CustomKubernetesEntity + private sealed class NullableStringArrayEntity : CustomKubernetesEntity { public string[]? Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class EnumerableNullableIntEntity : CustomKubernetesEntity + private sealed class EnumerableNullableIntEntity : CustomKubernetesEntity { public IEnumerable Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class EnumerableIntEntity : CustomKubernetesEntity + private sealed class EnumerableIntEntity : CustomKubernetesEntity { public IEnumerable Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class HashSetIntEntity : CustomKubernetesEntity + private sealed class HashSetIntEntity : CustomKubernetesEntity { public HashSet Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class SetIntEntity : CustomKubernetesEntity + private sealed class SetIntEntity : CustomKubernetesEntity { public ISet Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class InheritedEnumerableEntity : CustomKubernetesEntity + private sealed class InheritedEnumerableEntity : CustomKubernetesEntity { public IntegerList Property { get; set; } = null!; @@ -705,7 +705,7 @@ public class IntegerList : Collection; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class EnumEntity : CustomKubernetesEntity + private sealed class EnumEntity : CustomKubernetesEntity { public TestSpecEnum Property { get; set; } @@ -717,7 +717,7 @@ public enum TestSpecEnum } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NullableEnumEntity : CustomKubernetesEntity + private sealed class NullableEnumEntity : CustomKubernetesEntity { public TestSpecEnum? Property { get; set; } @@ -729,7 +729,7 @@ public enum TestSpecEnum } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class NamedEnumEntity : CustomKubernetesEntity + private sealed class NamedEnumEntity : CustomKubernetesEntity { public TestSpecEnum Property { get; set; } @@ -743,31 +743,31 @@ public enum TestSpecEnum } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class SimpleDictionaryEntity : CustomKubernetesEntity + private sealed class SimpleDictionaryEntity : CustomKubernetesEntity { public IDictionary Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class UnknownFieldsEntity : CustomKubernetesEntity + private sealed class UnknownFieldsEntity : CustomKubernetesEntity { [PreserveUnknownFields] - public class EntitySpec; + public sealed class EntitySpec; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class EntityWithSystemObject : CustomKubernetesEntity + private sealed class EntityWithSystemObject : CustomKubernetesEntity { - public class EntitySpec + public sealed class EntitySpec { public object Obj { get; set; } = null!; } } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class UnknownFieldsListEntity : CustomKubernetesEntity + private sealed class UnknownFieldsListEntity : CustomKubernetesEntity { - public class EntitySpec + public sealed class EntitySpec { public List PropertyList { get; set; } = null!; @@ -777,31 +777,31 @@ public class ObjectList; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class DictionaryEntity : CustomKubernetesEntity + private sealed class DictionaryEntity : CustomKubernetesEntity { public IDictionary Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class EnumerableKeyPairsEntity : CustomKubernetesEntity + private sealed class EnumerableKeyPairsEntity : CustomKubernetesEntity { public IEnumerable> Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class IntstrOrStringEntity : CustomKubernetesEntity + private sealed class IntstrOrStringEntity : CustomKubernetesEntity { public IntOrString Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class EmbeddedResourceEntity : CustomKubernetesEntity + private sealed class EmbeddedResourceEntity : CustomKubernetesEntity { public V1Pod Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class EmbeddedCustomResourceEntity : CustomKubernetesEntity + private sealed class EmbeddedCustomResourceEntity : CustomKubernetesEntity { public EmbeddedCustomResource Property { get; set; } = null!; } @@ -813,7 +813,7 @@ private class EmbeddedCustomResource : CustomKubernetesEntity } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class EmbeddedCustomResourceGenericEntity : CustomKubernetesEntity + private sealed class EmbeddedCustomResourceGenericEntity : CustomKubernetesEntity { public EmbeddedCustomResourceGeneric Property { get; set; } = null!; } @@ -821,18 +821,18 @@ private class EmbeddedCustomResourceGenericEntity : CustomKubernetesEntity [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] private class EmbeddedCustomResourceGeneric : CustomKubernetesEntity { - public class EntitySpec; + public sealed class EntitySpec; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - private class EmbeddedResourceListEntity : CustomKubernetesEntity + private sealed class EmbeddedResourceListEntity : CustomKubernetesEntity { public IList Property { get; set; } = null!; } [Ignore] [KubernetesEntity] - private class IgnoredEntity : CustomKubernetesEntity; + private sealed class IgnoredEntity : CustomKubernetesEntity; public class NonEntity; @@ -962,21 +962,21 @@ public class MultipleOfAttrEntity : CustomKubernetesEntity } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public class PatternAttrEntity : CustomKubernetesEntity + public sealed class PatternAttrEntity : CustomKubernetesEntity { [Pattern(@"/\d*/")] public string Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public class RangeMinimumAttrEntity : CustomKubernetesEntity + public sealed class RangeMinimumAttrEntity : CustomKubernetesEntity { [RangeMinimum(15, true)] public string Property { get; set; } = null!; } [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public class RangeMaximumAttrEntity : CustomKubernetesEntity + public sealed class RangeMaximumAttrEntity : CustomKubernetesEntity { [RangeMaximum(15, true)] public string Property { get; set; } = null!;