From d1ba0aae721fe02678e59a922a36c7f7e3cd907d Mon Sep 17 00:00:00 2001 From: Ivan Josipovic <9521987+IvanJosipovic@users.noreply.github.com> Date: Thu, 27 Jul 2023 04:44:08 -0700 Subject: [PATCH] fix: yaml with merge (#1332) * fix: yaml with merge * chore: cleanup --- src/KubernetesClient.Models/KubernetesYaml.cs | 13 +- .../KubernetesYamlTests.cs | 194 ++++++++++++++++++ 2 files changed, 202 insertions(+), 5 deletions(-) diff --git a/src/KubernetesClient.Models/KubernetesYaml.cs b/src/KubernetesClient.Models/KubernetesYaml.cs index 0869df7a..ffcd5a44 100644 --- a/src/KubernetesClient.Models/KubernetesYaml.cs +++ b/src/KubernetesClient.Models/KubernetesYaml.cs @@ -15,7 +15,7 @@ namespace k8s /// public static class KubernetesYaml { - private static readonly DeserializerBuilder CommonDeserializerBuilder = + private static DeserializerBuilder CommonDeserializerBuilder => new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new IntOrStringYamlConverter()) @@ -23,6 +23,7 @@ public static class KubernetesYaml .WithTypeConverter(new ResourceQuantityYamlConverter()) .WithAttemptingUnquotedStringTypeDeserialization() .WithOverridesFromJsonPropertyAttributes(); + private static readonly IDeserializer StrictDeserializer = CommonDeserializerBuilder .WithDuplicateKeyChecking() @@ -154,7 +155,7 @@ public static List LoadAllFromString(string content, IDictionary mergedTypeMap[x.Key] = x.Value); var types = new List(); - var parser = new Parser(new StringReader(content)); + var parser = new MergingParser(new Parser(new StringReader(content))); parser.Consume(); while (parser.Accept(out _)) { @@ -162,7 +163,7 @@ public static List LoadAllFromString(string content, IDictionary(); var ix = 0; var results = new List(); @@ -205,12 +206,14 @@ public static string SaveToString(T value) public static TValue Deserialize(string yaml, bool strict = false) { - return GetDeserializer(strict).Deserialize(yaml); + using var reader = new StringReader(yaml); + return GetDeserializer(strict).Deserialize(new MergingParser(new Parser(reader))); } public static TValue Deserialize(Stream yaml, bool strict = false) { - return GetDeserializer(strict).Deserialize(new StreamReader(yaml)); + using var reader = new StreamReader(yaml); + return GetDeserializer(strict).Deserialize(new MergingParser(new Parser(reader))); } public static string SerializeAll(IEnumerable values) diff --git a/tests/KubernetesClient.Tests/KubernetesYamlTests.cs b/tests/KubernetesClient.Tests/KubernetesYamlTests.cs index e12acd7f..9cd30e72 100644 --- a/tests/KubernetesClient.Tests/KubernetesYamlTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesYamlTests.cs @@ -901,5 +901,199 @@ public void LoadAllFromStringWithTypeMapGenericCRD() Assert.Equal(12.0, Assert.IsType(v1AlphaFoo.Spec["float"]), 3); Assert.Equal(content.Replace("\r\n", "\n"), KubernetesYaml.SerializeAll(objs).Replace("\r\n", "\n")); } + + [Fact] + public void LoadFromStringCRDMerge() + { + var content = @"apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + eventing.knative.dev/source: ""true"" + duck.knative.dev/source: ""true"" + knative.dev/crd-install: ""true"" + app.kubernetes.io/version: ""1.10.1"" + app.kubernetes.io/name: knative-eventing + annotations: + # TODO add schemas and descriptions + registry.knative.dev/eventTypes: | + [ + { ""type"": ""dev.knative.sources.ping"" } + ] + name: pingsources.sources.knative.dev +spec: + group: sources.knative.dev + versions: + - &version + name: v1beta2 + served: true + storage: false + subresources: + status: {} + schema: + openAPIV3Schema: + type: object + description: 'PingSource describes an event source with a fixed payload produced on a specified cron schedule.' + properties: + spec: + type: object + description: 'PingSourceSpec defines the desired state of the PingSource (from the client).' + properties: + ceOverrides: + description: 'CloudEventOverrides defines overrides to control the output format and modifications of the event sent to the sink.' + type: object + properties: + extensions: + description: 'Extensions specify what attribute are added or overridden on the outbound event. Each `Extensions` key-value pair are set on the event as an attribute extension independently.' + type: object + additionalProperties: + type: string + x-kubernetes-preserve-unknown-fields: true + contentType: + description: 'ContentType is the media type of `data` or `dataBase64`. Default is empty.' + type: string + data: + description: 'Data is data used as the body of the event posted to the sink. Default is empty. Mutually exclusive with `dataBase64`.' + type: string + dataBase64: + description: ""DataBase64 is the base64-encoded string of the actual event's body posted to the sink. Default is empty. Mutually exclusive with `data`."" + type: string + schedule: + description: 'Schedule is the cron schedule. Defaults to `* * * * *`.' + type: string + sink: + description: 'Sink is a reference to an object that will resolve to a uri to use as the sink.' + type: object + properties: + ref: + description: 'Ref points to an Addressable.' + type: object + properties: + apiVersion: + description: 'API version of the referent.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ This is optional field, it gets defaulted to the object holding it if left out.' + type: string + uri: + description: 'URI can be an absolute URL(non-empty scheme and non-empty host) pointing to the target or a relative URI. Relative URIs will be resolved using the base URI retrieved from Ref.' + type: string + timezone: + description: 'Timezone modifies the actual time relative to the specified timezone. Defaults to the system time zone. More general information about time zones: https://www.iana.org/time-zones List of valid timezone values: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones' + type: string + status: + type: object + description: 'PingSourceStatus defines the observed state of PingSource (from the controller).' + properties: + annotations: + description: 'Annotations is additional Status fields for the Resource to save some additional State as well as convey more information to the user. This is roughly akin to Annotations on any k8s resource, just the reconciler conveying richer information outwards.' + type: object + x-kubernetes-preserve-unknown-fields: true + ceAttributes: + description: 'CloudEventAttributes are the specific attributes that the Source uses as part of its CloudEvents.' + type: array + items: + type: object + properties: + source: + description: 'Source is the CloudEvents source attribute.' + type: string + type: + description: 'Type refers to the CloudEvent type attribute.' + type: string + conditions: + description: 'Conditions the latest available observations of a resource''s current state.' + type: array + items: + type: object + required: + - type + - status + properties: + lastTransitionTime: + description: 'LastTransitionTime is the last time the condition transitioned from one status to another. We use VolatileTime in place of metav1.Time to exclude this from creating equality.Semantic differences (all other things held constant).' + type: string + message: + description: 'A human readable message indicating details about the transition.' + type: string + reason: + description: 'The reason for the condition''s last transition.' + type: string + severity: + description: 'Severity with which to treat failures of this type of condition. When this is not specified, it defaults to Error.' + type: string + status: + description: 'Status of the condition, one of True, False, Unknown.' + type: string + type: + description: 'Type of condition.' + type: string + observedGeneration: + description: 'ObservedGeneration is the ""Generation"" of the Service that was last processed by the controller.' + type: integer + format: int64 + sinkUri: + description: 'SinkURI is the current active sink URI that has been configured for the Source.' + type: string + additionalPrinterColumns: + - name: Sink + type: string + jsonPath: .status.sinkUri + - name: Schedule + type: string + jsonPath: .spec.schedule + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + - name: Ready + type: string + jsonPath: "".status.conditions[?(@.type=='Ready')].status"" + - name: Reason + type: string + jsonPath: "".status.conditions[?(@.type=='Ready')].reason"" + - !!merge <<: *version + name: v1 + served: true + storage: true + # v1 schema is identical to the v1beta2 schema + names: + categories: + - all + - knative + - sources + kind: PingSource + plural: pingsources + singular: pingsource + scope: Namespaced + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: [""v1"", ""v1beta1""] + clientConfig: + service: + name: eventing-webhook + namespace: knative-eventing +"; + + var obj = KubernetesYaml.Deserialize(content); + + Assert.Equal("pingsources.sources.knative.dev", obj.Metadata.Name); + Assert.Equal("v1beta2", obj.Spec.Versions[0].Name); + Assert.Equal("v1", obj.Spec.Versions[1].Name); + + var obj2 = KubernetesYaml.LoadAllFromString(content); + + var crd = obj2[0] as V1CustomResourceDefinition; + + Assert.Equal("pingsources.sources.knative.dev", crd.Metadata.Name); + Assert.Equal("v1beta2", crd.Spec.Versions[0].Name); + Assert.Equal("v1", crd.Spec.Versions[1].Name); + } } }