diff --git a/PSKubectl/Formats/JsonPatch.Operations.Operation.Format.ps1xml b/PSKubectl/Formats/JsonPatch.Operations.Operation.Format.ps1xml deleted file mode 100644 index 8f531db..0000000 --- a/PSKubectl/Formats/JsonPatch.Operations.Operation.Format.ps1xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - Default - - Microsoft.AspNetCore.JsonPatch.Operations.Operation - - - - - - - - - - - - - - - - - - - - - - $color = switch ($_.OperationType) { - "Remove" { 31 } - "Add" { 32 } - "Replace" { 33 } - "Copy" { 34 } - "Move" { 35 } - "Test" { 0 } - } - "$([char]0x001b)[$($color)m" + $_.OperationType - - - - Path - - - Value - - - $_.From + "$([char]0x001b)[0m" - - - - - - - - diff --git a/PSKubectl/PSKubectl.psd1 b/PSKubectl/PSKubectl.psd1 index 4dad413..f0acf5d 100644 --- a/PSKubectl/PSKubectl.psd1 +++ b/PSKubectl/PSKubectl.psd1 @@ -77,7 +77,6 @@ FormatsToProcess = @( 'Formats/Config.Format.ps1xml', 'Formats/DeploymentV1.Format.ps1xml', - 'Formats/JsonPatch.Operations.Operation.Format.ps1xml', 'Formats/NamespaceV1.Format.ps1xml', 'Formats/PodV1.Format.ps1xml' ) @@ -90,7 +89,6 @@ # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( - 'Compare-KubeResource', 'ConvertFrom-KubeYaml', 'ConvertTo-KubeYaml', 'Get-KubeConfig', diff --git a/Tests/PSKubectl.Tests.ps1 b/Tests/PSKubectl.Tests.ps1 index a6de913..22b8ec3 100644 --- a/Tests/PSKubectl.Tests.ps1 +++ b/Tests/PSKubectl.Tests.ps1 @@ -299,7 +299,7 @@ Describe Get-KubeConfig { } } -Describe Convert-KubeYaml { +Describe ConvertFrom-KubeYaml { It 'Should read in YAML' { $parsed = Get-Content -Raw $PSScriptRoot/test.Deployment.yml | ConvertFrom-KubeYaml $parsed.PSObject.TypeNames | Should -Contain 'KubeClient.Models.DeploymentV1' diff --git a/src/Annotations.cs b/src/Annotations.cs deleted file mode 100644 index 35d15a2..0000000 --- a/src/Annotations.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using KubeClient; -using KubeClient.Models; -using KubeClient.ResourceClients; -using Microsoft.AspNetCore.JsonPatch; -using Microsoft.AspNetCore.JsonPatch.Operations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; - -namespace Kubectl { - public static class Annotations { - public const string LastAppliedConfig = "kubectl.kubernetes.io/last-applied-configuration"; - } -} diff --git a/src/Cmdlets/CompareKubeResourceCmdlet.cs b/src/Cmdlets/CompareKubeResourceCmdlet.cs deleted file mode 100644 index f401c01..0000000 --- a/src/Cmdlets/CompareKubeResourceCmdlet.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using KubeClient; -using KubeClient.Models; -using KubeClient.ResourceClients; -using Microsoft.AspNetCore.JsonPatch; -using Microsoft.AspNetCore.JsonPatch.Operations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; - -namespace Kubectl.Cmdlets { - [Cmdlet(VerbsData.Compare, "KubeResource")] - [OutputType(new[] { typeof(Operation) })] - public sealed class CompareKubeResourceCmdlet : KubeCmdlet { - [Parameter(Mandatory = true, Position = 0)] - public dynamic Original { get; set; } - - [Parameter(Mandatory = true, Position = 1)] - - public dynamic Modified { get; set; } - - [Parameter(Mandatory = true, Position = 2, ParameterSetName = "ThreeWay")] - public dynamic Current { get; set; } - - [Parameter(Mandatory = true, ParameterSetName = "ThreeWayFromLastApplied")] - public SwitchParameter ThreeWayFromLastApplied { get; set; } - - [Parameter(ParameterSetName = "ThreeWayFromLastApplied")] - public SwitchParameter Annotate { get; set; } - - [Parameter(ParameterSetName = "TwoWay")] - public SwitchParameter IgnoreDeletions { get; set; } - - [Parameter(ParameterSetName = "TwoWay")] - public SwitchParameter IgnoreAdditionsAndModifications { get; set; } - - protected override async Task ProcessRecordAsync(CancellationToken cancellationToken) { - await base.ProcessRecordAsync(cancellationToken); - // TODO do not pass the ContractResolver here once KubeClient allows customizing the serialisation - var patch = new JsonPatchDocument(new List(), new PSObjectAwareContractResolver()); - var comparer = new KubeResourceComparer(LoggerFactory); - string apiGroupVersion = (string)Original.ApiVersion; - string apiVersion = apiGroupVersion.Split('/').Last(); - string kind = (string)Original.Kind; - if (!ModelTypes.TryGetValue((kind, apiVersion), out Type type)) { - WriteError(new ErrorRecord(new Exception($"Unknown (kind: {kind}, apiVersion: {apiVersion}). {ModelTypes.Count} Known:\n{String.Join("\n", ModelTypes.Keys)}"), null, ErrorCategory.InvalidData, null)); - return; - } - if (ThreeWayFromLastApplied) { - comparer.CreateThreeWayPatchFromLastApplied( - current: Original, - modified: Modified, - type: type, - patch: patch, - annotate: Annotate - ); - } else if (Current == null) { - comparer.CreateTwoWayPatch( - original: Original, - modified: Modified, - type: type, - patch: patch, - ignoreDeletions: IgnoreDeletions, - ignoreAdditionsAndModifications: IgnoreAdditionsAndModifications - ); - } else { - comparer.CreateThreeWayPatch( - original: Original, - modified: Modified, - current: Current, - type: type, - patch: patch - ); - } - foreach (var operation in patch.Operations) { - WriteObject(operation); - } - } - } -} diff --git a/src/KubeResourceComparer.cs b/src/KubeResourceComparer.cs deleted file mode 100644 index 2568164..0000000 --- a/src/KubeResourceComparer.cs +++ /dev/null @@ -1,424 +0,0 @@ -using System; -using System.ComponentModel; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using KubeClient; -using KubeClient.Models; -using KubeClient.ResourceClients; -using Microsoft.AspNetCore.JsonPatch; -using Microsoft.AspNetCore.JsonPatch.Operations; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; - -namespace Kubectl { - public sealed class KubeResourceComparer { - private Dictionary> nonUpdateableTypes = new Dictionary> { - [typeof(KubeObjectV1)] = new HashSet { - "ApiVersion", - "Kind", - }, - [typeof(KubeResourceV1)] = new HashSet { - // Status cannot be updated directly, only spec - // Not that Status is not strictly a property of KubeResourceV1 but effectively all subclasses have it - "Status", - }, - [typeof(ObjectMetaV1)] = new HashSet { - "ResourceVersion", - "DeletionTimestamp", - "Generation", - "Uid", - "SelfLink", - "CreationTimestamp", - }, - // [typeof(ContainerV1)] = new HashSet { - // "TerminationMessagePolicy", - // "TerminationMessagePath", - // } - }; - private ILogger logger; - public KubeResourceComparer(ILoggerFactory loggerFactory) { - this.logger = loggerFactory.CreateLogger(nameof(KubeResourceComparer)); - } - - /// - /// CreateThreeWayMergePatch reconciles a modified configuration with an original configuration, - /// while preserving any changes or deletions made to the original configuration in the interim, - /// and not overridden by the current configuration. - /// - /// The configuration in the current state on the server - /// The user-modified local configuration - /// If true, update the annotation in modified with the value of modified before diffing - public void CreateThreeWayPatchFromLastApplied(object current, object modified, Type type, JsonPatchDocument patch, bool annotate) { - if (current == null) throw new ArgumentNullException(nameof(current)); - if (modified == null) throw new ArgumentNullException(nameof(modified)); - if (type == null) throw new ArgumentNullException(nameof(type)); - if (patch == null) throw new ArgumentNullException(nameof(patch)); - - object original = modified; - var originalAnnotations = (IDictionary)current.GetDynamicPropertyValue("Metadata")?.GetDynamicPropertyValue("Annotations"); - var originalJson = (string)originalAnnotations?[Annotations.LastAppliedConfig]; - if (!String.IsNullOrEmpty(originalJson)) { - original = JsonConvert.DeserializeObject(originalJson, type); - } - if (annotate) { - var modifiedJson = JsonConvert.SerializeObject(modified, new PSObjectJsonConverter()); - if (modified.GetDynamicPropertyValue("Metadata") == null) { - modified.SetDynamicPropertyValue("Metadata", new PSObject()); - } - if (modified.GetDynamicPropertyValue("Metadata").GetDynamicPropertyValue("Annotations") == null) { - modified.GetDynamicPropertyValue("Metadata").SetDynamicPropertyValue("Annotations", new Dictionary()); - } - var annotations = (IDictionary)modified.GetDynamicPropertyValue("Metadata").GetDynamicPropertyValue("Annotations"); - annotations[Annotations.LastAppliedConfig] = modifiedJson; - } - CreateThreeWayPatch(original, modified, current, type, patch); - } - - /// - /// CreateThreeWayMergePatch reconciles a modified configuration with an original configuration, - /// while preserving any changes or deletions made to the original configuration in the interim, - /// and not overridden by the current configuration. - /// - /// The original configuration from the annotation in current - /// The user-modified local configuration - /// The configuration in the current state on the server - public void CreateThreeWayPatch(object original, object modified, dynamic current, Type type, JsonPatchDocument patch) { - if (current == null) throw new ArgumentNullException(nameof(current)); - if (modified == null) throw new ArgumentNullException(nameof(modified)); - if (current == null) throw new ArgumentNullException(nameof(current)); - if (type == null) throw new ArgumentNullException(nameof(type)); - if (patch == null) throw new ArgumentNullException(nameof(patch)); - - CreateTwoWayPatch(current, modified, type, patch, ignoreDeletions: true); - CreateTwoWayPatch(original, modified, type, patch, ignoreAdditionsAndModifications: true); - } - - /// - /// Configures the passed JSON Patch so that it yields modified when applied to original - /// - Adding fields to the patch present in modified, missing from original - /// - Setting fields to the patch present in modified and original with different values - /// - Delete fields present in original, missing from modified through - /// - IFF map field - set to nil in patch ??? - /// - IFF list of maps && merge strategy - use deleteDirective for the elements ??? - /// - IFF list of primitives && merge strategy - use parallel deletion list ??? - /// - IFF list of maps or primitives with replace strategy (default) - set patch value to the value in modified ??? - /// - Build $retainKeys directive for fields with retainKeys patch strategy ??? - /// - /// The original object - /// The modified object - /// The type that original and modified represent (even if they are not actually that type, but a PSObject) - /// The JSON pointer to the currently inspected values - /// The strategy to use for patching (replace or merge with mergeKey) - public void CreateTwoWayPatch(object original, object modified, Type type, JsonPatchDocument patch, string path = "", MergeStrategyAttribute mergeStrategy = null, bool ignoreDeletions = false, bool ignoreAdditionsAndModifications = false) { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (patch == null) throw new ArgumentNullException(nameof(patch)); - if (path == null) throw new ArgumentNullException(nameof(path)); - - logger.LogTrace($"Path: {path}"); - if (modified == null && original == null) { - return; - } - if (modified == null && original != null) { - if (!ignoreDeletions) { - patch.Replace(path, modified); - } - return; - } - if (original == null && modified != null) { - if (!ignoreAdditionsAndModifications) { - patch.Replace(path, modified); - } - return; - } - - // From this point, original and modified are known to be non-null - - logger.LogTrace($"Type: original {original?.GetType().Name} modified {modified?.GetType().Name} expected {type?.Name}"); - - // string, int, float, bool, enum, DateTime - if (modified is string || type.IsValueType) { - logger.LogTrace($"Is value type, comparing {original} <-> {modified}"); - // Replace if changed, otherwise do nothing - // We NEED to use Equals() here instead of != because the static type is object, meaning the scalar is boxed. - // Since operators are resolved at compile time, this would use the == implementation for object, - // while Equals() is dynamically dispatched on the real boxed type. - if (!original.Equals(modified)) { - patch.Replace(path, modified); - } - return; - } - - // From this point, original and modified are known to be reference types - - if (System.Object.ReferenceEquals(original, modified)) { - // Same object, short cut - return; - } - - if (modified is IList) { - logger.LogTrace("Is List"); - Type valueType = type.GetGenericArguments()[0]; - // Handle lists - // Really just casting to generic IEnumerable get access to more LINQ. It's all object anyway. - IEnumerable originalEnumerable = ((IList)original).Cast(); - IEnumerable modifiedEnumerable = ((IList)modified).Cast(); - // Check if the list property has a strategic merge strategy attribute - if (mergeStrategy != null) { - if (mergeStrategy.Key != null) { - logger.LogTrace("List is unordered set keyed by merge key"); - // The lists are to be treated like dictionaries, keyed by Key - logger.LogTrace($"Merge key: {mergeStrategy.Key}"); - Func keySelector = listElement => { - PropertyInfo mergeProperty = ModelHelpers.FindJsonProperty(valueType, mergeStrategy.Key); - object value = listElement.GetDynamicPropertyValue(mergeProperty.Name); - if (value == null) { - throw new Exception($"Merge key {mergeProperty} on type {valueType.FullName} cannot be null"); - } - logger.LogTrace($"Merge property value: {value}"); - return value; - }; - // The merge key value is *not* guaranteed to be string, - // for example ContainerPortV1 has the merge key ContainerPort which is type int - Dictionary originalDict = originalEnumerable.ToDictionary(keySelector); - Dictionary modifiedDict = modifiedEnumerable.ToDictionary(keySelector); - var removeOperations = new List(); - int index = 0; - foreach (var originalElement in originalEnumerable) { - object elementKey = originalElement.GetDynamicPropertyValue(ModelHelpers.FindJsonProperty(valueType, mergeStrategy.Key).Name); - string elementPath = path + "/" + index; - if (!modifiedDict.ContainsKey(elementKey)) { - if (!ignoreDeletions) { - // Entry removed in modified - // Check that the value at the given index is really the value we want to modify, - // to make sure indexes were not modified on the server - // Queue these up because they shift array indexes around and for simplicity we want to get the modifications add first - // This makes the patch easier to reason about. - removeOperations.Add(() => patch.Test(elementPath + "/" + escapeJsonPointer(mergeStrategy.Key), elementKey)); - removeOperations.Add(() => patch.Remove(elementPath)); - } - } else { - // Entry present in both, merge recursively - patch.Test(elementPath + "/" + escapeJsonPointer(mergeStrategy.Key), elementKey); - var countBefore = patch.Operations.Count; - CreateTwoWayPatch( - original: originalElement, - modified: modifiedDict[elementKey], - type: valueType, - patch: patch, - path: elementPath, - ignoreDeletions: ignoreDeletions, - ignoreAdditionsAndModifications: ignoreAdditionsAndModifications - ); - if (patch.Operations.Count == countBefore) { - // Test was not needed, element was not modified - patch.Operations.RemoveAt(patch.Operations.Count - 1); - } - } - index++; - } - // Modifications are done, add remove operations - foreach (var action in removeOperations) { - action(); - } - if (!ignoreAdditionsAndModifications) { - // Entries added in modified - foreach (var modifiedEntry in modifiedDict) { - if (!originalDict.ContainsKey(modifiedEntry.Key)) { - // An element that was added in modified - patch.Add(path + "/-", modifiedEntry.Value); - } - } - } - } else { - logger.LogTrace("List is unordered set"); - // Lists are to be treated like unordered sets - var originalSet = new HashSet(originalEnumerable); - var modifiedSet = new HashSet(modifiedEnumerable); - // The index to adress the element on the server after applying every operation in the patch so far. - int index = 0; - foreach (var originalElement in originalEnumerable) { - string elementPath = path + "/" + index; - if (!modifiedSet.Contains(originalElement)) { - // Deleted from modified - if (!ignoreDeletions) { - // When patching indexes, make sure elements didn't get moved around on the server - // Can directly add them here because unordered sets do not use replace operations, - // only remove and adding to the end - patch.Test(elementPath, originalElement); - patch.Remove(elementPath); - } - } - // Present in both: do nothing - index++; - } - if (!ignoreAdditionsAndModifications) { - foreach (var modifiedElement in modifiedSet) { - if (!originalSet.Contains(modifiedElement)) { - // Added in modified - patch.Add(path + "/-", modifiedElement); - } - } - } - } - } else { - logger.LogTrace("List is ordered list"); - // List is to be treated as an ordered list, e.g. ContainerV1.Command - List originalList = originalEnumerable.ToList(); - List modifiedList = modifiedEnumerable.ToList(); - var removeOperations = new List(); - int index = 0; - foreach (var originalElement in originalList.Take(modifiedList.Count)) { - string elementPath = path + "/" + index; - if (index >= modifiedList.Count) { - // Not present in modified, remove - if (!ignoreDeletions) { - removeOperations.Add(() => patch.Test(elementPath, originalElement)); - removeOperations.Add(() => patch.Remove(elementPath)); - } - } else { - // Present in both, merge recursively - // Add a test to check that indexes were not moved on the server - patch.Test(elementPath, originalElement); - int countBefore = patch.Operations.Count; - CreateTwoWayPatch( - original: originalElement, - modified: modifiedList[index], - type: valueType, - patch: patch, - path: elementPath, - ignoreDeletions: ignoreDeletions, - ignoreAdditionsAndModifications: ignoreAdditionsAndModifications - ); - if (patch.Operations.Count == countBefore) { - // Test was not needed, element was not modified - patch.Operations.RemoveAt(patch.Operations.Count - 1); - } - } - index++; - } - // Modifications are done, register remove operations - foreach (var action in removeOperations) { - action(); - } - // Continue on modifiedList (if it's longer) to add added elements - for (; index < modifiedList.Count; index++) { - // Added in modifiedList - object addedElement = modifiedList[index]; - patch.Add(path + "/-", addedElement); - } - } - } else if (modified is IDictionary) { - logger.LogTrace("Is Dictionary"); - Type valueType = type.GetGenericArguments()[1]; - // Handle maps (e.g. KubeResourceV1.Annotations) - IDictionary originalDict = (IDictionary)original; - IDictionary modifiedDict = (IDictionary)modified; - // Always merge maps - foreach (DictionaryEntry originalEntry in originalDict) { - string entryKey = (string)originalEntry.Key; - object entryValue = (object)originalEntry.Value; - string entryPath = path + "/" + escapeJsonPointer(entryKey); - if (!modifiedDict.Contains(originalEntry.Key)) { - if (!ignoreDeletions) { - // Entry removed in modified - patch.Remove(entryPath); - } - } else { - // Entry present in both, merge recursively - CreateTwoWayPatch( - original: entryValue, - modified: modifiedDict[originalEntry.Key], - type: valueType, - patch: patch, - path: entryPath, - ignoreDeletions: ignoreDeletions, - ignoreAdditionsAndModifications: ignoreAdditionsAndModifications - ); - } - } - if (!ignoreAdditionsAndModifications) { - // Entries added in modified - foreach (DictionaryEntry modifiedEntry in modifiedDict) { - string entryKey = (string)modifiedEntry.Key; - object entryValue = (object)modifiedEntry.Value; - if (!originalDict.Contains(entryKey)) { - // An element that was added in modified - patch.Add(path + "/" + escapeJsonPointer(entryKey), entryValue); - } - } - } - } else { - logger.LogTrace("Is other object"); - // resourceVersion: a string that identifies the internal version of this object that can be used by - // clients to determine when objects have changed. This value MUST be treated as opaque by clients - // and passed unmodified back to the server. - // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata - // Add this test before traversing into other properties - if (type.IsSubclassOf(typeof(KubeResourceV1))) { - var resourceVersion = (string)original.GetDynamicPropertyValue("Metadata")?.GetDynamicPropertyValue("ResourceVersion"); - if (!String.IsNullOrEmpty(resourceVersion)) { - patch.Test(path + "/metadata/resourceVersion", resourceVersion); - } - } - // Warn if properties were passed that are not recognized to highlight mistakes - foreach (var propName in modified.GetDynamicPropertyNames().Where(name => type.GetProperty(name) == null)) { - logger.LogWarning($"Unknown property \"{propName}\" on {type.Name} at path \"{path}\""); - } - // KubeObjects, compare properties recursively - foreach (PropertyInfo prop in type.GetProperties()) { - logger.LogTrace($"Property {prop.Name}"); - - // Ignore properties that are not part of modified - if (modified is PSObject psObject && psObject.Properties[prop.Name] == null) { - continue; - } - - JsonPropertyAttribute jsonAttribute = (JsonPropertyAttribute)prop.GetCustomAttribute(typeof(JsonPropertyAttribute)); - string propPath = path + "/" + escapeJsonPointer(jsonAttribute.PropertyName); - - object originalValue = original.GetDynamicPropertyValue(prop.Name); - object modifiedValue = modified.GetDynamicPropertyValue(prop.Name); - - if (!isPropertyUpdateable(type, prop)) { - continue; - } - - // Pass patch strategy attribute to diff function for the property we're looking at - MergeStrategyAttribute attribute = (MergeStrategyAttribute)Attribute.GetCustomAttribute(prop, typeof(MergeStrategyAttribute)); - CreateTwoWayPatch( - original: originalValue, - modified: modifiedValue, - type: prop.PropertyType, - patch: patch, - path: propPath, - mergeStrategy: attribute, - ignoreDeletions: ignoreDeletions, - ignoreAdditionsAndModifications: ignoreAdditionsAndModifications - ); - } - } - } - - private static string escapeJsonPointer(string referenceToken) { - return referenceToken.Replace("~", "~0").Replace("/", "~1"); - } - - private bool isPropertyUpdateable(Type objectType, PropertyInfo prop) { - foreach (var typePropsEntry in nonUpdateableTypes) { - var nonUpdateableType = typePropsEntry.Key; - var nonUpdateableProperties = typePropsEntry.Value; - if (objectType.IsSubclassOf(nonUpdateableType) && nonUpdateableProperties.Contains(prop.Name)) { - return false; - } - } - return true; - } - } -} diff --git a/src/PSKubectl.csproj b/src/PSKubectl.csproj index 88af18b..d11a4f3 100644 --- a/src/PSKubectl.csproj +++ b/src/PSKubectl.csproj @@ -8,7 +8,6 @@ - All diff --git a/src/PSObjectAwareContractResolver.cs b/src/PSObjectAwareContractResolver.cs deleted file mode 100644 index c98ace6..0000000 --- a/src/PSObjectAwareContractResolver.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Management.Automation; -using Newtonsoft.Json.Serialization; - -namespace Kubectl { - public class PSObjectAwareContractResolver : DefaultContractResolver { - protected override JsonContract CreateContract(Type objectType) { - JsonContract contract = base.CreateContract(objectType); - if (objectType.IsSubclassOf(typeof(PSObject))) { - contract.Converter = new PSObjectJsonConverter(); - } - return contract; - } - } -} diff --git a/src/PSObjectJsonConverter.cs b/src/PSObjectJsonConverter.cs deleted file mode 100644 index b8906df..0000000 --- a/src/PSObjectJsonConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Management.Automation; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Kubectl { - /// JsonConverter that writes out PSObjects like normal objects - public class PSObjectJsonConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, PSObject psObject, JsonSerializer serializer) { - writer.WriteStartObject(); - foreach (var prop in psObject.Properties) { - // Do not include ScriptProperties defined in Types.ps1xml files, only the members added by PSKubectl - if (!prop.IsGettable || (prop.MemberType != PSMemberTypes.NoteProperty && prop.MemberType != PSMemberTypes.Property)) { - continue; - } - var propNameCamelCase = char.ToLower(prop.Name[0]) + prop.Name.Substring(1); - writer.WritePropertyName(propNameCamelCase); - serializer.Serialize(writer, prop.Value); - } - writer.WriteEndObject(); - } - - public override PSObject ReadJson(JsonReader reader, Type objectType, PSObject existingValue, bool hasExistingValue, JsonSerializer serializer) { - throw new NotImplementedException(); - } - - public override bool CanRead { get { return false; } } - } -}