YAML support for extension (#638)

* Yaml support for extensions

* Update RunawayProcessKillerTest.cs

* Update yaml_extension_support

Add unit test

* Update Yaml extension support

* Upate  extension yaml support

Remove old object query class and add methods to extension which can take yaml objects as aruments.

Add unit test for runawayprocesskiller for yaml support

* Use stong types instead object to manage extension confiuration with YAML

* Add unit test for SharedDirectoryMapper yaml config

* Update yaml extension support

Improve error messages
improve boolean parser
remove unncessary logs
pull/652/head
Buddhika Chathuranga 2020-08-21 07:06:39 +05:30 committed by GitHub
parent ef7ba3fe32
commit 35dc0b11f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 339 additions and 12 deletions

View File

@ -135,6 +135,8 @@ namespace WinSW.Configuration
// Extensions
public XmlNode? ExtensionsConfiguration => null;
public List<YamlExtensionConfiguration>? YamlExtensionsConfiguration => new List<YamlExtensionConfiguration>(0);
public string BaseName
{
get

View File

@ -76,6 +76,8 @@ namespace WinSW.Configuration
// Extensions
XmlNode? ExtensionsConfiguration { get; }
List<YamlExtensionConfiguration>? YamlExtensionsConfiguration { get; }
List<string> ExtensionIds { get; }
// Service Account

View File

@ -106,9 +106,6 @@ namespace WinSW.Configuration
[YamlMember(Alias = "securityDescriptor")]
public string? SecurityDescriptorYaml { get; set; }
[YamlMember(Alias = "extensions")]
public List<string>? YamlExtensionIds { get; set; }
public class YamlEnv
{
[YamlMember(Alias = "name")]
@ -658,10 +655,42 @@ namespace WinSW.Configuration
public string LogMode => this.Log.Mode is null ? this.Defaults.LogMode : this.Log.Mode;
// TODO - Extensions
XmlNode? IWinSWConfiguration.ExtensionsConfiguration => throw new NotImplementedException();
public XmlNode? ExtensionsConfiguration => null;
public List<string> ExtensionIds => this.YamlExtensionIds ?? this.Defaults.ExtensionIds;
// YAML Extension
[YamlMember(Alias = "extensions")]
public List<YamlExtensionConfiguration>? YamlExtensionsConfiguration { get; set; }
public List<string> ExtensionIds
{
get
{
int extensionNumber = 1;
if (this.YamlExtensionsConfiguration is null)
{
return new List<string>(0);
}
var result = new List<string>(this.YamlExtensionsConfiguration.Count);
foreach (var item in this.YamlExtensionsConfiguration)
{
try
{
result.Add(item.GetId());
}
catch (InvalidDataException)
{
throw new InvalidDataException("Id is null in Extension " + extensionNumber);
}
extensionNumber++;
}
return result;
}
}
public string BaseName { get; set; }

View File

@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.IO;
using YamlDotNet.Serialization;
namespace WinSW.Configuration
{
public class YamlExtensionConfiguration
{
[YamlMember(Alias = "id")]
public string? ExtensionId { get; set; }
[YamlMember(Alias = "className")]
public string? ExtensionClassName { get; set; }
[YamlMember(Alias = "enabled")]
public bool Enabled { get; set; }
[YamlMember(Alias = "settings")]
public Dictionary<object, object>? Settings { get; set; }
public string GetId()
{
if (this.ExtensionId is null)
{
throw new InvalidDataException();
}
return this.ExtensionId;
}
public string GetClassName()
{
if (this.ExtensionClassName is null)
{
throw new InvalidDataException($@"Extension ClassName is empty in extension {this.GetId()}");
}
return this.ExtensionClassName;
}
public Dictionary<object, object> GetSettings()
{
if (this.Settings is null)
{
throw new InvalidDataException(@$"Extension settings is empty in extension {this.GetId()}");
}
return this.Settings;
}
}
}

View File

@ -16,6 +16,11 @@ namespace WinSW.Extensions
// Do nothing
}
public virtual void Configure(IWinSWConfiguration descriptor, YamlExtensionConfiguration config)
{
// Do nothing
}
public virtual void OnWrapperStarted()
{
// Do nothing

View File

@ -30,6 +30,13 @@ namespace WinSW.Extensions
/// <param name="node">Configuration node</param>
void Configure(IWinSWConfiguration descriptor, XmlNode node);
/// <summary>
/// Configure the extension from Yaml configuration
/// </summary>
/// <param name="descriptor">YamlConfiguration</param>
/// <param name="config">Configuration Node</param>
void Configure(IWinSWConfiguration descriptor, YamlExtensionConfiguration config);
/// <summary>
/// Start handler. Called during startup of the service before the child process.
/// </summary>

View File

@ -1,4 +1,5 @@
using System.Xml;
using WinSW.Configuration;
using WinSW.Util;
namespace WinSW.Extensions
@ -40,5 +41,14 @@ namespace WinSW.Extensions
string id = XmlHelper.SingleAttribute<string>(node, "id");
return new WinSWExtensionDescriptor(id, className, enabled);
}
public static WinSWExtensionDescriptor FromYaml(YamlExtensionConfiguration config)
{
bool enabled = config.Enabled;
string className = config.GetClassName();
string id = config.GetId();
return new WinSWExtensionDescriptor(id, className, enabled);
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using log4net;
using WinSW.Configuration;
@ -127,6 +128,18 @@ namespace WinSW.Extensions
throw new ExtensionException(id, "Extension has been already loaded");
}
if (this.ServiceDescriptor.GetType() == typeof(ServiceDescriptor))
{
this.LoadExtensionFromXml(id);
}
else
{
this.LoadExtensionFromYaml(id);
}
}
private void LoadExtensionFromXml(string id)
{
XmlNode? extensionsConfig = this.ServiceDescriptor.ExtensionsConfiguration;
XmlElement? configNode = extensionsConfig is null ? null : extensionsConfig.SelectSingleNode("extension[@id='" + id + "'][1]") as XmlElement;
if (configNode is null)
@ -135,6 +148,7 @@ namespace WinSW.Extensions
}
var descriptor = WinSWExtensionDescriptor.FromXml(configNode);
if (descriptor.Enabled)
{
IWinSWExtension extension = this.CreateExtensionInstance(descriptor.Id, descriptor.ClassName);
@ -146,7 +160,7 @@ namespace WinSW.Extensions
catch (Exception ex)
{ // Consider any unexpected exception as fatal
Log.Fatal("Failed to configure the extension " + id, ex);
throw ex;
throw new ExtensionException(id, "Failed to configure the extension");
}
this.Extensions.Add(id, extension);
@ -158,6 +172,56 @@ namespace WinSW.Extensions
}
}
private void LoadExtensionFromYaml(string id)
{
var extensionConfigList = this.ServiceDescriptor.YamlExtensionsConfiguration;
if (extensionConfigList is null)
{
throw new ExtensionException(id, "Cannot get the configuration entry");
}
var configNode = GetYamlonfigById(extensionConfigList, id);
var descriptor = WinSWExtensionDescriptor.FromYaml(configNode);
if (descriptor.Enabled)
{
IWinSWExtension extension = this.CreateExtensionInstance(descriptor.Id, descriptor.ClassName);
extension.Descriptor = descriptor;
try
{
extension.Configure(this.ServiceDescriptor, configNode);
}
catch (Exception ex)
{ // Consider any unexpected exception as fatal
Log.Fatal("Failed to configure the extension " + id, ex);
throw new ExtensionException(id, "Failed to configure the extension");
}
this.Extensions.Add(id, extension);
Log.Info("Extension loaded: " + id);
}
else
{
Log.Warn("Extension is disabled: " + id);
}
YamlExtensionConfiguration GetYamlonfigById(List<YamlExtensionConfiguration> configs, string id)
{
foreach (var item in configs)
{
if (item.GetId().Equals(id))
{
return item;
}
}
throw new ExtensionException(id, $@"Can't find extension with id: ""{id}"" ");
}
}
private IWinSWExtension CreateExtensionInstance(string id, string className)
{
object created;

View File

@ -703,5 +703,7 @@ namespace WinSW
return environment;
}
public List<YamlExtensionConfiguration>? YamlExtensionsConfiguration => Defaults.YamlExtensionsConfiguration;
}
}

View File

@ -18,7 +18,7 @@ namespace WinSW
using (var reader = new StreamReader(basepath + ".yml"))
{
var file = reader.ReadToEnd();
var deserializer = new DeserializerBuilder().Build();
var deserializer = new DeserializerBuilder().IgnoreUnmatchedProperties().Build();
this.Configurations = deserializer.Deserialize<YamlConfiguration>(file);
}
@ -47,7 +47,7 @@ namespace WinSW
public static ServiceDescriptorYaml FromYaml(string yaml)
{
var deserializer = new DeserializerBuilder().Build();
var deserializer = new DeserializerBuilder().IgnoreUnmatchedProperties().Build();
var configs = deserializer.Deserialize<YamlConfiguration>(yaml);
return new ServiceDescriptorYaml(configs);
}

View File

@ -33,5 +33,17 @@ namespace WinSW.Util
{ "day", 1000L * 60L * 60L * 24L },
{ "days", 1000L * 60L * 60L * 24L }
};
public static bool YamlBoolParse(string value)
{
value = value.ToLower();
if (value.Equals("true") || value.Equals("yes") || value.Equals("on") || value.Equals("y") || value.Equals("1"))
{
return true;
}
return false;
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
@ -193,6 +194,24 @@ namespace WinSW.Plugins.RunawayProcessKiller
this.CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable is null ? true : bool.Parse(checkWinSWEnvironmentVariable);
}
public override void Configure(IWinSWConfiguration descriptor, YamlExtensionConfiguration config)
{
var dict = config.GetSettings();
this.Pidfile = (string)dict["pidfile"];
this.StopTimeout = TimeSpan.FromMilliseconds(int.Parse((string)dict["stopTimeOut"]));
this.StopParentProcessFirst = bool.Parse((string)dict["StopParentFirst"]);
try
{
this.CheckWinSWEnvironmentVariable = bool.Parse((string)dict["checkWinSWEnvironmentVariable"]);
}
catch
{
this.CheckWinSWEnvironmentVariable = true;
}
}
/// <summary>
/// This method checks if the PID file is stored on the disk and then terminates runaway processes if they exist.
/// </summary>

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Xml;
using log4net;
using WinSW.Configuration;
@ -43,6 +44,24 @@ namespace WinSW.Plugins.SharedDirectoryMapper
}
}
public override void Configure(IWinSWConfiguration descriptor, YamlExtensionConfiguration config)
{
var dict = config.GetSettings();
var mappingNode = dict["mapping"];
if (!(mappingNode is List<object> mappings))
{
throw new InvalidDataException("SharedDirectoryMapper mapping should be a list");
}
foreach (var map in mappings)
{
var mapConfig = SharedDirectoryMapperConfig.FromYaml(map);
this._entries.Add(mapConfig);
}
}
public override void OnWrapperStarted()
{
foreach (SharedDirectoryMapperConfig config in this._entries)

View File

@ -1,4 +1,6 @@
using System.Xml;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using WinSW.Util;
namespace WinSW.Plugins.SharedDirectoryMapper
@ -26,5 +28,20 @@ namespace WinSW.Plugins.SharedDirectoryMapper
string uncPath = XmlHelper.SingleAttribute<string>(node, "uncpath");
return new SharedDirectoryMapperConfig(enableMapping, label, uncPath);
}
public static SharedDirectoryMapperConfig FromYaml(object yamlObject)
{
if (!(yamlObject is Dictionary<object, object> dict))
{
// TODO : throw ExtensionExeption
throw new InvalidDataException("SharedDirectoryMapperConfig config error");
}
bool enableMapping = ConfigHelper.YamlBoolParse((string)dict["enabled"]);
string label = (string)dict["label"];
string uncPath = (string)dict["uncpath"];
return new SharedDirectoryMapperConfig(enableMapping, label, uncPath);
}
}
}

View File

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.IO;
using NUnit.Framework;
using WinSW;
using WinSW.Configuration;
using WinSW.Extensions;
using WinSW.Plugins.RunawayProcessKiller;
using WinSW.Util;
@ -14,7 +15,8 @@ namespace winswTests.Extensions
[TestFixture]
class RunawayProcessKillerExtensionTest : ExtensionTestBase
{
ServiceDescriptor _testServiceDescriptor;
IWinSWConfiguration _testServiceDescriptor;
IWinSWConfiguration _testServiceDescriptorYaml;
readonly string testExtension = GetExtensionClassNameWithAssembly(typeof(RunawayProcessKillerExtension));
@ -38,6 +40,29 @@ $@"<service>
</extensions>
</service>";
this._testServiceDescriptor = ServiceDescriptor.FromXML(seedXml);
string seedYaml = $@"---
id: jenkins
name: Jenkins
description: This service runs Jenkins automation server.
env:
-
name: JENKINS_HOME
value: '%LocalAppData%\Jenkins.jenkins'
executable: java
arguments: >-
-Xrs -Xmx256m -Dhudson.lifecycle=hudson.lifecycle.WindowsServiceLifecycle
-jar E:\Winsw Test\yml6\jenkins.war --httpPort=8081
extensions:
- id: killRunawayProcess
enabled: yes
className: ""{this.testExtension}""
settings:
pidfile: 'foo/bar/pid.txt'
stopTimeOut: 5000
StopParentFirst: true";
this._testServiceDescriptorYaml = ServiceDescriptorYaml.FromYaml(seedYaml).Configurations;
}
[Test]
@ -55,6 +80,21 @@ $@"<service>
Assert.AreEqual(true, extension.StopParentProcessFirst, "Loaded StopParentFirst is not equal to the expected one");
}
[Test]
public void LoadExtensionsYaml()
{
WinSWExtensionManager manager = new WinSWExtensionManager(this._testServiceDescriptorYaml);
manager.LoadExtensions();
Assert.AreEqual(1, manager.Extensions.Count, "One extension should be loaded");
// Check the file is correct
var extension = manager.Extensions["killRunawayProcess"] as RunawayProcessKillerExtension;
Assert.IsNotNull(extension, "RunawayProcessKillerExtension should be loaded");
Assert.AreEqual("foo/bar/pid.txt", extension.Pidfile, "Loaded PID file path is not equal to the expected one");
Assert.AreEqual(5000, extension.StopTimeout.TotalMilliseconds, "Loaded Stop Timeout is not equal to the expected one");
Assert.AreEqual(true, extension.StopParentProcessFirst, "Loaded StopParentFirst is not equal to the expected one");
}
[Test]
public void StartStopExtension()
{

View File

@ -1,5 +1,6 @@
using NUnit.Framework;
using WinSW;
using WinSW.Configuration;
using WinSW.Extensions;
using WinSW.Plugins.SharedDirectoryMapper;
@ -8,7 +9,8 @@ namespace winswTests.Extensions
[TestFixture]
class SharedDirectoryMapperTest : ExtensionTestBase
{
ServiceDescriptor _testServiceDescriptor;
IWinSWConfiguration _testServiceDescriptor;
IWinSWConfiguration _testServiceDescriptorYaml;
readonly string testExtension = GetExtensionClassNameWithAssembly(typeof(SharedDirectoryMapper));
@ -39,6 +41,44 @@ $@"<service>
</extensions>
</service>";
this._testServiceDescriptor = ServiceDescriptor.FromXML(seedXml);
string seedYaml = $@"---
id: jenkins
name: Jenkins
description: This service runs Jenkins automation server.
env:
-
name: JENKINS_HOME
value: '%LocalAppData%\Jenkins.jenkins'
executable: java
arguments: >-
-Xrs -Xmx256m -Dhudson.lifecycle=hudson.lifecycle.WindowsServiceLifecycle
-jar E:\Winsw Test\yml6\jenkins.war --httpPort=8081
extensions:
- id: mapNetworDirs
className: ""{this.testExtension}""
enabled: true
settings:
mapping:
- enabled: false
label: N
uncpath: \\UNC
- enabled: false
label: M
uncpath: \\UNC2
- id: mapNetworDirs2
className: ""{this.testExtension}""
enabled: true
settings:
mapping:
- enabled: false
label: X
uncpath: \\UNC
- enabled: false
label: Y
uncpath: \\UNC2";
this._testServiceDescriptorYaml = ServiceDescriptorYaml.FromYaml(seedYaml).Configurations;
}
[Test]
@ -49,6 +89,14 @@ $@"<service>
Assert.AreEqual(2, manager.Extensions.Count, "Two extensions should be loaded");
}
[Test]
public void LoadExtensionsYaml()
{
WinSWExtensionManager manager = new WinSWExtensionManager(this._testServiceDescriptorYaml);
manager.LoadExtensions();
Assert.AreEqual(2, manager.Extensions.Count, "Two extensions should be loaded");
}
[Test]
public void StartStopExtension()
{