src/ConvertFromKubeYamlCmdlet.cs
using System.Threading.Tasks; using System.IO; using System.Management.Automation; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using System; using Microsoft.Extensions.Logging; using System.Threading; using YamlDotNet.Core.Events; using YamlDotNet.Core; using KubeClient.Models; using System.Collections.Generic; using System.Linq; using System.Collections; using System.Reflection; namespace Kubectl { [Cmdlet(VerbsData.ConvertFrom, "KubeYaml")] [OutputType(new[] { typeof(KubeResourceV1) })] public class ConvertFromKubeYamlCmdlet : KubeCmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)] public string InputObject { get; set; } protected override async Task ProcessRecordAsync(CancellationToken cancellationToken) { await base.ProcessRecordAsync(cancellationToken); var deserializer = new DeserializerBuilder() .WithNamingConvention(new CamelCaseNamingConvention()) .IgnoreUnmatchedProperties() .Build(); // Deserialize to Dictionary first to check the kind field to determine the type Dictionary<string, object> dict = deserializer.Deserialize<Dictionary<string, object>>(InputObject); string kind = (string)dict["kind"]; string apiGroupVersion = (string)dict["apiVersion"]; string apiVersion = apiGroupVersion.Split('/').Last(); WriteVerbose($"apiVersion {apiVersion}"); Type type = modelTypes.GetValueOrDefault((kind, apiVersion)); if (type == null) { WriteError(new ErrorRecord(new Exception($"Unknown (kind: {kind}, apiVersion: {apiVersion}). {modelTypes.Count} Known:\n{String.Join("\n", modelTypes.Keys)}"), null, ErrorCategory.InvalidData, InputObject)); return; } var resource = toPSObject(dict, type); WriteObject(resource); } /// <summary> /// Converts a structure of Dictionaries and Lists recursively to PSObjects with type names, /// Dictionaries and Lists depending on the given type /// </summary> private object toPSObject(object value, Type type) { Logger.LogTrace($"Type {type.Name}"); if (type == null) { throw new ArgumentNullException(nameof(type)); } if (value == null) { return value; } if (type == typeof(string) || type.IsValueType) { Logger.LogTrace("Is scalar"); if (value.GetType() != typeof(string) && !value.GetType().IsValueType) { throw new Exception($"Invalid type: Expected {type.Name}, got {value.GetType().Name}"); } // Convert.ChangeType() can't cast to Nullable, need to unwrap if (type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) { type = Nullable.GetUnderlyingType(type); } // YAML is often ambiguous with scalar types, // e.g. HTTPGetActionV1.Port is officially string but often specified as int // Be tolerant here by trying to cast return Convert.ChangeType(value, type); } if (type.IsGenericType) { Logger.LogTrace("Is generic"); if (type.GetGenericTypeDefinition() == typeof(List<>)) { Logger.LogTrace("Is list"); var valueType = type.GetGenericArguments()[0]; return ((IList)value).Cast<object>().Select(element => toPSObject(element, valueType)).ToList(); } if (type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { Logger.LogTrace("Is map"); var valueType = type.GetGenericArguments()[1]; // Shadowed type is map too var dict = new Dictionary<string, object>(); foreach (DictionaryEntry entry in ((IDictionary)value)) { dict.Add((string)entry.Key, toPSObject(entry.Value, valueType)); } return dict; } } Logger.LogTrace("Is other object"); // Shadowed type is a kube model object, only copy the properties set in the map for diffing purposes var psObject = new PSObject(); foreach (DictionaryEntry entry in ((IDictionary)value)) { var key = (string)entry.Key; var prop = ModelHelpers.FindJsonProperty(type, key); if (prop == null) { throw new Exception($"Unknown property {key} on type {type.Name}"); } Logger.LogTrace($"Property {prop.Name}"); psObject.Properties.Add(new PSNoteProperty(prop.Name, toPSObject(entry.Value, prop.PropertyType))); } // Add type name for output formatting psObject.TypeNames.Insert(0, type.FullName); return psObject; } } } |