diff --git a/src/KubernetesClient/Yaml.cs b/src/KubernetesClient/Yaml.cs index 4aa9b63..4702306 100644 --- a/src/KubernetesClient/Yaml.cs +++ b/src/KubernetesClient/Yaml.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; using YamlDotNet.Core; @@ -18,6 +19,26 @@ namespace k8s /// public static class Yaml { + private static readonly IDeserializer Deserializer = + new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new IntOrStringYamlConverter()) + .WithTypeConverter(new ByteArrayStringYamlConverter()) + .WithOverridesFromJsonPropertyAttributes() + .IgnoreUnmatchedProperties() + .Build(); + + private static readonly IValueSerializer Serializer = + new SerializerBuilder() + .DisableAliases() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new IntOrStringYamlConverter()) + .WithTypeConverter(new ByteArrayStringYamlConverter()) + .WithEventEmitter(e => new StringQuotingEmitter(e)) + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) + .WithOverridesFromJsonPropertyAttributes() + .BuildValueSerializer(); + public class ByteArrayStringYamlConverter : IYamlTypeConverter { public bool Accepts(Type type) @@ -105,30 +126,15 @@ namespace k8s throw new ArgumentNullException(nameof(typeMap)); } - var deserializer = - new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeInspector(ti => new AutoRestTypeInspector(ti)) - .WithTypeConverter(new IntOrStringYamlConverter()) - .WithTypeConverter(new ByteArrayStringYamlConverter()) - .IgnoreUnmatchedProperties() - .Build(); var types = new List(); var parser = new Parser(new StringReader(content)); parser.Consume(); while (parser.Accept(out _)) { - var obj = deserializer.Deserialize(parser); + var obj = Deserializer.Deserialize(parser); types.Add(typeMap[obj.ApiVersion + "/" + obj.Kind]); } - deserializer = - new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeInspector(ti => new AutoRestTypeInspector(ti)) - .WithTypeConverter(new IntOrStringYamlConverter()) - .WithTypeConverter(new ByteArrayStringYamlConverter()) - .Build(); parser = new Parser(new StringReader(content)); parser.Consume(); var ix = 0; @@ -136,7 +142,7 @@ namespace k8s while (parser.Accept(out _)) { var objType = types[ix++]; - var obj = deserializer.Deserialize(parser, objType); + var obj = Deserializer.Deserialize(parser, objType); results.Add(obj); } @@ -160,14 +166,7 @@ namespace k8s public static T LoadFromString(string content) { - var deserializer = - new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeInspector(ti => new AutoRestTypeInspector(ti)) - .WithTypeConverter(new IntOrStringYamlConverter()) - .WithTypeConverter(new ByteArrayStringYamlConverter()) - .Build(); - var obj = deserializer.Deserialize(content); + var obj = Deserializer.Deserialize(content); return obj; } @@ -177,119 +176,44 @@ namespace k8s var writer = new StringWriter(stringBuilder); var emitter = new Emitter(writer); - var serializer = - new SerializerBuilder() - .DisableAliases() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeInspector(ti => new AutoRestTypeInspector(ti)) - .WithTypeConverter(new IntOrStringYamlConverter()) - .WithTypeConverter(new ByteArrayStringYamlConverter()) - .WithEventEmitter(e => new StringQuotingEmitter(e)) - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) - .BuildValueSerializer(); emitter.Emit(new StreamStart()); emitter.Emit(new DocumentStart()); - serializer.SerializeValue(emitter, value, typeof(T)); + Serializer.SerializeValue(emitter, value, typeof(T)); return stringBuilder.ToString(); } - private class AutoRestTypeInspector : ITypeInspector + private static TBuilder WithOverridesFromJsonPropertyAttributes(this TBuilder builder) + where TBuilder : BuilderSkeleton { - private readonly ITypeInspector _inner; + // Use VersionInfo from the model namespace as that should be stable. + // If this is not generated in the future we will get an obvious compiler error. + var targetNamespace = typeof(VersionInfo).Namespace; - public AutoRestTypeInspector(ITypeInspector inner) - { - _inner = inner; - } + // Get all the concrete model types from the code generated namespace. + var types = typeof(KubernetesEntityAttribute).Assembly + .ExportedTypes + .Where(type => type.Namespace == targetNamespace && + !type.IsInterface && + !type.IsAbstract); - public IEnumerable GetProperties(Type type, object container) + // Map any JsonPropertyAttribute instances to YamlMemberAttribute instances. + foreach (var type in types) { - var pds = _inner.GetProperties(type, container); - return pds.Select(pd => TrimPropertySuffix(pd, type)).ToList(); - } - - public IPropertyDescriptor GetProperty(Type type, object container, string name, bool ignoreUnmatched) - { - try + foreach (var property in type.GetProperties()) { - return _inner.GetProperty(type, container, name, ignoreUnmatched); - } - catch (System.Runtime.Serialization.SerializationException) - { - return _inner.GetProperty(type, container, name + "Property", ignoreUnmatched); + var jsonAttribute = property.GetCustomAttribute(); + if (jsonAttribute == null) + { + continue; + } + + var yamlAttribute = new YamlMemberAttribute { Alias = jsonAttribute.PropertyName }; + builder.WithAttributeOverride(type, property.Name, yamlAttribute); } } - private IPropertyDescriptor TrimPropertySuffix(IPropertyDescriptor pd, Type type) - { - if (!pd.Name.EndsWith("Property", StringComparison.InvariantCulture)) - { - return pd; - } - - // This might have been renamed by AutoRest. See if there is a - // JsonPropertyAttribute.PropertyName and use that instead if there is. - var jpa = pd.GetCustomAttribute(); - if (jpa == null || string.IsNullOrEmpty(jpa.PropertyName)) - { - return pd; - } - - return new RenamedPropertyDescriptor(pd, jpa.PropertyName); - } - - private class RenamedPropertyDescriptor : IPropertyDescriptor - { - private readonly IPropertyDescriptor _inner; - private readonly string _name; - - public RenamedPropertyDescriptor(IPropertyDescriptor inner, string name) - { - _inner = inner; - _name = name; - } - - public string Name => _name; - - public bool CanWrite => _inner.CanWrite; - - public Type Type => _inner.Type; - - public Type TypeOverride - { - get => _inner.TypeOverride; - set => _inner.TypeOverride = value; - } - - public int Order - { - get => _inner.Order; - set => _inner.Order = value; - } - - public ScalarStyle ScalarStyle - { - get => _inner.ScalarStyle; - set => _inner.ScalarStyle = value; - } - - public T GetCustomAttribute() - where T : Attribute - { - return _inner.GetCustomAttribute(); - } - - public IObjectDescriptor Read(object target) - { - return _inner.Read(target); - } - - public void Write(object target, object value) - { - _inner.Write(target, value); - } - } + return builder; } } } diff --git a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs index 0583f10..26c6faf 100644 --- a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs @@ -503,6 +503,19 @@ namespace k8s.Tests AssertConfigEqual(expectedCfg, cfg); } + [Fact] + public void LoadKubeConfigWithAdditionalProperties() + { + var txt = File.ReadAllText("assets/kubeconfig.additional-properties.yml"); + var expectedCfg = Yaml.LoadFromString(txt); + + var fileInfo = new FileInfo(Path.GetFullPath("assets/kubeconfig.additional-properties.yml")); + + var cfg = KubernetesClientConfiguration.LoadKubeConfig(new FileInfo[] { fileInfo, fileInfo }); + + AssertConfigEqual(expectedCfg, cfg); + } + [Fact] public void MergeKubeConfigNoDuplicates() { diff --git a/tests/KubernetesClient.Tests/YamlTests.cs b/tests/KubernetesClient.Tests/YamlTests.cs index be4a34b..7fc37fc 100644 --- a/tests/KubernetesClient.Tests/YamlTests.cs +++ b/tests/KubernetesClient.Tests/YamlTests.cs @@ -35,6 +35,35 @@ metadata: Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name); } + [Fact] + public void LoadAllFromStringWithAdditionalProperties() + { + var content = @"apiVersion: v1 +kind: Pod +metadata: + name: foo + namespace: ns + youDontKnow: Me +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ns + youDontKnow: Me"; + + var types = new Dictionary(); + types.Add("v1/Pod", typeof(V1Pod)); + types.Add("v1/Namespace", typeof(V1Namespace)); + + var objs = Yaml.LoadAllFromString(content, types); + Assert.Equal(2, objs.Count); + Assert.IsType(objs[0]); + Assert.IsType(objs[1]); + Assert.Equal("foo", ((V1Pod)objs[0]).Metadata.Name); + Assert.Equal("ns", ((V1Pod)objs[0]).Metadata.NamespaceProperty); + Assert.Equal("ns", ((V1Namespace)objs[1]).Metadata.Name); + } + [Fact] public async Task LoadAllFromFile() { @@ -87,6 +116,21 @@ metadata: Assert.Equal("foo", obj.Metadata.Name); } + [Fact] + public void LoadFromStringWithAdditionalProperties() + { + var content = @"apiVersion: v1 +kind: Pod +metadata: + name: foo + youDontKnow: Me +"; + + var obj = Yaml.LoadFromString(content); + + Assert.Equal("foo", obj.Metadata.Name); + } + [Fact] public void LoadNamespacedFromString() { @@ -172,6 +216,18 @@ metadata: } } + [Fact] + public void RoundtripTypeWithMismatchedPropertyName() + { + var content = @"namespace: foo"; + + var deserialized = Yaml.LoadFromString(content); + Assert.Equal("foo", deserialized.NamespaceProperty); + + var serialized = Yaml.SaveToString(deserialized); + Assert.Equal(content, serialized); + } + [Fact] public void WriteToString() { diff --git a/tests/KubernetesClient.Tests/assets/kubeconfig.additional-properties.yml b/tests/KubernetesClient.Tests/assets/kubeconfig.additional-properties.yml new file mode 100644 index 0000000..8222c65 --- /dev/null +++ b/tests/KubernetesClient.Tests/assets/kubeconfig.additional-properties.yml @@ -0,0 +1,64 @@ +# Sample file based on https://kubernetes.io/docs/tasks/access-application-cluster/authenticate-across-clusters-kubeconfig/ +# WARNING: File includes minor fixes +--- +current-context: federal-context +apiVersion: v1 +additionalProperty: foobar +clusters: +- cluster: + server: http://cow.org:8080 + name: cow-cluster + farm: old-mac-donalds +- cluster: + certificate-authority: assets/ca.crt + server: https://horse.org:4443 + name: horse-cluster +- cluster: + insecure-skip-tls-verify: true + server: https://pig.org:443 + name: pig-cluster +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURERENDQWZTZ0F3SUJBZ0lSQUo5ZCtLeThkTDJVSzRjdXplMmo2WnN3RFFZSktvWklodmNOQVFFTEJRQXcKTHpFdE1Dc0dBMVVFQXhNa1lXRTBZVFV3T0RZdE0yVm1aaTAwWWpCa0xUbGxORGt0WmpNeVpXWXpabUpqWWpNNApNQjRYRFRFM01ESXlOakExTURRek5Gb1hEVEl5TURJeU5UQTFNRFF6TkZvd0x6RXRNQ3NHQTFVRUF4TWtZV0UwCllUVXdPRFl0TTJWbVppMDBZakJrTFRsbE5Ea3Raak15WldZelptSmpZak00TUlJQklqQU5CZ2txaGtpRzl3MEIKQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2dkandhdHNsdCsvQVpqV3hmbkNQeGZqMzNHUUxlOU00VU42VmEwRQpKd0FYL2R3L1ZVa0dvVjlDc3NKRUZMdEdTUnM2K2h0RTEvOUN3ak1USDh2WExKcURHTE9KdFQ5dW9sR2c2Q2k1ClBKNDNKelVLWmJlYVE4Z3hhZndzQjdQU05vTTJOYzROVm9lZzBVTUw0bndGeEhXeTNYWHlFZ0QxTWxTUnVrb3oKTTNoRUVxUjJNVFdrNm9KK3VJNFF4WVZWMnZuWXdXaEJwUDlDR3RWUTlyUW9MVFowcmFpOCtDYURBMVltTWRhbQpRYUVPdURlSFRqU2FYM2dyR0FBVVFWNWl6MC9qVVBuK3lJNm1iV0trbzFzNytPY1dZR2F1aDFaMzFYSjJsc0RTCnU4a3F0d215UEcyUVl2aUQ4YjNOWFAyY0dRK2EwZlpRZnBrbTF0U3IxQnhhaXdJREFRQUJveU13SVRBT0JnTlYKSFE4QkFmOEVCQU1DQWdRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQQpuVzFXVXlLbVJ0TlNzU1VzVFBSUnhFRzhhek9kdjdYeUhRL0R5VWNqWm9rUEJVVHY4VjdvNG96RHgyVHV6UEdYCmZ2YlMvT2g0VDd6ZlYxdjJadmU3dTBxelNiRTl5OGpsaDNxYXJEcEd5ZmlTamwycmhIOFBmay9sZGR0VFpVL04KSkVtYW5ReGl6R20xV2pCSklRSE5LZENneVIwN3A1c0MwNnR3K25YUytla1MxMlBUTG45WjBuRDBKVDdQSzRXQgpQc3ZXeDVXN0w5dnJIdVN5SGRSTkt5eEEvbWI1WHdXMDBkZUpmaHZub0p3ZWRYNDVKZVRiME5MczUzaURqVEU1CnRpdU03Z1RVSjlCcGZTL0gvYSt2SmovVWQ2bHM0QndrWmpUNHNhOTA1bnNzdnRqamlwZ1N5a0QzVkxCQ3VueTkKd1NnbE1vSnZNWmg0bC9FVFJPeFE3Zz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://llama.org:443 + name: llama-cluster +contexts: +- context: + cluster: horse-cluster + namespace: chisel-ns + user: green-user + name: federal-context +- context: + cluster: pig-cluster + namespace: saw-ns + user: black-user + name: queen-anne-context +- context: + cluster: llama-cluster + namespace: saw-ns + user: red-user + name: victorian-context +- context: + cluster: llama-cluster + namespace: saw-ns + user: elliptic-user + name: elliptic-context +kind: Config +users: +- name: blue-user + user: + token: blue-token +- name: green-user + user: + client-certificate: assets/client.crt + client-key: assets/client.key +- name: black-user + user: + token: black-token +- name: red-user + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURrVENDQW5tZ0F3SUJBZ0lVTzlVTkdpbHhmSHpMbVJXcWU2dVMyRVZ1NVhVd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1FURUxNQWtHQTFVRUJoTUNWVk14RURBT0JnTlZCQWdUQjFKbFpHMXZibVF4Q3pBSkJnTlZCQWNUQWxkQgpNUk13RVFZRFZRUURFd3ByZFdKbGNtNWxkR1Z6TUNBWERURTRNVEl3T0RBNU5URXdNRm9ZRHpJeE1UZ3hNVEUwCk1EazFNVEF3V2pCQk1Rc3dDUVlEVlFRR0V3SlZVekVRTUE0R0ExVUVDQk1IVW1Wa2JXOXVaREVMTUFrR0ExVUUKQnhNQ1YwRXhFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQgpEd0F3Z2dFS0FvSUJBUUM4WkVRS296SE8zOExmRzRpcHFYWnRLTzNxSENjL1Z1WjUxWnZSeWJuTnpCYU5iMk9oClM0cU1nNDF6cFdzL3dMQnk4ZndPclpoOHpHb28wbHllQXVCSlBPWFc5SmswMmhOc1Y3ZHBqYXBZWFMrSXJvN04KcUxhWDB5amQxWWllSWFlb3NtV2xib2ZpcDVzS3dzVUI3bGREeXJpQklBYTEwNlhhTng5ZG82UEh1TDNibStldwpiaWRoRTlNUFRHY2V5WW9rZ3pNbGppanordk0yUnN5Z05ncmR4VDhROC9GRER2ZndTRmZUaEZlYXIzckQvRkJGCmVYb0Y5MHp6cG1aZlphTDlyZ2NjQ0pzQ2JSZlpiellVVERRVHo2dStaUVhUdGlrbVMvQ3d2d0hXa0w3bGhQNXYKQU0yVFpETEJhQXQwOU14dGlORFNWaFFTWVR2QWpKRmU4SzJoQWdNQkFBR2pmekI5TUE0R0ExVWREd0VCL3dRRQpBd0lGb0RBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFRWUlLd1lCQlFVSEF3SXdEQVlEVlIwVEFRSC9CQUl3CkFEQWRCZ05WSFE0RUZnUVVuQmo0T3Y3MnFlWWJ5YjVYSHJma1pkazd6VUF3SHdZRFZSMGpCQmd3Rm9BVUxwemQKRlplYkR6N0RmZUNkZkJqNmNkUWJmNk13RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQU1CL1RDaElEbTNkeDA4SApJeXlFS2dYUHh1d3A1cTB5QkFoT0pUS1JOVXNpayt6ZVUyUGpibnV0M3B1WnBENkZoTGFKQUZPbTdsMHhkNU1ZCkFZOVloSy9wa1U3Rmw1TFg0MitxQzRqK05JdGhBbFM5clNtNkRvNlN1b3JRcjdNajErY0syTjFkZHBtRWZya3kKZXZnMm02aFB5YTFlV0hNanVCd250bkJmV2EzWHBWMWVGZVBvd21zY3R6UmRJYUh3WDM3Yit3c2JmbkppczRvTgo2bHk0THBQb2xtYld3ZTRnaEtPWXNuL3lMT3FrUGJMZVpLeU5yaWJpSGhSQTM0NUVHUGJleXNmSlNIWWFqMnBaCmE0VmRTRFhnT2JlM3Vtdk9keXVEaGl5RFJ3TFozbVJLWlpIR0lzZC9ORzRncWo3a0gzV2N2Wm5mSkVHMXY2cG0KQTZFYTJiRT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdkdSRUNxTXh6dC9DM3h1SXFhbDJiU2p0Nmh3blAxYm1lZFdiMGNtNXpjd1dqVzlqCm9VdUtqSU9OYzZWclA4Q3djdkg4RHEyWWZNeHFLTkpjbmdMZ1NUemwxdlNaTk5vVGJGZTNhWTJxV0YwdmlLNk8KemFpMmw5TW8zZFdJbmlHbnFMSmxwVzZINHFlYkNzTEZBZTVYUThxNGdTQUd0ZE9sMmpjZlhhT2p4N2k5MjV2bgpzRzRuWVJQVEQweG5Ic21LSklNekpZNG84L3J6TmtiTW9EWUszY1UvRVBQeFF3NzM4RWhYMDRSWG1xOTZ3L3hRClJYbDZCZmRNODZabVgyV2kvYTRISEFpYkFtMFgyVzgyRkV3MEU4K3J2bVVGMDdZcEprdndzTDhCMXBDKzVZVCsKYndETmsyUXl3V2dMZFBUTWJZalEwbFlVRW1FN3dJeVJYdkN0b1FJREFRQUJBb0lCQUNCYk9EUjdndnA5QkFNOQp2Mk1rYitxZnRQMFlpTVVnTDhXTklvNE5qNVFCRVg2Sk94dGcxaEw4SlRkUG1mUUJMRTBSc3JEeXI5WC9aZHhOCkJRcytnemNROW9qTXllT0I4UVFTckxXOFZ4MkdJN3ZkL3pqaldUa0tVMktHWWtpR2p6MHlKck1iSU11VTdkUVQKVDdMZE5LKzRDYWhqejhNNjdxbGova2NlNitwSlRLdHZKR2tNSDRYUXJyMFRmU3hLMmEwUUNmNkZwSXp2OFFEMApISE9HbFJaWlBYK1dXYVA2UFpFU3JSZG1vbmFKaTJlRG03b1Y1WU1DMExWTHJzMVprZS9FT01CODZYOVlraGJUCmJsRkxRZjlNR1hTdWtGeW1MSnFSd01DUjJxZytKSXpEU1BuYzkzbjNwcHl4Nys3UkhFeXZNMFVNOVhyeHQrNHcKUkd3ZVZlRUNnWUVBeElkaFRCdW4rRk52YkVhSHZUdlpzNkVCa1lJUXUyV0NDR3dWN1lPbTZzc0hzczJBaldrVgprQ0pMajhZMTFpMHd0Y21yOG9DcE1EN0FDRWxMSWFEK3RubW5CTzlkQ2NnTzJoNXFCSkY1UytJeFlGcVZ5ZVBsCnptQkE2VzFvdzB2Y1hsYmkrVUg4SWF3WnNLaFlMaklscmdmQ3YwaU9LNzBYaEF3YzZ6Vlk4TjhDZ1lFQTlXYUUKMklkNjJEaGxLWVRaNW5yY3g1aVN3WU1jaitMTE5FYTFZMlZFNEhZZktvbjFkRnNSa2NKV1YyRVpoRGlqcVIyaQpXZEdEVldiMVVaVEpiM3BZWW1NakJFQXhFbEJXTSt0SzhialRqMWhxbXJPQWhLU0ZYUjg4RkNEanVkcStSbmo4CklyZFlWanNCdDA5RkVkT2RqdVFseGgyRVdQWDNPdVFhRXd5WnNYOENnWUJzekNtZ0VadHVqUG9kTGZxTlZ5blIKR0t3ZW1xdWFvcnBXNFVkT1l0aXdHTS9kTzRrVVAvMlErbnRzVDZXVU9SWkRQUzgwbytlRjd1Y3VieXpwcEEvKwpndUJraWdLdW5KTWtTendUNVZrS0dtR05YdmlYZU5QSzZWeG1IWXltdVVONDhvN2F3SjNOSWxKaWl2K3VLMUxTCndqY2M0QlRjditUWjFEN2FNNEZXYndLQmdHY1MyNE96VEJiYmdTb3lRZS83OVJYazhPZFU4YjlCN0VZVjJRUloKdWRkcDVlZFJNUWJoWlh6S21zZHk0bXZWK25BRElYa0dkbHA5dDFhLzN1ZnpCSUsyenpOdTN1MnBUcnZaL1kyUQpLMVJQTjkrb3U3ZDYvd1ZCSkZQMENKSzgzU1R1bGtEaXI3andhZVViNTQvNFNYcUdPNU4rUEdPOVZFMnBGNGFlCnlVTnpBb0dCQUptMU1vcFQ4N0llelBmQ010ck51cXVqQjFpdStvb1lIYm9IbGJXZUNxS3RSRnNYMWhXOWpTY1UKQ0ZRanhSVCtPcENVWndjbDdkY2xsTUlxQTRWQ1NmUUFRMW9CMU41WENRWE8xVFlXOHU1K1BRTUdUcFEvQlVEawp6WmRJZzZqQkFXd3R6YkNzKzRjVkFoclN3cDRBSXFvcndTZU1qRmdsTmNoNzgxMEF6emNJCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== +- name: elliptic-user + user: + client-certificate: assets/client.crt + client-key: assets/elliptic-client.key