Skip to content

Commit

Permalink
fix: yaml with merge (#1332)
Browse files Browse the repository at this point in the history
* fix: yaml with merge

* chore: cleanup
  • Loading branch information
IvanJosipovic committed Jul 27, 2023
1 parent 66ad77d commit d1ba0aa
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 5 deletions.
13 changes: 8 additions & 5 deletions src/KubernetesClient.Models/KubernetesYaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ namespace k8s
/// </summary>
public static class KubernetesYaml
{
private static readonly DeserializerBuilder CommonDeserializerBuilder =
private static DeserializerBuilder CommonDeserializerBuilder =>
new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeConverter(new IntOrStringYamlConverter())
.WithTypeConverter(new ByteArrayStringYamlConverter())
.WithTypeConverter(new ResourceQuantityYamlConverter())
.WithAttemptingUnquotedStringTypeDeserialization()
.WithOverridesFromJsonPropertyAttributes();

private static readonly IDeserializer StrictDeserializer =
CommonDeserializerBuilder
.WithDuplicateKeyChecking()
Expand Down Expand Up @@ -154,15 +155,15 @@ public static List<object> LoadAllFromString(string content, IDictionary<string,
typeMap?.ToList().ForEach(x => mergedTypeMap[x.Key] = x.Value);

var types = new List<Type>();
var parser = new Parser(new StringReader(content));
var parser = new MergingParser(new Parser(new StringReader(content)));
parser.Consume<StreamStart>();
while (parser.Accept<DocumentStart>(out _))
{
var dict = GetDeserializer(strict).Deserialize<Dictionary<object, object>>(parser);
types.Add(mergedTypeMap[dict["apiVersion"] + "/" + dict["kind"]]);
}

parser = new Parser(new StringReader(content));
parser = new MergingParser(new Parser(new StringReader(content)));
parser.Consume<StreamStart>();
var ix = 0;
var results = new List<object>();
Expand Down Expand Up @@ -205,12 +206,14 @@ public static string SaveToString<T>(T value)

public static TValue Deserialize<TValue>(string yaml, bool strict = false)
{
return GetDeserializer(strict).Deserialize<TValue>(yaml);
using var reader = new StringReader(yaml);
return GetDeserializer(strict).Deserialize<TValue>(new MergingParser(new Parser(reader)));
}

public static TValue Deserialize<TValue>(Stream yaml, bool strict = false)
{
return GetDeserializer(strict).Deserialize<TValue>(new StreamReader(yaml));
using var reader = new StreamReader(yaml);
return GetDeserializer(strict).Deserialize<TValue>(new MergingParser(new Parser(reader)));
}

public static string SerializeAll(IEnumerable<object> values)
Expand Down
194 changes: 194 additions & 0 deletions tests/KubernetesClient.Tests/KubernetesYamlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -901,5 +901,199 @@ public void LoadAllFromStringWithTypeMapGenericCRD()
Assert.Equal(12.0, Assert.IsType<float>(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<V1CustomResourceDefinition>(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);
}
}
}

0 comments on commit d1ba0aa

Please sign in to comment.