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 // Extensions
public XmlNode? ExtensionsConfiguration => null; public XmlNode? ExtensionsConfiguration => null;
public List<YamlExtensionConfiguration>? YamlExtensionsConfiguration => new List<YamlExtensionConfiguration>(0);
public string BaseName public string BaseName
{ {
get get

View File

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

View File

@ -106,9 +106,6 @@ namespace WinSW.Configuration
[YamlMember(Alias = "securityDescriptor")] [YamlMember(Alias = "securityDescriptor")]
public string? SecurityDescriptorYaml { get; set; } public string? SecurityDescriptorYaml { get; set; }
[YamlMember(Alias = "extensions")]
public List<string>? YamlExtensionIds { get; set; }
public class YamlEnv public class YamlEnv
{ {
[YamlMember(Alias = "name")] [YamlMember(Alias = "name")]
@ -658,10 +655,42 @@ namespace WinSW.Configuration
public string LogMode => this.Log.Mode is null ? this.Defaults.LogMode : this.Log.Mode; public string LogMode => this.Log.Mode is null ? this.Defaults.LogMode : this.Log.Mode;
// TODO - Extensions public XmlNode? ExtensionsConfiguration => null;
XmlNode? IWinSWConfiguration.ExtensionsConfiguration => throw new NotImplementedException();
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; } 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 // Do nothing
} }
public virtual void Configure(IWinSWConfiguration descriptor, YamlExtensionConfiguration config)
{
// Do nothing
}
public virtual void OnWrapperStarted() public virtual void OnWrapperStarted()
{ {
// Do nothing // Do nothing

View File

@ -30,6 +30,13 @@ namespace WinSW.Extensions
/// <param name="node">Configuration node</param> /// <param name="node">Configuration node</param>
void Configure(IWinSWConfiguration descriptor, XmlNode node); 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> /// <summary>
/// Start handler. Called during startup of the service before the child process. /// Start handler. Called during startup of the service before the child process.
/// </summary> /// </summary>

View File

@ -1,4 +1,5 @@
using System.Xml; using System.Xml;
using WinSW.Configuration;
using WinSW.Util; using WinSW.Util;
namespace WinSW.Extensions namespace WinSW.Extensions
@ -40,5 +41,14 @@ namespace WinSW.Extensions
string id = XmlHelper.SingleAttribute<string>(node, "id"); string id = XmlHelper.SingleAttribute<string>(node, "id");
return new WinSWExtensionDescriptor(id, className, enabled); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Xml; using System.Xml;
using log4net; using log4net;
using WinSW.Configuration; using WinSW.Configuration;
@ -127,6 +128,18 @@ namespace WinSW.Extensions
throw new ExtensionException(id, "Extension has been already loaded"); 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; XmlNode? extensionsConfig = this.ServiceDescriptor.ExtensionsConfiguration;
XmlElement? configNode = extensionsConfig is null ? null : extensionsConfig.SelectSingleNode("extension[@id='" + id + "'][1]") as XmlElement; XmlElement? configNode = extensionsConfig is null ? null : extensionsConfig.SelectSingleNode("extension[@id='" + id + "'][1]") as XmlElement;
if (configNode is null) if (configNode is null)
@ -135,6 +148,7 @@ namespace WinSW.Extensions
} }
var descriptor = WinSWExtensionDescriptor.FromXml(configNode); var descriptor = WinSWExtensionDescriptor.FromXml(configNode);
if (descriptor.Enabled) if (descriptor.Enabled)
{ {
IWinSWExtension extension = this.CreateExtensionInstance(descriptor.Id, descriptor.ClassName); IWinSWExtension extension = this.CreateExtensionInstance(descriptor.Id, descriptor.ClassName);
@ -146,7 +160,7 @@ namespace WinSW.Extensions
catch (Exception ex) catch (Exception ex)
{ // Consider any unexpected exception as fatal { // Consider any unexpected exception as fatal
Log.Fatal("Failed to configure the extension " + id, ex); 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); 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) private IWinSWExtension CreateExtensionInstance(string id, string className)
{ {
object created; object created;

View File

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

View File

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

View File

@ -33,5 +33,17 @@ namespace WinSW.Util
{ "day", 1000L * 60L * 60L * 24L }, { "day", 1000L * 60L * 60L * 24L },
{ "days", 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;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@ -193,6 +194,24 @@ namespace WinSW.Plugins.RunawayProcessKiller
this.CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable is null ? true : bool.Parse(checkWinSWEnvironmentVariable); 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> /// <summary>
/// This method checks if the PID file is stored on the disk and then terminates runaway processes if they exist. /// This method checks if the PID file is stored on the disk and then terminates runaway processes if they exist.
/// </summary> /// </summary>

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Xml; using System.Xml;
using log4net; using log4net;
using WinSW.Configuration; 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() public override void OnWrapperStarted()
{ {
foreach (SharedDirectoryMapperConfig config in this._entries) 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; using WinSW.Util;
namespace WinSW.Plugins.SharedDirectoryMapper namespace WinSW.Plugins.SharedDirectoryMapper
@ -26,5 +28,20 @@ namespace WinSW.Plugins.SharedDirectoryMapper
string uncPath = XmlHelper.SingleAttribute<string>(node, "uncpath"); string uncPath = XmlHelper.SingleAttribute<string>(node, "uncpath");
return new SharedDirectoryMapperConfig(enableMapping, label, 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 System.IO;
using NUnit.Framework; using NUnit.Framework;
using WinSW; using WinSW;
using WinSW.Configuration;
using WinSW.Extensions; using WinSW.Extensions;
using WinSW.Plugins.RunawayProcessKiller; using WinSW.Plugins.RunawayProcessKiller;
using WinSW.Util; using WinSW.Util;
@ -14,7 +15,8 @@ namespace winswTests.Extensions
[TestFixture] [TestFixture]
class RunawayProcessKillerExtensionTest : ExtensionTestBase class RunawayProcessKillerExtensionTest : ExtensionTestBase
{ {
ServiceDescriptor _testServiceDescriptor; IWinSWConfiguration _testServiceDescriptor;
IWinSWConfiguration _testServiceDescriptorYaml;
readonly string testExtension = GetExtensionClassNameWithAssembly(typeof(RunawayProcessKillerExtension)); readonly string testExtension = GetExtensionClassNameWithAssembly(typeof(RunawayProcessKillerExtension));
@ -38,6 +40,29 @@ $@"<service>
</extensions> </extensions>
</service>"; </service>";
this._testServiceDescriptor = ServiceDescriptor.FromXML(seedXml); 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] [Test]
@ -55,6 +80,21 @@ $@"<service>
Assert.AreEqual(true, extension.StopParentProcessFirst, "Loaded StopParentFirst is not equal to the expected one"); 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] [Test]
public void StartStopExtension() public void StartStopExtension()
{ {

View File

@ -1,5 +1,6 @@
using NUnit.Framework; using NUnit.Framework;
using WinSW; using WinSW;
using WinSW.Configuration;
using WinSW.Extensions; using WinSW.Extensions;
using WinSW.Plugins.SharedDirectoryMapper; using WinSW.Plugins.SharedDirectoryMapper;
@ -8,7 +9,8 @@ namespace winswTests.Extensions
[TestFixture] [TestFixture]
class SharedDirectoryMapperTest : ExtensionTestBase class SharedDirectoryMapperTest : ExtensionTestBase
{ {
ServiceDescriptor _testServiceDescriptor; IWinSWConfiguration _testServiceDescriptor;
IWinSWConfiguration _testServiceDescriptorYaml;
readonly string testExtension = GetExtensionClassNameWithAssembly(typeof(SharedDirectoryMapper)); readonly string testExtension = GetExtensionClassNameWithAssembly(typeof(SharedDirectoryMapper));
@ -39,6 +41,44 @@ $@"<service>
</extensions> </extensions>
</service>"; </service>";
this._testServiceDescriptor = ServiceDescriptor.FromXML(seedXml); 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] [Test]
@ -49,6 +89,14 @@ $@"<service>
Assert.AreEqual(2, manager.Extensions.Count, "Two extensions should be loaded"); 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] [Test]
public void StartStopExtension() public void StartStopExtension()
{ {