diff --git a/.gitignore b/.gitignore index e905105f..cad58981 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ src/SIM.Telemetry.Tests/bin/ src/SIM.SitecoreEnvironments/bin/ /.vs src/SIM.Sitecore9Installer.Tests/bin/ +src/SIM.ContainerInstaller/bin/ +src/SIM.Products.Tests/bin/ diff --git a/src/SIM.Adapters/WebServer/Website.cs b/src/SIM.Adapters/WebServer/Website.cs index 3b64026a..1fa61b2a 100644 --- a/src/SIM.Adapters/WebServer/Website.cs +++ b/src/SIM.Adapters/WebServer/Website.cs @@ -20,8 +20,6 @@ public class Website public long ID { get; } - private string name; - #endregion #region Constructors @@ -163,21 +161,9 @@ public virtual string Name { get { - try - { - if (string.IsNullOrEmpty(name)) - { - using (WebServerManager.WebServerContext context = WebServerManager.CreateContext()) - { - name = GetName(context); - } - } - return name; - } - catch(Exception ex) + using (WebServerManager.WebServerContext context = WebServerManager.CreateContext()) { - Log.Error(ex, ex.Message); - return "Error"; + return GetName(context); } } } diff --git a/src/SIM.Base/ApplicationManager.cs b/src/SIM.Base/ApplicationManager.cs index bb33543a..d98d9e61 100644 --- a/src/SIM.Base/ApplicationManager.cs +++ b/src/SIM.Base/ApplicationManager.cs @@ -9,10 +9,15 @@ namespace SIM using Sitecore.Diagnostics.Base; using JetBrains.Annotations; using SIM.Extensions; + using System.Timers; + using System.ServiceProcess; + using System.Linq; + using System.Runtime.CompilerServices; + using System.Threading; #region - + #endregion @@ -29,6 +34,10 @@ public static class ApplicationManager public const string AppDataRoot = @"%AppData%\Sitecore\Sitecore Instance Manager"; public const string DefaultConfigurations = "Configurations"; public const string DefaultPackages = "Packages"; + public const string DockerDesktopPath = @"C:\Program Files\Docker\Docker\Docker Desktop.exe"; + public const string DockerProcessName = "Docker Desktop"; + public const string DockerServiceName = "docker"; + public const string IisServiceName = "W3SVC"; #endregion @@ -82,6 +91,18 @@ public static class ApplicationManager public static string AppsFolder { get; } + public static bool IsIisRunning { get; set; } + + public static bool IsDockerRunning { get; set; } + + public static event PropertyChangedEventHandler IisStatusChanged; + + public static event PropertyChangedEventHandler DockerStatusChanged; + + private static readonly System.Timers.Timer Timer; + + private const double TimerInterval = 10000; + #endregion #region Constructors @@ -108,6 +129,11 @@ static ApplicationManager() AppLabel = GetLabel(); UnInstallParamsFolder = InitializeDataFolder("UnInstallParams"); TempFolder = InitializeDataFolder("Temp"); + IsIisRunning = GetIisStatus(); + IsDockerRunning = GetDockerStatus(); + Timer = new System.Timers.Timer(TimerInterval); + Timer.Elapsed += Timer_Elapsed; + Timer.Enabled = true; } #endregion @@ -224,6 +250,89 @@ public static void RaiseAttemptToClose(CancelEventArgs e) EventHelper.RaiseEvent(AttemptToClose, typeof(ApplicationManager), e); } + public static bool StartStopIis(bool start) + { + Timer.Stop(); + try + { + using (ServiceController serviceController = new ServiceController(IisServiceName)) + { + if (start) + { + serviceController.Start(); + serviceController.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromMilliseconds(TimerInterval)); + } + else + { + serviceController.Stop(); + serviceController.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromMilliseconds(TimerInterval)); + } + } + + RunTimerElapsed(4000); + Timer.Start(); + return true; + } + catch + { + Timer.Start(); + return false; + } + } + + public static bool StartStopDocker(bool start) + { + Timer.Stop(); + try + { + if (start) + { + if (File.Exists(DockerDesktopPath)) + { + Process process = Process.Start(DockerDesktopPath); + if (process != null) + { + RunTimerElapsed(10000); + Timer.Start(); + return true; + } + } + } + else + { + ServiceController serviceController = GetDockerService(); + + if (serviceController != null) + { + serviceController.Stop(); + + foreach (Process process in Process.GetProcessesByName(DockerProcessName)) + { + process.Kill(); + } + + RunTimerElapsed(4000); + Timer.Start(); + return true; + } + } + + Timer.Start(); + return false; + } + catch + { + Timer.Start(); + return false; + } + } + + private static void RunTimerElapsed(int timeout) + { + Thread.Sleep(timeout); + Timer_Elapsed(null, null); + } + #endregion #region Private methods @@ -282,6 +391,76 @@ private static string GetVersion() return version != null ? version.Version : string.Empty; } + private static void Timer_Elapsed(object sender, ElapsedEventArgs e) + { + bool iisStatus = GetIisStatus(); + if (IsIisRunning != iisStatus) + { + IsIisRunning = iisStatus; + OnIisStatusChanged(); + } + + bool dockerStatus = GetDockerStatus(); + if (IsDockerRunning != dockerStatus) + { + IsDockerRunning = dockerStatus; + OnDockerStatusChanged(); + } + } + + private static bool GetIisStatus() + { + try + { + using (ServiceController serviceController = new ServiceController(IisServiceName)) + { + if (serviceController.Status.Equals(ServiceControllerStatus.Running)) + { + return true; + } + return false; + } + } + catch + { + return false; + } + } + + private static bool GetDockerStatus() + { + try + { + ServiceController serviceController = GetDockerService(); + + if (serviceController != null && serviceController.Status.Equals(ServiceControllerStatus.Running)) + { + return true; + } + + return false; + } + catch + { + return false; + } + } + + private static ServiceController GetDockerService() + { + return ServiceController.GetServices().FirstOrDefault(s => s.ServiceName == DockerServiceName); + } + + private static void OnIisStatusChanged([CallerMemberName] string name = null) + { + IisStatusChanged?.Invoke(null, new PropertyChangedEventArgs(name)); + } + + private static void OnDockerStatusChanged([CallerMemberName] string name = null) + { + DockerStatusChanged?.Invoke(null, new PropertyChangedEventArgs(name)); + } + #endregion #region Methods diff --git a/src/SIM.Base/Loggers/EmptyLogger.cs b/src/SIM.Base/Loggers/EmptyLogger.cs new file mode 100644 index 00000000..4c790b0d --- /dev/null +++ b/src/SIM.Base/Loggers/EmptyLogger.cs @@ -0,0 +1,17 @@ +namespace SIM.Loggers +{ + public class EmptyLogger : ILogger + { + public void Info(string message, bool includeSeverityLevel = true) + { + } + + public void Warn(string message, bool includeSeverityLevel = true) + { + } + + public void Error(string message, bool includeSeverityLevel = true) + { + } + } +} \ No newline at end of file diff --git a/src/SIM.Base/Loggers/ILogger.cs b/src/SIM.Base/Loggers/ILogger.cs new file mode 100644 index 00000000..e4a448a3 --- /dev/null +++ b/src/SIM.Base/Loggers/ILogger.cs @@ -0,0 +1,11 @@ +namespace SIM.Loggers +{ + public interface ILogger + { + void Info(string message, bool includeSeverityLevel = true); + + void Warn(string message, bool includeSeverityLevel = true); + + void Error(string message, bool includeSeverityLevel = true); + } +} \ No newline at end of file diff --git a/src/SIM.Base/Loggers/Logger.cs b/src/SIM.Base/Loggers/Logger.cs new file mode 100644 index 00000000..bd985166 --- /dev/null +++ b/src/SIM.Base/Loggers/Logger.cs @@ -0,0 +1,38 @@ +using System; +using Sitecore.Diagnostics.Base; + +namespace SIM.Loggers +{ + public class Logger : ILogger + { + internal Action _WriteLogMessage; + + public Logger(Action writeLogMessage) + { + Assert.ArgumentNotNull(writeLogMessage); + this._WriteLogMessage = writeLogMessage; + } + + public void Info(string message, bool includeSeverityLevel = true) + { + DoWriteMessage(message, includeSeverityLevel, "INFO"); + } + + public void Warn(string message, bool includeSeverityLevel = true) + { + DoWriteMessage(message, includeSeverityLevel, "WARN"); + } + + public void Error(string message, bool includeSeverityLevel = true) + { + DoWriteMessage(message, includeSeverityLevel, "ERROR"); + } + + private void DoWriteMessage(string message, bool includeSeverityLevel = true, string severity = "") + { + string time = DateTime.Now.ToString("HH:mm:ss"); + string text = includeSeverityLevel ? $"[{time}] {severity}: {message}" : $"[{time}] {message}"; + this._WriteLogMessage(text); + } + } +} \ No newline at end of file diff --git a/src/SIM.Base/RangeObservableCollection.cs b/src/SIM.Base/RangeObservableCollection.cs deleted file mode 100644 index f9f6dd0f..00000000 --- a/src/SIM.Base/RangeObservableCollection.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SIM -{ - public class RangeObservableCollection : ObservableCollection - { - public RangeObservableCollection(IEnumerable list) : base(list) - { - - } - - public RangeObservableCollection():base() - { - - } - private bool _suppressNotification = false; - - protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) - { - if (!_suppressNotification) - base.OnCollectionChanged(e); - } - - public void AddRange(IEnumerable list) - { - if (list == null) - throw new ArgumentNullException("list"); - - _suppressNotification = true; - - foreach (T item in list) - { - Add(item); - } - _suppressNotification = false; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } -} diff --git a/src/SIM.Base/SIM.Base.csproj b/src/SIM.Base/SIM.Base.csproj index a0a9a781..e9b160b9 100644 --- a/src/SIM.Base/SIM.Base.csproj +++ b/src/SIM.Base/SIM.Base.csproj @@ -54,6 +54,7 @@ + @@ -78,7 +79,9 @@ - + + + diff --git a/src/SIM.ContainerInstaller/DockerCompose/DockerComposeRepository.cs b/src/SIM.ContainerInstaller/DockerCompose/DockerComposeRepository.cs new file mode 100644 index 00000000..19f5c6ab --- /dev/null +++ b/src/SIM.ContainerInstaller/DockerCompose/DockerComposeRepository.cs @@ -0,0 +1,60 @@ +using SIM.ContainerInstaller.DockerCompose.Model; +using System.Collections.Generic; +using YamlDotNet.Serialization; +using System.IO; +using Sitecore.Diagnostics.Base; +using YamlDotNet.Serialization.NamingConventions; + +namespace SIM.ContainerInstaller.DockerCompose +{ + public class DockerComposeRepository : IRepository + { + private DockerComposeModel _dockerCompose; + private readonly string _pathToComposeFile; + + private DockerComposeModel DockerCompose + { + get + { + if (_dockerCompose == null) + { + _dockerCompose = DeserializeFile(_pathToComposeFile); + } + + return _dockerCompose; + } + } + + public DockerComposeRepository(string pathToComposeFile) + { + this._pathToComposeFile = pathToComposeFile; + } + + protected virtual DockerComposeModel DeserializeFile(string pathToComposeFile) + { + string yamlDocument = File.ReadAllText(pathToComposeFile); + + return DeserializeDocument(yamlDocument); + } + + protected internal virtual DockerComposeModel DeserializeDocument(string ymlDocument) + { + Assert.ArgumentNotNullOrEmpty(ymlDocument, "ymlDocument"); + + StringReader input = new StringReader(ymlDocument); + + IDeserializer deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + var dockerComposeModel = deserializer.Deserialize(input); + + return dockerComposeModel; + } + + public IDictionary GetServices() + { + return DockerCompose.Services; + } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/DockerCompose/IRepository.cs b/src/SIM.ContainerInstaller/DockerCompose/IRepository.cs new file mode 100644 index 00000000..09827409 --- /dev/null +++ b/src/SIM.ContainerInstaller/DockerCompose/IRepository.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SIM.ContainerInstaller.DockerCompose +{ + public interface IRepository + where T : class + { + IDictionary GetServices(); // Get all services + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/DockerCompose/Model/DockerComposeModel.cs b/src/SIM.ContainerInstaller/DockerCompose/Model/DockerComposeModel.cs new file mode 100644 index 00000000..545fc521 --- /dev/null +++ b/src/SIM.ContainerInstaller/DockerCompose/Model/DockerComposeModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using YamlDotNet.Core; +using YamlDotNet.Serialization; + +namespace SIM.ContainerInstaller.DockerCompose.Model +{ + public class DockerComposeModel + { + [YamlMember(ScalarStyle = ScalarStyle.DoubleQuoted)] + public string Version { get; set; } + public Dictionary Services { get; set; } + } +} diff --git a/src/SIM.ContainerInstaller/EnvModel.cs b/src/SIM.ContainerInstaller/EnvModel.cs new file mode 100644 index 00000000..1b062240 --- /dev/null +++ b/src/SIM.ContainerInstaller/EnvModel.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using JetBrains.Annotations; + +namespace SIM.ContainerInstaller +{ + public class EnvModel : IEnumerable + { + private List variables = new List(); + + #region Env variables properties + public string SitecoreRegistry + { + get + { + return this["SITECORE_DOCKER_REGISTRY"]; + } + set + { + this["SITECORE_DOCKER_REGISTRY"] = value; + } + } + public string ProjectName + { + get + { + return this["COMPOSE_PROJECT_NAME"]; + } + set + { + this["COMPOSE_PROJECT_NAME"] = value; + } + } + + public string SitecoreVersion + { + get + { + return this["SITECORE_VERSION"]; + } + set + { + this["SITECORE_VERSION"] = value; + } + } + + public string SitecoreAdminPassword + { + get + { + return this["SITECORE_ADMIN_PASSWORD"]; + } + set + { + this["SITECORE_ADMIN_PASSWORD"] = value; + } + } + + public string SqlAdminPassword + { + get + { + return this["SQL_SA_PASSWORD"]; + } + set + { + this["SQL_SA_PASSWORD"] = value; + } + } + + public string ReportingApiKey + { + get + { + return this["REPORTING_API_KEY"]; + } + set + { + this["REPORTING_API_KEY"] = value; + } + } + + public string TelerikKey + { + get + { + return this["TELERIK_ENCRYPTION_KEY"]; + } + set + { + this["TELERIK_ENCRYPTION_KEY"] = value; + } + } + + public string IdServerSecret + { + get + { + return this["SITECORE_IDSECRET"]; + } + set + { + this["SITECORE_IDSECRET"] = value; + } + } + + public string IdServerCert + { + get + { + return this["SITECORE_ID_CERTIFICATE"]; + } + set + { + this["SITECORE_ID_CERTIFICATE"] = value; + } + } + + public string IdServerCertPassword + { + get + { + return this["SITECORE_ID_CERTIFICATE_PASSWORD"]; + } + set + { + this["SITECORE_ID_CERTIFICATE_PASSWORD"] = value; + } + } + + public string SitecoreLicense + { + get + { + return this["SITECORE_LICENSE"]; + } + set + { + this["SITECORE_LICENSE"] = value; + } + } + + public string CmHost + { + get + { + return this["CM_HOST"]; + } + set + { + this["CM_HOST"] = value; + } + } + + public string CdHost + { + get + { + return this["CD_HOST"]; + } + set + { + this["CD_HOST"] = value; + } + } + + public string IdHost + { + get + { + return this["ID_HOST"]; + } + set + { + this["ID_HOST"] = value; + } + } + #endregion + + public string this[string Name] + { + get + { + NameValuePair pair = this.GetPairOrNull(Name); + if (pair == null) + { + return null; + } + + return pair.Value; + } + set + { + NameValuePair pair = this.GetPairOrNull(Name); + if (pair == null) + { + pair = new NameValuePair(Name, value); + this.variables.Add(pair); + } + else + { + pair.Value = value; + } + } + } + + public IEnumerable GetNames() + { + return this.variables.Select(p => p.Name); + } + + public void SaveToFile(string path) + { + using(StreamWriter writer=new StreamWriter(path)) + { + foreach(NameValuePair pair in this) + { + writer.WriteLine(string.Format("{0}={1}", pair.Name, pair.Value)); + } + } + } + + public static EnvModel LoadFromFile(string path) + { + EnvModel model = new EnvModel(); + using (StreamReader reader = new StreamReader(path)) + { + string line = reader.ReadLine(); + while (!string.IsNullOrEmpty(line)) + { + int num = line.IndexOf('='); + string key = line.Substring(0, num); + string value = line.Substring(num + 1); + model[key] = value; + line = reader.ReadLine(); + } + } + + return model; + } + + public bool KeyExists([NotNull]string key) + { + return this.GetNames().Contains(key); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)variables).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)variables).GetEnumerator(); + } + + private NameValuePair GetPairOrNull(string name) + { + return this.variables.FirstOrDefault(v => v.Name == name); + } + } +} diff --git a/src/SIM.ContainerInstaller/IGenerator.cs b/src/SIM.ContainerInstaller/IGenerator.cs new file mode 100644 index 00000000..4b69a8c8 --- /dev/null +++ b/src/SIM.ContainerInstaller/IGenerator.cs @@ -0,0 +1,7 @@ +namespace SIM.ContainerInstaller +{ + public interface IGenerator + { + string Generate(); + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/IIdentityServerValuesGenerator.cs b/src/SIM.ContainerInstaller/IIdentityServerValuesGenerator.cs new file mode 100644 index 00000000..09c176aa --- /dev/null +++ b/src/SIM.ContainerInstaller/IIdentityServerValuesGenerator.cs @@ -0,0 +1,11 @@ +namespace SIM.ContainerInstaller +{ + public interface IIdentityServerValuesGenerator + { + void Generate(string targetFolder, + out string idSecret, + out string idCertificate, + out string idCertificatePassword + ); + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/ISitecoreLicenseConverter.cs b/src/SIM.ContainerInstaller/ISitecoreLicenseConverter.cs new file mode 100644 index 00000000..557ecbed --- /dev/null +++ b/src/SIM.ContainerInstaller/ISitecoreLicenseConverter.cs @@ -0,0 +1,7 @@ +namespace SIM.ContainerInstaller +{ + public interface ISitecoreLicenseConverter + { + string Convert(string licenseFilePath); + } +} diff --git a/src/SIM.ContainerInstaller/IdentityServerValuesGenerator.cs b/src/SIM.ContainerInstaller/IdentityServerValuesGenerator.cs new file mode 100644 index 00000000..e6f47ee3 --- /dev/null +++ b/src/SIM.ContainerInstaller/IdentityServerValuesGenerator.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Management.Automation; +using JetBrains.Annotations; +using Sitecore.Diagnostics.Base; + +namespace SIM.ContainerInstaller +{ + [UsedImplicitly] + public class IdentityServerValuesGenerator : IIdentityServerValuesGenerator + { + public void Generate([NotNull] string targetFolder, + out string idSecret, + out string idCertificate, + out string idCertificatePassword + ) + { + Assert.ArgumentNotNull(targetFolder, "targetFolder"); + + string scriptFile = Path.Combine(Directory.GetCurrentDirectory(), "ContainerFiles/scripts/IdentityServerScript.txt"); + PSFileExecutor ps = new PSFileExecutor(scriptFile, Path.GetDirectoryName(targetFolder)); + Collection results = ps.Execute(); + + if (results.Count < 3) + { + throw new InvalidOperationException("Error in: IdentityServerVariablesGenerator. PS script has returned less than 3 results."); + } + + idSecret = results[0].ToString(); + idCertificate = results[1].ToString(); + idCertificatePassword = results[2].ToString(); + + Assert.ArgumentNotNullOrEmpty(idSecret, "idSecret"); + Assert.ArgumentNotNullOrEmpty(idCertificate, "idCertificate"); + Assert.ArgumentNotNullOrEmpty(idCertificatePassword, "idCertificatePassword"); + } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/NameValuePair.cs b/src/SIM.ContainerInstaller/NameValuePair.cs new file mode 100644 index 00000000..5ad38655 --- /dev/null +++ b/src/SIM.ContainerInstaller/NameValuePair.cs @@ -0,0 +1,21 @@ +using System; + +namespace SIM.ContainerInstaller +{ + public class NameValuePair + { + public NameValuePair(string name, string value) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Name connot be null or whitespace"); + } + + this.Name = name; + this.Value = value; + } + + public string Name { get; } + public string Value { get; set; } + } +} diff --git a/src/SIM.ContainerInstaller/PSExecutor.cs b/src/SIM.ContainerInstaller/PSExecutor.cs new file mode 100644 index 00000000..b711d7f9 --- /dev/null +++ b/src/SIM.ContainerInstaller/PSExecutor.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.ObjectModel; +using System.Management.Automation; + +namespace SIM.ContainerInstaller +{ + public abstract class PSExecutor + { + private string root; + + public PSExecutor(string executionDir) + { + this.root = executionDir; + } + public Collection Execute() + { + using (PowerShell PowerShellInstance = PowerShell.Create()) + { + PowerShellInstance.AddScript($"cd \"{this.root}\"\n"); + PowerShellInstance.AddScript(this.GetScript()); + Collection result= PowerShellInstance.Invoke(); + + if (PowerShellInstance.Streams.Error.Count > 0) + { + foreach (ErrorRecord error in PowerShellInstance.Streams.Error) + { + PSCmdlet target = error.TargetObject as PSCmdlet; + if (target != null) + { + ActionPreference param = (ActionPreference)target.MyInvocation.BoundParameters["ErrorAction"]; + if (param == ActionPreference.Continue) + { + continue; + } + + throw new AggregateException($"Failed to execute script.\n{error.ToString()}"); + } + + if (error.FullyQualifiedErrorId.Equals("CommandNotFoundException", + StringComparison.InvariantCultureIgnoreCase)) + { + throw new CommandNotFoundException(error.Exception.Message); + } + } + } + + return result; + } + } + + public abstract string GetScript(); + } +} diff --git a/src/SIM.ContainerInstaller/PSFileExecutor.cs b/src/SIM.ContainerInstaller/PSFileExecutor.cs new file mode 100644 index 00000000..5165444a --- /dev/null +++ b/src/SIM.ContainerInstaller/PSFileExecutor.cs @@ -0,0 +1,21 @@ +using System.IO; + +namespace SIM.ContainerInstaller +{ + public class PSFileExecutor:PSExecutor + { + private string script; + public PSFileExecutor(string scriptFile, string executionDir) : base(executionDir) + { + using(StreamReader sr= new StreamReader(scriptFile)) + { + this.script = sr.ReadToEnd(); + } + } + + public override string GetScript() + { + return this.script; + } + } +} diff --git a/src/SIM.ContainerInstaller/PSScriptExecutor.cs b/src/SIM.ContainerInstaller/PSScriptExecutor.cs new file mode 100644 index 00000000..4c5531fc --- /dev/null +++ b/src/SIM.ContainerInstaller/PSScriptExecutor.cs @@ -0,0 +1,20 @@ +using Sitecore.Diagnostics.Base; + +namespace SIM.ContainerInstaller +{ + public class PSScriptExecutor : PSExecutor + { + private readonly string _script; + public PSScriptExecutor(string executionDir, string script) : base(executionDir) + { + Assert.ArgumentNotNullOrEmpty(script, "script"); + + this._script = script; + } + + public override string GetScript() + { + return this._script; + } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/Properties/AssemblyInfo.cs b/src/SIM.ContainerInstaller/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..bd4cf10b --- /dev/null +++ b/src/SIM.ContainerInstaller/Properties/AssemblyInfo.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SIM.ContainerInstaller")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Sitecore Corporation")] +[assembly: AssemblyProduct("Sitecore Instance Manager")] +[assembly: AssemblyCopyright("Copyright © 2001-2020 Sitecore Corporation")] +[assembly: AssemblyTrademark("Sitecore® is a registered trademark of Sitecore Corporation")] +[assembly: AssemblyCulture("")] +[assembly: InternalsVisibleTo("SIM.Tests")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d9ad90f9-bf6d-4c46-8546-3bed9ff93450")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/SIM.ContainerInstaller/ReportingApiKeyGenerator.cs b/src/SIM.ContainerInstaller/ReportingApiKeyGenerator.cs new file mode 100644 index 00000000..c8e0d4cd --- /dev/null +++ b/src/SIM.ContainerInstaller/ReportingApiKeyGenerator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Management.Automation; +using JetBrains.Annotations; + +namespace SIM.ContainerInstaller +{ + [UsedImplicitly] + public class ReportingApiKeyGenerator : IGenerator + { + public string Generate() + { + string scriptFile = Path.Combine(Directory.GetCurrentDirectory(), "ContainerFiles/scripts/ReportingApiKeyGenerator.txt"); + PSFileExecutor ps = new PSFileExecutor(scriptFile, Directory.GetCurrentDirectory()); + Collection results = ps.Execute(); + + if (results.Count < 1) + { + throw new InvalidOperationException("Error in: ReportingApiKeyGenerator. PS script has returned less than 1 result."); + } + + return results[0].ToString(); + } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/Repositories/TagRepository/GitHubTagRepository.cs b/src/SIM.ContainerInstaller/Repositories/TagRepository/GitHubTagRepository.cs new file mode 100644 index 00000000..6f8799ee --- /dev/null +++ b/src/SIM.ContainerInstaller/Repositories/TagRepository/GitHubTagRepository.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using SIM.ContainerInstaller.Repositories.TagRepository.Models; +using SIM.ContainerInstaller.Repositories.TagRepository.Parsers; + +namespace SIM.ContainerInstaller.Repositories.TagRepository +{ + public class GitHubTagRepository : ITagRepository + { + private readonly string _shortTagPattern = @"^([0-9]+.[0-9]+.[0-9]+|[0-9]+.[0-9]+)-[^-]+$"; // tag examples: "10.0.0-ltsc2019", "10.0-2004" + + private IEnumerable _sitecoreTagsEntities; + + private IEnumerable SitecoreTagsEntities => + _sitecoreTagsEntities ?? (_sitecoreTagsEntities = + new SitecoreTagsParser().GetTagsEntities()); + + private static GitHubTagRepository _instance; + + public static GitHubTagRepository GetInstance() + { + return _instance ?? (_instance = new GitHubTagRepository()); + } + + public IEnumerable GetTags() + { + return SitecoreTagsEntities?.Select(entity => entity.Tags) + .SelectMany(tags => tags.Select(tag => tag.Tag)); + } + + private IEnumerable GetSitecoreTags(string sitecoreVersionParam, string namespaceParam) + { + return SitecoreTagsEntities?.Where(entity => entity.Namespace == namespaceParam) + .Select(entity => entity.Tags).SelectMany(tags => tags.Select(tag => tag.Tag)) + .Where(t => t.StartsWith(sitecoreVersionParam)); + } + + public IEnumerable GetSortedShortSitecoreTags(string sitecoreVersionParam, string namespaceParam) + { + IEnumerable tags = this.GetSitecoreTags(sitecoreVersionParam, namespaceParam); + if (tags != null) + { + Regex regex = new Regex(_shortTagPattern); + return tags.Where(tag => regex.IsMatch(tag)).Distinct().OrderBy(tag => tag); + } + + return new List(); + } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/Repositories/TagRepository/ITagRepository.cs b/src/SIM.ContainerInstaller/Repositories/TagRepository/ITagRepository.cs new file mode 100644 index 00000000..35a6f6d5 --- /dev/null +++ b/src/SIM.ContainerInstaller/Repositories/TagRepository/ITagRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace SIM.ContainerInstaller.Repositories.TagRepository +{ + public interface ITagRepository + { + IEnumerable GetTags(); + + IEnumerable GetSortedShortSitecoreTags(string sitecoreVersionParam, string namespaceParam); + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/Repositories/TagRepository/Models/SitecoreTagsEntity.cs b/src/SIM.ContainerInstaller/Repositories/TagRepository/Models/SitecoreTagsEntity.cs new file mode 100644 index 00000000..aaada196 --- /dev/null +++ b/src/SIM.ContainerInstaller/Repositories/TagRepository/Models/SitecoreTagsEntity.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace SIM.ContainerInstaller.Repositories.TagRepository.Models +{ + public class SitecoreTagsEntity + { + public string Name { get; set; } + + public string Namespace { get; set; } + + public IEnumerable Tags { get; set; } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/Repositories/TagRepository/Models/SitecoreTagsHashEntity.cs b/src/SIM.ContainerInstaller/Repositories/TagRepository/Models/SitecoreTagsHashEntity.cs new file mode 100644 index 00000000..e3e741b0 --- /dev/null +++ b/src/SIM.ContainerInstaller/Repositories/TagRepository/Models/SitecoreTagsHashEntity.cs @@ -0,0 +1,11 @@ +using System; + +namespace SIM.ContainerInstaller.Repositories.TagRepository.Models +{ + public class SitecoreTagsHashEntity + { + public DateTime CheckingDate { get; set; } + + public string Hash { get; set; } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/Repositories/TagRepository/Models/TagEntity.cs b/src/SIM.ContainerInstaller/Repositories/TagRepository/Models/TagEntity.cs new file mode 100644 index 00000000..76996009 --- /dev/null +++ b/src/SIM.ContainerInstaller/Repositories/TagRepository/Models/TagEntity.cs @@ -0,0 +1,23 @@ +using System; + +namespace SIM.ContainerInstaller.Repositories.TagRepository.Models +{ + public class TagEntity + { + public string Tag { get; set; } + + public string Digest { get; set; } + + public string Architecture { get; set; } + + public string OS { get; set; } + + public DateTime CreatedTime { get; set; } + + public DateTime LastUpdateTime { get; set; } + + public string OSVersion { get; set; } + + public string TargetOS { get; set; } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/Repositories/TagRepository/Parsers/SitecoreTagsParser.cs b/src/SIM.ContainerInstaller/Repositories/TagRepository/Parsers/SitecoreTagsParser.cs new file mode 100644 index 00000000..ce7f38c8 --- /dev/null +++ b/src/SIM.ContainerInstaller/Repositories/TagRepository/Parsers/SitecoreTagsParser.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using SIM.ContainerInstaller.Repositories.TagRepository.Models; +using Newtonsoft.Json; +using Sitecore.Diagnostics.Logging; + +namespace SIM.ContainerInstaller.Repositories.TagRepository.Parsers +{ + public class SitecoreTagsParser + { + private const string SitecoreTagsPath = "https://raw.githubusercontent.com/Sitecore/docker-images/master/tags/sitecore-tags.json"; + + private readonly int _SitecoreTagsHashFileCheckingDays = 1; + + private string SitecoreTagsFilePath => Path.Combine(ApplicationManager.TempFolder, "sitecore-tags.json"); + + private string SitecoreTagsHashFilePath => Path.Combine(ApplicationManager.TempFolder, "sitecore-tags-hash.json"); + + public SitecoreTagsParser() + { + } + + public IEnumerable GetTagsEntities() + { + return JsonConvert.DeserializeObject>(this.GetTagsData()); + } + + public string GetTagsData() + { + string json; + + if (!this.IsLocalTagsFileValid()) + { + json = this.DownloadSitecoreTagsFile(); + if (this.IsValidJson(json)) + { + File.WriteAllText(this.SitecoreTagsFilePath, json); + SitecoreTagsHashEntity sitecoreTagsHashEntity = new SitecoreTagsHashEntity + { + CheckingDate = DateTime.Now, + Hash = this.GenerateFileHash(json) + }; + File.WriteAllText(this.SitecoreTagsHashFilePath, JsonConvert.SerializeObject(sitecoreTagsHashEntity)); + } + } + else + { + using (StreamReader streamReader = new StreamReader(this.SitecoreTagsFilePath)) + { + json = streamReader.ReadToEnd(); + } + } + + return json; + } + + private bool IsLocalTagsFileValid() + { + if (File.Exists(this.SitecoreTagsFilePath) && File.Exists(this.SitecoreTagsHashFilePath)) + { + string data; + using (StreamReader streamReader = new StreamReader(this.SitecoreTagsHashFilePath)) + { + data = streamReader.ReadToEnd(); + } + + SitecoreTagsHashEntity sitecoreTagsHashEntity = JsonConvert.DeserializeObject(data); + if (sitecoreTagsHashEntity != null) + { + if ((DateTime.Now - sitecoreTagsHashEntity.CheckingDate).TotalDays > _SitecoreTagsHashFileCheckingDays) + { + data = this.DownloadSitecoreTagsFile(); + if (!string.IsNullOrEmpty(data)) + { + if (this.GenerateFileHash(data) == sitecoreTagsHashEntity.Hash) + { + return true; + } + + return false; + } + } + + return true; + } + } + + return false; + } + + private bool IsValidJson(string json) + { + if (!string.IsNullOrEmpty(json)) + { + IEnumerable sitecoreTagsEntities = JsonConvert.DeserializeObject>(json); + if (sitecoreTagsEntities != null && sitecoreTagsEntities.Any()) + { + return true; + } + } + + return false; + } + + private string GenerateFileHash(string data) + { + var md5 = MD5.Create(); + var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(data)); + return Convert.ToBase64String(hash); + } + + private string DownloadSitecoreTagsFile() + { + try + { + return new WebClient().DownloadString(SitecoreTagsPath); + } + catch (Exception ex) + { + Log.Error(ex, string.Format("The '{0}' error occurred during getting the list of tags:{1}{2}", + ex.Message, Environment.NewLine, ex.StackTrace)); + + return null; + } + } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/SIM.ContainerInstaller.csproj b/src/SIM.ContainerInstaller/SIM.ContainerInstaller.csproj new file mode 100644 index 00000000..7becbd1c --- /dev/null +++ b/src/SIM.ContainerInstaller/SIM.ContainerInstaller.csproj @@ -0,0 +1,105 @@ + + + + + Debug + AnyCPU + {D9AD90F9-BF6D-4C46-8546-3BED9FF93450} + Library + Properties + SIM.ContainerInstaller + SIM.ContainerInstaller + v4.7.1 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll + + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Sitecore.Diagnostics.Base.2.0.1.171\lib\Sitecore.Diagnostics.Base.dll + + + ..\packages\Sitecore.Diagnostics.Logging.2.0.1.171\lib\Sitecore.Diagnostics.Logging.dll + + + + + False + ..\..\..\..\Windows\assembly\GAC_MSIL\System.Management.Automation\1.0.0.0__31bf3856ad364e35\System.Management.Automation.dll + + + + + + + + + ..\packages\YamlDotNet.8.1.2\lib\net45\YamlDotNet.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {ca9339a0-9a7d-4900-839e-f21b7269bdaa} + SIM.Base + + + {5c870ab3-89f2-4e24-ae5d-3593ec816495} + SIM.Sitecore9Installer + + + + + + + + + + \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/SitecoreLicenseCoverter.cs b/src/SIM.ContainerInstaller/SitecoreLicenseCoverter.cs new file mode 100644 index 00000000..414cc50b --- /dev/null +++ b/src/SIM.ContainerInstaller/SitecoreLicenseCoverter.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; +using System.IO; +using System.Management.Automation; + +namespace SIM.ContainerInstaller +{ + public class SitecoreLicenseCoverter:ISitecoreLicenseConverter + { + public string Convert(string licenseFilePath) + { + string scriptfile = Path.Combine(Directory.GetCurrentDirectory(), "ContainerFiles/scripts/LicenseScript.txt"); + PSFileExecutor ps = new PSFileExecutor(scriptfile,Path.GetDirectoryName(licenseFilePath)); + Collection results= ps.Execute(); + return results[0].ToString(); + } + } +} diff --git a/src/SIM.ContainerInstaller/SqlAdminPasswordGenerator.cs b/src/SIM.ContainerInstaller/SqlAdminPasswordGenerator.cs new file mode 100644 index 00000000..036e3002 --- /dev/null +++ b/src/SIM.ContainerInstaller/SqlAdminPasswordGenerator.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Management.Automation; + +namespace SIM.ContainerInstaller +{ + public class SqlAdminPasswordGenerator: IGenerator + { + public string Generate() + { + string scriptFile = Path.Combine(Directory.GetCurrentDirectory(), "ContainerFiles/scripts/SqlAdminPasswordGenerator.txt"); + PSFileExecutor ps = new PSFileExecutor(scriptFile, Directory.GetCurrentDirectory()); + Collection results = ps.Execute(); + + if (results.Count < 1) + { + throw new InvalidOperationException("Error in: SqlAdminPasswordGenerator. PS script has returned less than 1 result."); + } + + return results[0].ToString(); + } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/TelerikKeyGenerator.cs b/src/SIM.ContainerInstaller/TelerikKeyGenerator.cs new file mode 100644 index 00000000..6e6e627f --- /dev/null +++ b/src/SIM.ContainerInstaller/TelerikKeyGenerator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Management.Automation; +using JetBrains.Annotations; + +namespace SIM.ContainerInstaller +{ + [UsedImplicitly] + public class TelerikKeyGenerator : IGenerator + { + public string Generate() + { + string scriptFile = Path.Combine(Directory.GetCurrentDirectory(), "ContainerFiles/scripts/TelerikKeyGeneratorScript.txt"); + PSFileExecutor ps = new PSFileExecutor(scriptFile, Directory.GetCurrentDirectory()); + Collection results = ps.Execute(); + + if (results.Count < 1) + { + throw new InvalidOperationException("Error in: TelerikKeyGenerator. PS script has returned less than 1 result."); + } + + return results[0].ToString(); + } + } +} \ No newline at end of file diff --git a/src/SIM.ContainerInstaller/packages.config b/src/SIM.ContainerInstaller/packages.config new file mode 100644 index 00000000..d8c5daf2 --- /dev/null +++ b/src/SIM.ContainerInstaller/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/SIM.Instances/ContainerizedInstance.cs b/src/SIM.Instances/ContainerizedInstance.cs new file mode 100644 index 00000000..1afe66b1 --- /dev/null +++ b/src/SIM.Instances/ContainerizedInstance.cs @@ -0,0 +1,41 @@ +using JetBrains.Annotations; +using SIM.Products; +using System; + +namespace SIM.Instances +{ + public class ContainerizedInstance : Instance + { + private string _name; + + public ContainerizedInstance() + { + } + + public ContainerizedInstance(string name) + : base(Int32.MaxValue) // Containerized instances are not run on local IIS, we cannot resolve proper site ID, thus 'Int32.MaxValue' is set. + { + this._name = name; + } + + public override string Name => _name; + + public override string DisplayName + { + get { return Name; } + } + + public override string BindingsNames => string.Empty; + + [NotNull] + public override Product Product + { + get { return Product.Undefined; } + } + + public override string WebRootPath + { + get { return this.SitecoreEnvironment.UnInstallDataPath; } + } + } +} \ No newline at end of file diff --git a/src/SIM.Instances/Instance.cs b/src/SIM.Instances/Instance.cs index 3652cc17..259e5c7f 100644 --- a/src/SIM.Instances/Instance.cs +++ b/src/SIM.Instances/Instance.cs @@ -36,7 +36,6 @@ public class Instance : Website, IXmlSerializable public Instance([NotNull] int id) : base(id) { - this.SitecoreEnvironment = SitecoreEnvironmentHelper.GetExistingOrNewSitecoreEnvironment(this.Name); } @@ -261,7 +260,10 @@ public virtual string PackagesFolderPath [NotNull] - public virtual SitecoreEnvironment SitecoreEnvironment { get; } + public virtual SitecoreEnvironment SitecoreEnvironment + { + get { return SitecoreEnvironmentHelper.GetExistingOrNewSitecoreEnvironment(this.Name); } + } [NotNull] public virtual Product Product @@ -468,6 +470,11 @@ public InstanceType Type { get { + if (SitecoreEnvironmentHelper.GetExistingSitecoreEnvironment(this.Name)?.EnvType == SitecoreEnvironment.EnvironmentType.Container) + { + return InstanceType.SitecoreContainer; + } + if (Product == Product.Undefined || Product.Release == null) { return InstanceType.SitecoreMember; @@ -492,6 +499,7 @@ public enum InstanceType Sitecore8AndEarlier, Sitecore9AndLater, SitecoreMember, + SitecoreContainer, Unknown } diff --git a/src/SIM.Instances/InstanceManager.cs b/src/SIM.Instances/InstanceManager.cs index 92b2e667..6c2c6254 100644 --- a/src/SIM.Instances/InstanceManager.cs +++ b/src/SIM.Instances/InstanceManager.cs @@ -1,9 +1,4 @@ -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using SIM.Extensions; - -namespace SIM.Instances +namespace SIM.Instances { using System; using System.Collections.Generic; @@ -15,7 +10,6 @@ namespace SIM.Instances using Sitecore.Diagnostics.Base.Extensions.EnumerableExtensions; using Sitecore.Diagnostics.Logging; using SIM.SitecoreEnvironments; - using System.Collections.ObjectModel; #region @@ -23,9 +17,6 @@ namespace SIM.Instances public class InstanceManager { - public InstanceManager() - { - } #region Fields private IDictionary _CachedInstances; @@ -76,7 +67,7 @@ public IDictionary PartiallyCachedInstances } _CachedInstances = new Dictionary(); - Instances.ForEach(x => { _CachedInstances.Add(x.Name, new PartiallyCachedInstance((int) x.ID)); }); + Instances.ForEach(x => { _CachedInstances.Add(x.Name, new PartiallyCachedInstance((int)x.ID)); }); return _CachedInstances; } @@ -100,14 +91,11 @@ private set public static InstanceManager Default { get; } = new InstanceManager(); - public RangeObservableCollection InstancesObservableCollection { get; } = new RangeObservableCollection(); - - #endregion #endregion - #region Methods + #region Public Methods #region Public methods @@ -134,17 +122,49 @@ public void Initialize([CanBeNull] string defaultRootFolder = null) { SitecoreEnvironmentHelper.RefreshEnvironments(); + List instances = new List(); + + if(ApplicationManager.IsIisRunning) {instances.AddRange(GetIISInstances());} + + if(ApplicationManager.IsDockerRunning) {instances.AddRange(GetContainerizedInstances());} + + Dictionary partiallyCachedInstances = instances.ToDictionary(i => i.Name); + + PartiallyCachedInstances = partiallyCachedInstances; + + Instances = instances; + } + + private IEnumerable GetContainerizedInstances() + { + List environments = + SitecoreEnvironmentHelper.SitecoreEnvironments. + Where(e => e.EnvType == SitecoreEnvironment.EnvironmentType.Container).ToList(); + + List instances = environments.SelectMany(e => e.Members).Select(m => MemberToInstance(m)).ToList(); + + return instances; + } + + private Instance MemberToInstance(SitecoreEnvironmentMember member) + { + Instance instance = new ContainerizedInstance(member.Name); + + return instance; + } + + private IEnumerable GetIISInstances([CanBeNull] string defaultRootFolder = null) + { using (WebServerManager.WebServerContext context = WebServerManager.CreateContext()) { ProfileSection.Argument("defaultRootFolder", defaultRootFolder); IEnumerable sites = GetOperableSites(context, defaultRootFolder); - PartiallyCachedInstances = GetPartiallyCachedInstances(sites); - Instances = GetInstances(); + + return GetPartiallyCachedInstances(sites); } } - - + public void InitializeWithSoftListRefresh([CanBeNull] string defaultRootFolder = null) { SitecoreEnvironmentHelper.RefreshEnvironments(); @@ -167,62 +187,8 @@ public void InitializeWithSoftListRefresh([CanBeNull] string defaultRootFolder = GetPartiallyCachedInstance(site)) .Where(IsSitecoreOrSitecoreEnvironmentMember) .Where(IsNotHidden).ToDictionary(value => value.Name); - - Instances = PartiallyCachedInstances?.Values.Select(x => GetInstance(x.ID)).ToArray(); - } - } - } - - public Instance GetInstance(long id) - { - using (new ProfileSection("Get instance by id")) - { - ProfileSection.Argument("id", id); - - var instance = new Instance((int)id); - - return ProfileSection.Result(instance); - } - } - public void AddNewInstance(string instanceName, bool isSitecore8Instance) - { - using (WebServerManager.WebServerContext context = WebServerManager.CreateContext()) - { - SiteCollection sites = context.Sites; - List instances = new List(); - - if (isSitecore8Instance) - { - - Site site = sites.FirstOrDefault(s => s.Name == instanceName); - if (site != null) - { - instances.Add(new Instance((int)site.Id)); - } - } - else - { - SitecoreEnvironment environment = - SitecoreEnvironmentHelper.SitecoreEnvironments.FirstOrDefault(e => e.Name == instanceName); - if (environment != null && environment.Members != null && environment.Members.Count != 0) - { - foreach (var member in environment.Members.Where(x=>x.Type== SitecoreEnvironmentMember.Types.Site.ToString())) - { - Site site = sites.FirstOrDefault(s => s.Name == member.Name); - if (site != null) - { - instances.Add(new Instance((int)site.Id)); - } - } - } - } - - foreach (var instance in instances) - { - this.InstancesObservableCollection.Add(instance); - this.PartiallyCachedInstances?.Add(instance.Name, instance); - this.Instances = this.Instances.Add(instance); + Instances = PartiallyCachedInstances?.Values.Select(x => GetInstance(x.ID)).ToArray(); } } } @@ -241,20 +207,26 @@ private IEnumerable GetInstances() } } - private IDictionary GetPartiallyCachedInstances(IEnumerable sites) + private IEnumerable GetPartiallyCachedInstances(IEnumerable sites) { using (new ProfileSection("Getting partially cached Sitecore instances")) { ProfileSection.Argument("sites", sites); - IDictionary partiallyCachedInstances = sites.Select(GetPartiallyCachedInstance) + IEnumerable partiallyCachedInstances = sites.Select(GetPartiallyCachedInstance) .Where(IsSitecoreOrSitecoreEnvironmentMember) - .Where(IsNotHidden).ToDictionary(value => value.Name); + .Where(IsNotHidden); return partiallyCachedInstances; } } + #endregion + + #endregion + + #region Methods + private IEnumerable GetOperableSites([NotNull] WebServerManager.WebServerContext context, [CanBeNull] string defaultRootFolder = null) { Assert.IsNotNull(context, "Context cannot be null"); @@ -305,6 +277,20 @@ private void OnInstancesListUpdated() #endregion + #region Public methods + + public Instance GetInstance(long id) + { + using (new ProfileSection("Get instance by id")) + { + ProfileSection.Argument("id", id); + + var instance = new Instance((int)id); + + return ProfileSection.Result(instance); + } + } + #endregion } } diff --git a/src/SIM.Instances/SIM.Instances.csproj b/src/SIM.Instances/SIM.Instances.csproj index 6a53aaec..47d6923c 100644 --- a/src/SIM.Instances/SIM.Instances.csproj +++ b/src/SIM.Instances/SIM.Instances.csproj @@ -76,6 +76,7 @@ + diff --git a/src/SIM.Pipelines/BaseProcessors/RunCMDCommandBaseProcessor.cs b/src/SIM.Pipelines/BaseProcessors/RunCMDCommandBaseProcessor.cs new file mode 100644 index 00000000..b18a7dc6 --- /dev/null +++ b/src/SIM.Pipelines/BaseProcessors/RunCMDCommandBaseProcessor.cs @@ -0,0 +1,61 @@ +using JetBrains.Annotations; +using System; +using SIM.Loggers; +using SIM.Pipelines.Processors; + +namespace SIM.Pipelines.BaseProcessors +{ + public abstract class RunCmdCommandBaseProcessor : Processor + { + protected ILogger _logger; + + protected abstract string GetCommand(ProcessorArgs procArgs); + + protected abstract string GetExecutionFolder(ProcessorArgs procArgs); + + protected virtual ILogger GetLogger(ProcessorArgs procArgs) + { + return new EmptyLogger(); + } + + protected override void Process([NotNull] ProcessorArgs procArgs) + { + string command = GetCommand(procArgs); + + string executionFolder = GetExecutionFolder(procArgs); + + this._logger = GetLogger(procArgs); + + RunCommand(executionFolder, command); + } + + private void RunCommand(string executionFolder, string command) + { + string strCmdText = $"/C cd \"{executionFolder}\"&{command}"; + System.Diagnostics.Process proc = new System.Diagnostics.Process(); + proc.StartInfo.Arguments = strCmdText; + proc.StartInfo.FileName = "CMD.exe"; + proc.StartInfo.UseShellExecute = false; + proc.StartInfo.RedirectStandardError = true; + proc.StartInfo.RedirectStandardOutput = true; + proc.OutputDataReceived += Proc_DataReceived; + proc.ErrorDataReceived += Proc_DataReceived; + proc.Start(); + proc.BeginOutputReadLine(); + proc.BeginErrorReadLine(); + proc.WaitForExit(); + if (proc.ExitCode != 0) + { + throw new AggregateException($"Failed to run '{command}' in '{executionFolder}'\n{proc.StandardError.ReadToEnd()}"); + } + } + + private void Proc_DataReceived(object sender, System.Diagnostics.DataReceivedEventArgs e) + { + if (!string.IsNullOrEmpty(e.Data)) + { + this._logger.Info(e.Data, false); + } + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Delete/Containers/CleanupEnvironmentData.cs b/src/SIM.Pipelines/Delete/Containers/CleanupEnvironmentData.cs new file mode 100644 index 00000000..96e8993d --- /dev/null +++ b/src/SIM.Pipelines/Delete/Containers/CleanupEnvironmentData.cs @@ -0,0 +1,35 @@ +using System; +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; +using SIM.SitecoreEnvironments; +using SIM.Pipelines.Install.Containers; + +namespace SIM.Pipelines.Delete.Containers +{ + [UsedImplicitly] + public class CleanupEnvironmentData : Processor + { + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, "arguments"); + + DeleteContainersArgs args = (DeleteContainersArgs)arguments; + Assert.ArgumentNotNull(args, "args"); + + Guid environmentId = args.EnvironmentId; + + SitecoreEnvironment environment; + if (!SitecoreEnvironmentHelper.TryGetEnvironmentById(environmentId, out environment)) + { + // TODO: log warn message if the env cannot be resolved from the environments.json + return; + } + + SitecoreEnvironmentHelper.SitecoreEnvironments.Remove(environment); + SitecoreEnvironmentHelper.SaveSitecoreEnvironmentData(SitecoreEnvironmentHelper.SitecoreEnvironments); + + return; + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Delete/Containers/DeleteContainersArgs.cs b/src/SIM.Pipelines/Delete/Containers/DeleteContainersArgs.cs new file mode 100644 index 00000000..e6242cfb --- /dev/null +++ b/src/SIM.Pipelines/Delete/Containers/DeleteContainersArgs.cs @@ -0,0 +1,30 @@ +using System; +using SIM.ContainerInstaller; +using SIM.Loggers; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Delete.Containers +{ + public class DeleteContainersArgs : ProcessorArgs + { + public string DestinationFolder { get; } + + public EnvModel Env { get; } + + public Guid EnvironmentId { get; } + + public ILogger Logger { get; } + + public DeleteContainersArgs(string destinationFolder, EnvModel env, Guid environmentId, ILogger logger) + { + Assert.ArgumentNotNullOrEmpty(destinationFolder, "destinationFolder"); + Assert.ArgumentNotNull(env, "env"); + + this.DestinationFolder = destinationFolder; + this.Env = env; + this.EnvironmentId = environmentId; + this.Logger = logger; + } + } +} diff --git a/src/SIM.Pipelines/Delete/Containers/RemoveEnvironmentFolder.cs b/src/SIM.Pipelines/Delete/Containers/RemoveEnvironmentFolder.cs new file mode 100644 index 00000000..5c2dbccb --- /dev/null +++ b/src/SIM.Pipelines/Delete/Containers/RemoveEnvironmentFolder.cs @@ -0,0 +1,21 @@ +using JetBrains.Annotations; +using SIM.Pipelines.Install.Containers; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Delete.Containers +{ + [UsedImplicitly] + public class RemoveEnvironmentFolder : Processor + { + protected override void Process([NotNull] ProcessorArgs args) + { + Assert.ArgumentNotNull(args, "args"); + + DeleteContainersArgs deleteArgs = (DeleteContainersArgs)args; + Assert.ArgumentNotNull(deleteArgs, "deleteArgs"); + + FileSystem.FileSystem.Local.Directory.DeleteIfExists(deleteArgs.DestinationFolder); + } + } +} diff --git a/src/SIM.Pipelines/Delete/Containers/RemoveFromDocker.cs b/src/SIM.Pipelines/Delete/Containers/RemoveFromDocker.cs new file mode 100644 index 00000000..ad1aed1c --- /dev/null +++ b/src/SIM.Pipelines/Delete/Containers/RemoveFromDocker.cs @@ -0,0 +1,30 @@ +using System; +using JetBrains.Annotations; +using SIM.Pipelines.Install.Containers; +using SIM.Pipelines.Processors; + +namespace SIM.Pipelines.Delete.Containers +{ + [UsedImplicitly] + public class RemoveFromDocker : RunCommandBaseProcessor + { + protected override string GetDestination(ProcessorArgs arguments) + { + DeleteContainersArgs args = (DeleteContainersArgs)arguments; + + string destinationFolder = args?.DestinationFolder; + + if (string.IsNullOrEmpty(destinationFolder)) + { + throw new InvalidOperationException($"destinationFolder is null or empty in {this.GetType().Name}"); + } + + return destinationFolder; + } + + protected override string GetCommand(ProcessorArgs arguments) + { + return "docker-compose.exe down"; + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Delete/Containers/RemoveHostsProcessor.cs b/src/SIM.Pipelines/Delete/Containers/RemoveHostsProcessor.cs new file mode 100644 index 00000000..dc7f4151 --- /dev/null +++ b/src/SIM.Pipelines/Delete/Containers/RemoveHostsProcessor.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using SIM.Adapters.WebServer; +using SIM.Pipelines.Install.Containers; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Delete.Containers +{ + [UsedImplicitly] + public class RemoveHostsProcessor : Processor + { + protected void RemoveHosts(IEnumerable hosts) + { + Hosts.Remove(hosts); + } + + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, nameof(arguments)); + + DeleteContainersArgs args = (DeleteContainersArgs)arguments; + Assert.ArgumentNotNull(args, nameof(args)); + + IEnumerable hosts = GetHostNames(args); + + RemoveHosts(hosts); + } + + protected virtual IEnumerable GetHostNames(DeleteContainersArgs args) + { + IEnumerable hosts = args.Env.Where(nvp => nvp.Name.EndsWith("_HOST")).Select(nvp => nvp.Value); + + return hosts; + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Install/Containers/AddHostsProcessor.cs b/src/SIM.Pipelines/Install/Containers/AddHostsProcessor.cs new file mode 100644 index 00000000..abe90dcd --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/AddHostsProcessor.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using SIM.Adapters.WebServer; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Install.Containers +{ + [UsedImplicitly] + public class AddHostsProcessor: Processor + { + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, nameof(arguments)); + + InstallContainerArgs args = (InstallContainerArgs)arguments; + + Assert.ArgumentNotNull(args, nameof(args)); + + IEnumerable hosts = GetHostNames(args); + + DoUpdate(hosts); + } + + protected virtual void DoUpdate(IEnumerable hosts) + { + foreach (var hostName in hosts) + { + Hosts.Append(hostName); + } + } + + protected virtual IEnumerable GetHostNames(InstallContainerArgs args) + { + IEnumerable hosts = args.EnvModel.Where(nvp => nvp.Name.EndsWith("_HOST")).Select(nvp => nvp.Value); + + return hosts; + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Install/Containers/ConvertLicenseProcessor.cs b/src/SIM.Pipelines/Install/Containers/ConvertLicenseProcessor.cs new file mode 100644 index 00000000..907630da --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/ConvertLicenseProcessor.cs @@ -0,0 +1,26 @@ +using SIM.ContainerInstaller; +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SIM.Pipelines.Install.Containers +{ + public class ConvertLicenseProcessor : Processor + { + private ISitecoreLicenseConverter converter = new SitecoreLicenseCoverter(); + protected override void Process([NotNull] ProcessorArgs arguments) + { + InstallContainerArgs args = (InstallContainerArgs)arguments; + string licensePath = Path.Combine(args.Destination, "license.xml"); + if (FileSystem.FileSystem.Local.File.Exists(licensePath)) + { + args.EnvModel.SitecoreLicense = this.converter.Convert(licensePath); + } + } + } +} diff --git a/src/SIM.Pipelines/Install/Containers/CopyFilesToDestination.cs b/src/SIM.Pipelines/Install/Containers/CopyFilesToDestination.cs new file mode 100644 index 00000000..b4206ccf --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/CopyFilesToDestination.cs @@ -0,0 +1,20 @@ +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using System.IO; + +namespace SIM.Pipelines.Install.Containers +{ + public class CopyFilesToDestination : Processor + { + protected override void Process([NotNull] ProcessorArgs arguments) + { + InstallContainerArgs args = (InstallContainerArgs)arguments; + SIM.FileSystem.FileSystem.Local.Directory.Copy(args.FilesRoot, args.Destination, true); + if (FileSystem.FileSystem.Local.File.Exists(args.EnvModel.SitecoreLicense)) + { + string licensePath = Path.Combine(args.Destination, "license.xml"); + FileSystem.FileSystem.Local.File.Copy(args.EnvModel.SitecoreLicense, licensePath); + } + } + } +} diff --git a/src/SIM.Pipelines/Install/Containers/GenerateCertificatesProcessor.cs b/src/SIM.Pipelines/Install/Containers/GenerateCertificatesProcessor.cs new file mode 100644 index 00000000..9991594d --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/GenerateCertificatesProcessor.cs @@ -0,0 +1,186 @@ +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; +using System; +using SIM.ContainerInstaller; +using System.Collections.ObjectModel; +using System.IO; +using System.Management.Automation; +using SIM.Loggers; + +namespace SIM.Pipelines.Install.Containers +{ + [UsedImplicitly] + public class GenerateCertificatesProcessor : Processor + { + private string ProcessorName => this.GetType().Name; + + private ILogger _logger; + + [NotNull] + private ILogger Logger + { + get + { + if (_logger == null) + { + _logger = new EmptyLogger(); + } + + return _logger; + } + set + { + _logger = value; + } + } + + private const string PathToCertFolder = "traefik\\certs"; + + private const string PathToDynamicConfigFolder = "traefik\\config\\dynamic"; + + private const string CertsConfigFileName = "certs_config.yaml"; + + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, "arguments"); + + InstallContainerArgs args = (InstallContainerArgs)arguments; + + this._logger = args.Logger; + + Assert.ArgumentNotNull(args, "args"); + + string destinationFolder = args.Destination; + + Assert.ArgumentNotNullOrEmpty(destinationFolder, "destinationFolder"); + + UpdateTlsDynamicConfig(args); + + string script = GetScript(args); + + PSExecutor ps = new PSScriptExecutor(destinationFolder, script); + + ExecuteScript(() => ps.Execute()); + } + + private void UpdateTlsDynamicConfig(InstallContainerArgs args) + { + string yamlContent = GetConfig(args); + + string yamlFileName = Path.Combine(args.Destination, PathToDynamicConfigFolder, CertsConfigFileName); + + try + { + UpdateConfigFile(yamlFileName, yamlContent); + } + catch (Exception ex) + { + args.Logger.Error($"Could not update the '{CertsConfigFileName}' file. Message: {ex.Message}"); + + throw; + } + } + + private string GetConfig(InstallContainerArgs args) + { + Topology topology = args.Topology; + + string pathToCerts = @"C:\etc\traefik\certs"; + + switch (topology) + { + case Topology.Xm1: + case Topology.Xp1: + return $@"tls: + certificates: + - certFile: {pathToCerts}\{args.EnvModel.CmHost}.crt + keyFile: {pathToCerts}\{args.EnvModel.CmHost}.key + - certFile: {pathToCerts}\{args.EnvModel.CdHost}.crt + keyFile: {pathToCerts}\{args.EnvModel.CdHost}.key + - certFile: {pathToCerts}\{args.EnvModel.IdHost}.crt + keyFile: {pathToCerts}\{args.EnvModel.IdHost}.key +"; + case Topology.Xp0: + return $@"tls: + certificates: + - certFile: {pathToCerts}\{args.EnvModel.CmHost}.crt + keyFile: {pathToCerts}\{args.EnvModel.CmHost}.key + - certFile: {pathToCerts}\{args.EnvModel.IdHost}.crt + keyFile: {pathToCerts}\{args.EnvModel.IdHost}.key +"; + default: + throw new InvalidOperationException("Config is not defined for '" + topology.ToString() + "' topology."); + } + } + + private void UpdateConfigFile(string fileName, string content) + { + File.WriteAllText(fileName, content); + } + + protected virtual string GetScript(InstallContainerArgs args) + { + Topology topology = args.Topology; + + switch (topology) + { + case Topology.Xm1: + return GetXm1Script(args.EnvModel.CmHost, args.EnvModel.CdHost, args.EnvModel.IdHost); + case Topology.Xp0: + return GetXp0Script(args.EnvModel.CmHost, args.EnvModel.IdHost); + case Topology.Xp1: + return GetXp1Script(args.EnvModel.CmHost, args.EnvModel.CdHost, args.EnvModel.IdHost); + default: + throw new InvalidOperationException("Generate certificates script cannot be resolved for '" + topology.ToString() + "'"); + } + } + + private void ExecuteScript(Func> p) + { + try + { + p.Invoke(); + } + catch (CommandNotFoundException ex) + { + this._logger.Error($"Failed to generate certificates in '{this.ProcessorName}'. Message: {ex.Message}"); + + this._logger.Info( + $"{this.ProcessorName}: please make sure that 'mkcert.exe' has been installed. See 'https://containers.doc.sitecore.com/docs/run-sitecore#install-mkcert' for additional details."); + + throw; + } + catch (Exception ex) + { + this._logger.Error($"Failed to generate certificates in '{this.ProcessorName}'. Message: {ex.Message}"); + + throw; + } + } + + private string GetXp1Script(string cmHost, string cdHost, string idHost) + { + return GetXm1Script(cmHost, cdHost, idHost); + } + + private string GetXp0Script(string cmHost, string idHost) + { + string template = @" +mkcert -cert-file {0}\{1}.crt -key-file {0}\{1}.key ""{1}"" +mkcert -cert-file {0}\{2}.crt -key-file {0}\{2}.key ""{2}"""; + + return string.Format(template, PathToCertFolder, cmHost, idHost); + } + + private string GetXm1Script(string cmHost, string cdHost, string idHost) + { + string template = @" +mkcert -cert-file {0}\{1}.crt -key-file {0}\{1}.key ""{1}"" +mkcert -cert-file {0}\{2}.crt -key-file {0}\{2}.key ""{2}"" +mkcert -cert-file {0}\{3}.crt -key-file {0}\{3}.key ""{3}"""; + + return string.Format(template, PathToCertFolder, cmHost, idHost, cdHost); + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Install/Containers/GenerateEnvironmentData.cs b/src/SIM.Pipelines/Install/Containers/GenerateEnvironmentData.cs new file mode 100644 index 00000000..d0458549 --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/GenerateEnvironmentData.cs @@ -0,0 +1,129 @@ +using System; +using System.IO; +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; +using SIM.SitecoreEnvironments; +using System.Collections.Generic; +using System.Linq; +using MongoDB.Bson.Serialization.Conventions; +using SIM.ContainerInstaller.DockerCompose; +using SIM.ContainerInstaller.DockerCompose.Model; + +namespace SIM.Pipelines.Install.Containers +{ + [UsedImplicitly] + public class GenerateEnvironmentData : Processor + { + private IList siteTypes => new List() + { + "id", + "cm", + "cd", + "xconnect", + "prc", + "rep", + "xdbcollection", + "xdbsearch" + }; + + private IList serviceTypes => new List() + { + "xdbsearchworker", + "xdbautomationworker", + "cortexprocessingworker", + "xdbautomation", + "xdbautomationrpt", + "cortexprocessing", + "cortexreporting", + "xdbrefdata", + "xdbsearchworker", + "xdbautomationworker", + "cortexprocessingworker" + }; + + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, "ProcessorArgs:arguments"); + + InstallContainerArgs args = (InstallContainerArgs)arguments; + + string destinationFolder = args.Destination; + + Assert.ArgumentNotNullOrEmpty(destinationFolder, "destinationFolder"); + + if (!Directory.Exists(destinationFolder)) + { + throw new InvalidOperationException("'args.Destination' points to non-existing Folder."); + } + + string environmentName = args.EnvModel.ProjectName; + Assert.ArgumentNotNullOrEmpty(environmentName, "projectName"); + + this.AddEnvironment(this.CreateEnvironment(environmentName, destinationFolder)); + } + + private SitecoreEnvironment CreateEnvironment(string environmentName, string destinationFolder) + { + SitecoreEnvironment environment = new SitecoreEnvironment(environmentName, SitecoreEnvironment.EnvironmentType.Container); + environment.UnInstallDataPath = destinationFolder; + environment.Members = GetEnvironmentMembers(environmentName, destinationFolder).ToList(); + + return environment; + } + + private IEnumerable GetEnvironmentMembers( + string environmentName, + string destinationFolder, + string fileName = "docker-compose.yml" + ) + { + string pathToComposeFile = Path.Combine(destinationFolder, fileName); + + IRepository repository = new DockerComposeRepository(pathToComposeFile); + + IDictionary services = repository.GetServices(); + + foreach (var serviceName in services.Keys) + { + string memberType = GetMemberType(serviceName); + + if (!string.IsNullOrEmpty(memberType)) + { + string memberName = $"{environmentName}-{serviceName}"; + + yield return new SitecoreEnvironmentMember(memberName, memberType, isContainer: true); + } + } + } + + private string GetMemberType(string serviceName) + { + if (siteTypes.Contains(serviceName)) + { + return SitecoreEnvironmentMember.Types.Site.ToString(); + } + + if (serviceTypes.Contains(serviceName)) + { + return SitecoreEnvironmentMember.Types.Service.ToString(); + } + + return null; + } + + private void AddEnvironment(SitecoreEnvironment environment) + { + foreach (SitecoreEnvironment se in SitecoreEnvironmentHelper.SitecoreEnvironments) + { + if (se.Name == environment.Name) + { + return; + } + } + + SitecoreEnvironmentHelper.SitecoreEnvironments.Add(environment); + SitecoreEnvironmentHelper.SaveSitecoreEnvironmentData(SitecoreEnvironmentHelper.SitecoreEnvironments); + } + } +} diff --git a/src/SIM.Pipelines/Install/Containers/GenerateIdEnvValuesProcessor.cs b/src/SIM.Pipelines/Install/Containers/GenerateIdEnvValuesProcessor.cs new file mode 100644 index 00000000..2896e6e8 --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/GenerateIdEnvValuesProcessor.cs @@ -0,0 +1,33 @@ +using SIM.ContainerInstaller; +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Install.Containers +{ + [UsedImplicitly] + public class GenerateIdEnvValuesProcessor : Processor + { + private readonly IIdentityServerValuesGenerator _generator = new IdentityServerValuesGenerator(); + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, "arguments"); + + InstallContainerArgs args = (InstallContainerArgs)arguments; + + string idServerSecret; + string idServerCert; + string idServerCertPassword; + + this._generator.Generate(args.Destination, + out idServerSecret, + out idServerCert, + out idServerCertPassword + ); + + args.EnvModel.IdServerSecret = idServerSecret; + args.EnvModel.IdServerCert = idServerCert; + args.EnvModel.IdServerCertPassword = idServerCertPassword; + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Install/Containers/GenerateReportingApiKeyProcessor.cs b/src/SIM.Pipelines/Install/Containers/GenerateReportingApiKeyProcessor.cs new file mode 100644 index 00000000..c340f2a6 --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/GenerateReportingApiKeyProcessor.cs @@ -0,0 +1,26 @@ +using SIM.ContainerInstaller; +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Install.Containers +{ + [UsedImplicitly] + public class GenerateReportingApiKeyProcessor : Processor + { + private const string Key = "REPORTING_API_KEY"; + + private readonly IGenerator _generator = new ReportingApiKeyGenerator(); + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, "arguments"); + + InstallContainerArgs args = (InstallContainerArgs)arguments; + + if (args.EnvModel.KeyExists(Key)) + { + args.EnvModel[Key] = this._generator.Generate(); + } + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Install/Containers/GenerateSqlAdminPasswordProcessor.cs b/src/SIM.Pipelines/Install/Containers/GenerateSqlAdminPasswordProcessor.cs new file mode 100644 index 00000000..e02de3c7 --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/GenerateSqlAdminPasswordProcessor.cs @@ -0,0 +1,24 @@ +using JetBrains.Annotations; +using SIM.ContainerInstaller; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Install.Containers +{ + [UsedImplicitly] + public class GenerateSqlAdminPasswordProcessor : Processor + { + private readonly IGenerator _generator = new SqlAdminPasswordGenerator(); + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, "arguments"); + + InstallContainerArgs args = (InstallContainerArgs)arguments; + + if (string.IsNullOrEmpty(args.EnvModel.SqlAdminPassword)) + { + args.EnvModel.SqlAdminPassword = this._generator.Generate(); + } + } + } +} diff --git a/src/SIM.Pipelines/Install/Containers/GenerateTelerikKeyProcessor.cs b/src/SIM.Pipelines/Install/Containers/GenerateTelerikKeyProcessor.cs new file mode 100644 index 00000000..10952846 --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/GenerateTelerikKeyProcessor.cs @@ -0,0 +1,21 @@ +using SIM.ContainerInstaller; +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Install.Containers +{ + [UsedImplicitly] + public class GenerateTelerikKeyProcessor : Processor + { + private readonly IGenerator _generator = new TelerikKeyGenerator(); + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, "arguments"); + + InstallContainerArgs args = (InstallContainerArgs)arguments; + + args.EnvModel.TelerikKey = this._generator.Generate(); + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Install/Containers/InstallContainerArgs.cs b/src/SIM.Pipelines/Install/Containers/InstallContainerArgs.cs new file mode 100644 index 00000000..8ce7f694 --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/InstallContainerArgs.cs @@ -0,0 +1,45 @@ +using System; +using SIM.ContainerInstaller; +using SIM.Loggers; +using SIM.Pipelines.Processors; + +namespace SIM.Pipelines.Install.Containers +{ + public enum Topology + { + Xm1, + Xp0, + Xp1 + } + + public class InstallContainerArgs : ProcessorArgs + { + public InstallContainerArgs(EnvModel model, string destination, string filesRoot, string topology, ILogger logger) + { + this.EnvModel = model; + this.FilesRoot = filesRoot; + this.Destination = destination; + this.Topology = (Topology)Enum.Parse(typeof(Topology), topology, true); + this.Logger = logger; + } + + public EnvModel EnvModel { get; } + + public string Destination + { + get; set; + } + + public Topology Topology { get; } + + public string FilesRoot + { + get; + } + + public ILogger Logger + { + get; set; + } + } +} diff --git a/src/SIM.Pipelines/Install/Containers/InstallDockerToolsProcessor.cs b/src/SIM.Pipelines/Install/Containers/InstallDockerToolsProcessor.cs new file mode 100644 index 00000000..806b5636 --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/InstallDockerToolsProcessor.cs @@ -0,0 +1,19 @@ +using System.IO; +using SIM.ContainerInstaller; +using JetBrains.Annotations; +using SIM.Pipelines.Processors; + +namespace SIM.Pipelines.Install.Containers +{ + [UsedImplicitly] + public class InstallDockerToolsProcessor : Processor + { + protected override void Process(ProcessorArgs args) + { + string scriptFile = Path.Combine(Directory.GetCurrentDirectory(), "ContainerFiles/scripts/InstallDockerToolsModuleScript.txt"); + PSFileExecutor ps = new PSFileExecutor(scriptFile, Directory.GetCurrentDirectory()); + + ps.Execute(); + } + } +} diff --git a/src/SIM.Pipelines/Install/Containers/RunCommandBaseProcessor.cs b/src/SIM.Pipelines/Install/Containers/RunCommandBaseProcessor.cs new file mode 100644 index 00000000..e0ac899e --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/RunCommandBaseProcessor.cs @@ -0,0 +1,56 @@ +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using System; +using System.IO; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Install.Containers +{ + public abstract class RunCommandBaseProcessor: Processor + { + protected abstract string GetDestination(ProcessorArgs arguments); + protected abstract string GetCommand(ProcessorArgs arguments); + + protected override void Process([NotNull] ProcessorArgs arguments) + { + Assert.ArgumentNotNull(arguments, "arguments"); + + string command = GetCommand(arguments); + + if (string.IsNullOrEmpty(command)) + { + throw new InvalidOperationException($"Failed to generate the command in '{this.GetType().Name}'"); + } + + string destination = GetDestination(arguments); + + if (string.IsNullOrEmpty(destination)) + { + throw new InvalidOperationException($"Failed to resolve destination path in {this.GetType().Name}"); + } + if (!Directory.Exists(destination)) + { + throw new InvalidOperationException($"Resolve destination folder does not exist. Folder: '{destination}', type: '{this.GetType().Name}'"); + } + + RunCommandInProcess(destination, command); + } + + private void RunCommandInProcess(string destination, string command) + { + string strCmdText = $"/C cd \"{destination}\"&{command}"; + System.Diagnostics.Process proc = new System.Diagnostics.Process(); + proc.StartInfo.Arguments = strCmdText; + proc.StartInfo.FileName = "CMD.exe"; + proc.StartInfo.UseShellExecute = false; + proc.StartInfo.RedirectStandardError = true; + proc.StartInfo.RedirectStandardOutput = true; + proc.Start(); + proc.WaitForExit(); + if (proc.ExitCode != 0) + { + throw new AggregateException($"Failed to run '{command}'\n{proc.StandardError.ReadToEnd()}"); + } + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Install/Containers/RunDockerProcessor.cs b/src/SIM.Pipelines/Install/Containers/RunDockerProcessor.cs new file mode 100644 index 00000000..c30301d2 --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/RunDockerProcessor.cs @@ -0,0 +1,30 @@ +using SIM.Loggers; +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using SIM.Pipelines.BaseProcessors; + +namespace SIM.Pipelines.Install.Containers +{ + [UsedImplicitly] + public class RunDockerProcessor : RunCmdCommandBaseProcessor + { + protected override string GetCommand(ProcessorArgs procArgs) + { + return "docker-compose.exe up -d"; + } + + protected override string GetExecutionFolder(ProcessorArgs procArgs) + { + InstallContainerArgs args = (InstallContainerArgs)procArgs; + + return args.Destination; + } + + protected override ILogger GetLogger(ProcessorArgs procArgs) + { + InstallContainerArgs args = (InstallContainerArgs)procArgs; + + return args.Logger; + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Install/Containers/WriteEnvFileProcessor.cs b/src/SIM.Pipelines/Install/Containers/WriteEnvFileProcessor.cs new file mode 100644 index 00000000..38343af3 --- /dev/null +++ b/src/SIM.Pipelines/Install/Containers/WriteEnvFileProcessor.cs @@ -0,0 +1,20 @@ +using JetBrains.Annotations; +using SIM.Pipelines.Processors; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SIM.Pipelines.Install.Containers +{ + public class WriteEnvFileProcessor : Processor + { + protected override void Process([NotNull] ProcessorArgs arguments) + { + InstallContainerArgs args = (InstallContainerArgs)arguments; + args.EnvModel.SaveToFile(Path.Combine(args.Destination,".env")); + } + } +} diff --git a/src/SIM.Pipelines/PipelinesConfig.cs b/src/SIM.Pipelines/PipelinesConfig.cs index 59024c3e..002ef12d 100644 --- a/src/SIM.Pipelines/PipelinesConfig.cs +++ b/src/SIM.Pipelines/PipelinesConfig.cs @@ -20,6 +20,56 @@ public static class PipelinesConfig + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SIM.Pipelines/Reinstall/Containers/CleanupDataBaseProcessor.cs b/src/SIM.Pipelines/Reinstall/Containers/CleanupDataBaseProcessor.cs new file mode 100644 index 00000000..cbc5e7e1 --- /dev/null +++ b/src/SIM.Pipelines/Reinstall/Containers/CleanupDataBaseProcessor.cs @@ -0,0 +1,59 @@ +using System.IO; +using JetBrains.Annotations; +using SIM.Loggers; +using SIM.Pipelines.Delete.Containers; +using SIM.Pipelines.Processors; +using Sitecore.Diagnostics.Base; + +namespace SIM.Pipelines.Reinstall.Containers +{ + [UsedImplicitly] + public abstract class CleanupDataBaseProcessor : Processor + { + protected abstract string DataFolder { get; } + + private ILogger _logger; + + protected override void Process(ProcessorArgs procArgs) + { + DeleteContainersArgs args = (DeleteContainersArgs)procArgs; + + Assert.ArgumentNotNull(args, "args"); + + this._logger = args.Logger; + + string dataFolder = Path.Combine(args.DestinationFolder, DataFolder); + + CleanupFolder(dataFolder); + } + + private void CleanupFolder(string dataFolder) + { + if (!Directory.Exists(dataFolder)) + { + this._logger.Warn($"The '{dataFolder}' folder does not exist."); + + return; + } + + DirectoryInfo parentDirectoryInfo = new DirectoryInfo(dataFolder); + + FileInfo[] fileInfos = parentDirectoryInfo.GetFiles(); + DirectoryInfo[] directoryInfos = parentDirectoryInfo.GetDirectories(); + + this._logger.Info($"Deleting file(s) and folder(s) from the '{dataFolder}' folder"); + + foreach (FileInfo fileInfo in fileInfos) + { + fileInfo.Delete(); + } + + foreach (DirectoryInfo directoryInfo in directoryInfos) + { + directoryInfo.Delete(recursive: true); + } + + this._logger.Info($"The '{dataFolder}' folder has been clean up."); + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Reinstall/Containers/CleanupSolrDataProcessor.cs b/src/SIM.Pipelines/Reinstall/Containers/CleanupSolrDataProcessor.cs new file mode 100644 index 00000000..b4752e26 --- /dev/null +++ b/src/SIM.Pipelines/Reinstall/Containers/CleanupSolrDataProcessor.cs @@ -0,0 +1,10 @@ +using JetBrains.Annotations; + +namespace SIM.Pipelines.Reinstall.Containers +{ + [UsedImplicitly] + public class CleanupSolrDataProcessor : CleanupDataBaseProcessor + { + protected override string DataFolder => "solr-data"; + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Reinstall/Containers/CleanupSqlDataProcessor.cs b/src/SIM.Pipelines/Reinstall/Containers/CleanupSqlDataProcessor.cs new file mode 100644 index 00000000..462d4b83 --- /dev/null +++ b/src/SIM.Pipelines/Reinstall/Containers/CleanupSqlDataProcessor.cs @@ -0,0 +1,11 @@ +using System.IO; +using JetBrains.Annotations; + +namespace SIM.Pipelines.Reinstall.Containers +{ + [UsedImplicitly] + public class CleanupSqlDataProcessor : CleanupDataBaseProcessor + { + protected override string DataFolder => "mssql-data"; + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Reinstall/Containers/RemoveFromDockerProcessor.cs b/src/SIM.Pipelines/Reinstall/Containers/RemoveFromDockerProcessor.cs new file mode 100644 index 00000000..960bf5cc --- /dev/null +++ b/src/SIM.Pipelines/Reinstall/Containers/RemoveFromDockerProcessor.cs @@ -0,0 +1,31 @@ +using JetBrains.Annotations; +using SIM.Loggers; +using SIM.Pipelines.BaseProcessors; +using SIM.Pipelines.Delete.Containers; +using SIM.Pipelines.Processors; + +namespace SIM.Pipelines.Reinstall.Containers +{ + [UsedImplicitly] + public class RemoveFromDockerProcessor : RunCmdCommandBaseProcessor + { + protected override string GetCommand(ProcessorArgs procArgs) + { + return "docker-compose.exe down"; + } + + protected override string GetExecutionFolder(ProcessorArgs procArgs) + { + DeleteContainersArgs args = (DeleteContainersArgs)procArgs; + + return args.DestinationFolder; + } + + protected override ILogger GetLogger(ProcessorArgs procArgs) + { + DeleteContainersArgs args = (DeleteContainersArgs)procArgs; + + return args.Logger; + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/Reinstall/Containers/RunDockerProcessor.cs b/src/SIM.Pipelines/Reinstall/Containers/RunDockerProcessor.cs new file mode 100644 index 00000000..b6577fa4 --- /dev/null +++ b/src/SIM.Pipelines/Reinstall/Containers/RunDockerProcessor.cs @@ -0,0 +1,25 @@ +using JetBrains.Annotations; +using SIM.Loggers; +using SIM.Pipelines.Delete.Containers; +using SIM.Pipelines.Processors; + +namespace SIM.Pipelines.Reinstall.Containers +{ + [UsedImplicitly] + public class RunDockerProcessor : Install.Containers.RunDockerProcessor + { + protected override string GetExecutionFolder(ProcessorArgs procArgs) + { + DeleteContainersArgs args = (DeleteContainersArgs)procArgs; + + return args.DestinationFolder; + } + + protected override ILogger GetLogger(ProcessorArgs procArgs) + { + DeleteContainersArgs args = (DeleteContainersArgs)procArgs; + + return args.Logger; + } + } +} \ No newline at end of file diff --git a/src/SIM.Pipelines/SIM.Pipelines.csproj b/src/SIM.Pipelines/SIM.Pipelines.csproj index 99f9c652..1212db0b 100644 --- a/src/SIM.Pipelines/SIM.Pipelines.csproj +++ b/src/SIM.Pipelines/SIM.Pipelines.csproj @@ -70,6 +70,7 @@ + @@ -89,9 +90,13 @@ + + + + @@ -100,6 +105,21 @@ + + + + + + + + + + + + + + + @@ -110,6 +130,7 @@ + @@ -162,6 +183,11 @@ + + + + + @@ -235,6 +261,10 @@ {CA9339A0-9A7D-4900-839E-F21B7269BDAA} SIM.Base + + {d9ad90f9-bf6d-4c46-8546-3bed9ff93450} + SIM.ContainerInstaller + {02B6C2D7-3083-4DF3-B86D-B6D4728C4EF2} SIM.FileSystem diff --git a/src/SIM.Products.Tests/Parsers/ContainerProductParserTests.cs b/src/SIM.Products.Tests/Parsers/ContainerProductParserTests.cs new file mode 100644 index 00000000..eae4289a --- /dev/null +++ b/src/SIM.Products.Tests/Parsers/ContainerProductParserTests.cs @@ -0,0 +1,180 @@ +using Xunit; +using NSubstitute; +using SIM.Products.ProductParsers; + +namespace SIM.Products.Tests.Parsers +{ + public class ContainerProductParserTests + { + [Theory] + [InlineData("zzz")] + public void TryParseProduct_CannotParsePackagePath_ReturnsFalse(string packagePath) + { + // Arrange + var parser = new ContainerProductParser(); + + // Act + var result = parser.TryParseProduct(packagePath, out Product product); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("zzz")] + public void TryParseProduct_CannotParsePackagePath_OutProductIsNull(string packagePath) + { + // Arrange + var parser = new ContainerProductParser(); + + // Act + var result = parser.TryParseProduct(packagePath, out Product product); + + // Assert + Assert.Null(product); + } + + [Theory] + [InlineData("c:\\Install\\sc\\SitecoreContainerDeployment.10.0.0.004346.150.zip")] + public void TryParseProduct_CanParsePackagePath_ReturnsTrue(string packagePath) + { + // Arrange + var parser = new ContainerProductParser(); + + // Act + var result = parser.TryParseProduct(packagePath, out Product product); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("c:\\Install\\sc\\SitecoreContainerDeployment.10.0.0.004346.150.zip")] + public void TryParseProduct_CanParsePackagePath_OutProductIsNotNull(string packagePath) + { + // Arrange + var parser = new ContainerProductParser(); + + // Act + var result = parser.TryParseProduct(packagePath, out Product product); + + // Assert + Assert.NotNull(product); + } + + [Theory] + [InlineData( + "c:\\Install\\sc\\SitecoreContainerDeployment.10.0.0.004346.150.zip", + "SitecoreContainerDeployment", + "10.0", + "10.0.0", + "004346.150")] + public void TryParseProduct_CanParsePackagePath_GetOrCreateProductIsCalled( + string packagePath, + string originalName, + string twoVersion, + string triVersion, + string revision + ) + { + // Arrange + var parser = Substitute.ForPartsOf(); + parser.WhenForAnyArgs(x => x.GetOrCreateProduct( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + )).DoNotCallBase(); + + // Act + parser.TryParseProduct(packagePath, out Product product); + + // Assert + parser.Received().GetOrCreateProduct(originalName, packagePath, twoVersion, triVersion, revision); + //parser.DidNotReceive().GetOrCreateProduct(originalName, packagePath, twoVersion, triVersion, revision); + } + + [Theory] + [InlineData("zzz")] + public void TryParseProduct_CannotParsePackagePath_GetOrCreateProductIsNotCalled(string packagePath) + { + // Arrange + var parser = Substitute.ForPartsOf(); + parser.WhenForAnyArgs(x => x.GetOrCreateProduct( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + )).DoNotCallBase(); + + // Act + parser.TryParseProduct(packagePath, out Product product); + + // Assert + parser.DidNotReceive().GetOrCreateProduct( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Theory] + [InlineData("zzz")] + public void TryParseName_CannotParsePackagePath_ReturnsFalse(string packagePath) + { + // Arrange + var parser = new ContainerProductParser(); + + // Act + var result = parser.TryParseName(packagePath, out string name); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("zzz")] + public void TryParseName_CannotParsePackagePath_OutNameIsNull(string packagePath) + { + // Arrange + var parser = new ContainerProductParser(); + + // Act + parser.TryParseName(packagePath, out string name); + + // Assert + Assert.Null(name); + } + + [Theory] + [InlineData("c:\\Install\\sc\\SitecoreContainerDeployment.10.0.0.004346.150.zip")] + public void TryParseName_CanParsePackagePath_ReturnsTrue(string packagePath) + { + // Arrange + var parser = new ContainerProductParser(); + + // Act + var result = parser.TryParseName(packagePath, out string name); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("c:\\Install\\sc\\SitecoreContainerDeployment.10.0.0.004346.150.zip", "SitecoreContainerDeployment")] + public void TryParseName_CanParsePackagePath_OutNameIsProper(string packagePath, string expectedName) + { + // Arrange + var parser = new ContainerProductParser(); + + // Act + parser.TryParseName(packagePath, out string actualName); + + // Assert + Assert.Equal(expectedName, actualName); + } + } +} \ No newline at end of file diff --git a/src/SIM.Products.Tests/Properties/AssemblyInfo.cs b/src/SIM.Products.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..627df272 --- /dev/null +++ b/src/SIM.Products.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SIM.Products.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SIM.Products.Tests")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9c3a325b-61be-40c2-9a77-d790212f1a22")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/SIM.Products.Tests/SIM.Products.Tests.csproj b/src/SIM.Products.Tests/SIM.Products.Tests.csproj new file mode 100644 index 00000000..e0657139 --- /dev/null +++ b/src/SIM.Products.Tests/SIM.Products.Tests.csproj @@ -0,0 +1,102 @@ + + + + + + Debug + AnyCPU + {9C3A325B-61BE-40C2-9A77-D790212F1A22} + Library + Properties + SIM.Products.Tests + SIM.Products.Tests + v4.7.2 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Castle.Core.4.4.0\lib\net45\Castle.Core.dll + + + ..\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll + + + ..\packages\NSubstitute.4.2.1\lib\net46\NSubstitute.dll + + + ..\packages\Sitecore.Diagnostics.Base.2.0.1.171\lib\Sitecore.Diagnostics.Base.dll + + + + + + ..\packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + + + + + + + + ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll + + + ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll + + + + + + + + + + + + + + + + + + {bc8b4ee5-c053-42e1-a5ba-07f8c41915a9} + SIM.Products + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/SIM.Products.Tests/packages.config b/src/SIM.Products.Tests/packages.config new file mode 100644 index 00000000..5a9b9089 --- /dev/null +++ b/src/SIM.Products.Tests/packages.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/SIM.Products/ManifestHelper.cs b/src/SIM.Products/ManifestHelper.cs index 1a47a91a..1aa64db0 100644 --- a/src/SIM.Products/ManifestHelper.cs +++ b/src/SIM.Products/ManifestHelper.cs @@ -1,4 +1,7 @@ -namespace SIM.Products +using SIM.Products.ProductParsers; +using Sitecore.Diagnostics.InfoService.Client.Model; + +namespace SIM.Products { using System; using System.Collections.Generic; @@ -87,10 +90,10 @@ public static List GetFileNamePatterns(string packageFile, string origin var result = new[] { - name, - name + " " + version.SubstringEx(0, 1), - name + " " + version.SubstringEx(0, 3), - name + " " + version.SubstringEx(0, 5), + name, + name + " " + version.SubstringEx(0, 1), + name + " " + version.SubstringEx(0, 3), + name + " " + version.SubstringEx(0, 5), name + " " + version + " rev. " + revision } .Distinct() @@ -99,6 +102,25 @@ public static List GetFileNamePatterns(string packageFile, string origin return ProfileSection.Result(result); } + // Consider fallback product parsers + IProductParser[] parsers = ProductManager.ProductParsers; + + foreach (var parser in parsers) + { + if (parser.TryParseName(packageFile, out originalName)) + { + if (!string.IsNullOrEmpty(originalName)) + { + var result = new List() + { + originalName + }; + + return result; + } + } + } + return new List(new[] { Path.GetFileNameWithoutExtension(packageFile) @@ -212,7 +234,7 @@ private static XmlDocumentEx Compute(string packageFile, LookupFolder[] manifest using (var stream = new MemoryStream()) { entry.Extract(stream); - stream.Seek(0,SeekOrigin.Begin); + stream.Seek(0, SeekOrigin.Begin); mainDocument = XmlDocumentEx.LoadStream(stream); } @@ -270,15 +292,15 @@ private static XmlDocumentEx Compute(string packageFile, LookupFolder[] manifest return ProfileSection.Result(packageManifest); } - using(var zip = new RealZipFile(new RealFileSystem().ParseFile(packageFile))) - if (zip.Entries.Contains("metadata/sc_name.txt") && - zip.Entries.Contains("installer/version")) - { - Log.Info($"The '{packageFile}' file is considered as Sitecore Package, (type #2)"); - CacheManager.SetEntry("IsPackage", packageFile, "true"); + using (var zip = new RealZipFile(new RealFileSystem().ParseFile(packageFile))) + if (zip.Entries.Contains("metadata/sc_name.txt") && + zip.Entries.Contains("installer/version")) + { + Log.Info($"The '{packageFile}' file is considered as Sitecore Package, (type #2)"); + CacheManager.SetEntry("IsPackage", packageFile, "true"); - return ProfileSection.Result(packageManifest); - } + return ProfileSection.Result(packageManifest); + } CacheManager.SetEntry("IsPackage", packageFile, "false"); diff --git a/src/SIM.Products/Product.cs b/src/SIM.Products/Product.cs index b0d46dc1..01741a21 100644 --- a/src/SIM.Products/Product.cs +++ b/src/SIM.Products/Product.cs @@ -1,4 +1,6 @@ -namespace SIM.Products +using SIM.Products.ProductParsers; + +namespace SIM.Products { #region @@ -47,11 +49,11 @@ public class Product : IXmlSerializable public static Product Undefined { get; } = new Product() { - Name = "Undefined", - OriginalName = "Undefined", - PackagePath = string.Empty, - IsStandalone = true, - ShortName = "undefined", + Name = "Undefined", + OriginalName = "Undefined", + PackagePath = string.Empty, + IsStandalone = true, + ShortName = "undefined", TwoVersion = string.Empty, TriVersion = string.Empty, Revision = string.Empty @@ -66,6 +68,7 @@ public class Product : IXmlSerializable private string _ShortName; private string _ShortVersion; private int? _SortOrder; + private bool? _IsContainer; #endregion @@ -157,6 +160,14 @@ public bool IsArchive } } + public bool IsSitecoreWdpPackage + { + get + { + return !string.IsNullOrEmpty(this.PackagePath) && this.PackagePath.EndsWith(".scwdp.zip"); + } + } + public bool IsPackage { get @@ -165,6 +176,14 @@ public bool IsPackage } } + public bool IsContainer + { + get + { + return (bool)(this._IsContainer ?? (this._IsContainer = this.Manifest.With(m => m.SelectSingleElement(ManifestPrefix + "container")) != null)); + } + } + public bool IsStandalone { get @@ -354,7 +373,7 @@ public string ShortName return _ShortName ?? (_ShortName = Manifest.With(m => m.SelectSingleElement(ManifestPrefix + "*/shortName")).With(m => m.InnerText.EmptyToNull()) ?? Name.Split(' ') - .Aggregate(string.Empty, + .Aggregate(string.Empty, (current, word) => current + (word.Length > 0 ? word[0].ToString(CultureInfo.InvariantCulture) : string.Empty)) @@ -467,351 +486,368 @@ public static bool TryParse([NotNull] string packagePath, [CanBeNull] out Produc Match match = ProductRegex.Match(packagePath); if (!match.Success) { - return false; + return TryFallbackParsers(packagePath, out product); } return TryParse(packagePath, match, out product); } - public bool IsMatchRequirements([NotNull] Product instanceProduct) + private static bool TryFallbackParsers([NotNull] string packagePath, [CanBeNull] out Product product) { - Assert.ArgumentNotNull(instanceProduct, nameof(instanceProduct)); + product = null; - // !ProductHelper.Settings.InstallModulesCheckRequirements.Value&& - if (!Name.EqualsIgnoreCase("Sitecore Analytics")) - { - return true; - } + IProductParser[] productParsers = ProductManager.ProductParsers; - foreach (XmlElement product in Manifest.SelectElements(ManifestPrefix + "*/requirements/product")) + foreach (var productParser in productParsers) { - IEnumerable rules = product.ChildNodes.OfType().ToArray(); - foreach (IGrouping group in rules.GroupBy(ch => ch.Name.ToLower())) + if (productParser.TryParseProduct(packagePath, out product)) { - var key = group.Key; - switch (key) - { - case "version": - if (rules.Where(r => r.Name.ToLower() == key).All(s => !VersionMatch(instanceProduct, s.GetAttribute("value"), s))) - { - return false; - } - - break; - case "revision": - if (rules.Where(r => r.Name.ToLower() == key).All(s => !RevisionMatch(instanceProduct, s.GetAttribute("value")))) - { - return false; - } - - break; - } + return true; } } - return true; + return false; } - public override string ToString() + public bool IsMatchRequirements([NotNull] Product instanceProduct) + { + Assert.ArgumentNotNull(instanceProduct, nameof(instanceProduct)); + + // !ProductHelper.Settings.InstallModulesCheckRequirements.Value&& + if (!Name.EqualsIgnoreCase("Sitecore Analytics")) { - return ToString(false); + return true; } - public virtual string ToString(bool triVersion) + foreach (XmlElement product in Manifest.SelectElements(ManifestPrefix + "*/requirements/product")) { - const string Pattern = "{0} {1} rev. {2}"; + IEnumerable rules = product.ChildNodes.OfType().ToArray(); + foreach (IGrouping group in rules.GroupBy(ch => ch.Name.ToLower())) + { + var key = group.Key; + switch (key) + { + case "version": + if (rules.Where(r => r.Name.ToLower() == key).All(s => !VersionMatch(instanceProduct, s.GetAttribute("value"), s))) + { + return false; + } + + break; + case "revision": + if (rules.Where(r => r.Name.ToLower() == key).All(s => !RevisionMatch(instanceProduct, s.GetAttribute("value")))) + { + return false; + } - return Pattern.FormatWith(Name, triVersion ? TriVersion : TwoVersion, Revision).TrimEnd(" rev. ").TrimEnd(' '); + break; + } + } } - #endregion + return true; + } - #region Private methods + public override string ToString() + { + return ToString(false); + } + + public virtual string ToString(bool triVersion) + { + const string Pattern = "{0} {1} rev. {2}"; + + return Pattern.FormatWith(Name, triVersion ? TriVersion : TwoVersion, Revision).TrimEnd(" rev. ").TrimEnd(' '); + } - private Dictionary GetReadme() + #endregion + + #region Private methods + + private Dictionary GetReadme() + { + var readmeNode = Manifest.GetElementsByTagName("readme")[0]; + if (readmeNode != null && !string.IsNullOrEmpty(readmeNode.InnerText)) { - var readmeNode = Manifest.GetElementsByTagName("readme")[0]; - if (readmeNode != null && !string.IsNullOrEmpty(readmeNode.InnerText)) - { - return new Dictionary + return new Dictionary { { true, readmeNode.InnerText } }; - } + } - var tempExtractFolderPath = Path.Combine(Directory.GetParent(PackagePath).FullName, "TempExtract"); - if (!Directory.Exists(tempExtractFolderPath)) - { - Directory.CreateDirectory(tempExtractFolderPath); - } + var tempExtractFolderPath = Path.Combine(Directory.GetParent(PackagePath).FullName, "TempExtract"); + if (!Directory.Exists(tempExtractFolderPath)) + { + Directory.CreateDirectory(tempExtractFolderPath); + } - using (var zip = ZipFile.Read(PackagePath)) - { - ZipEntry readmeEntry; + using (var zip = ZipFile.Read(PackagePath)) + { + ZipEntry readmeEntry; - var packageEntry = zip["package.zip"]; + var packageEntry = zip["package.zip"]; - if (packageEntry != null) - { - packageEntry.Extract(tempExtractFolderPath, ExtractExistingFileAction.OverwriteSilently); + if (packageEntry != null) + { + packageEntry.Extract(tempExtractFolderPath, ExtractExistingFileAction.OverwriteSilently); - using (var packageZip = ZipFile.Read(Path.Combine(tempExtractFolderPath, "package.zip"))) - { - readmeEntry = packageZip["metadata/sc_readme.txt"]; - if (readmeEntry != null) - { - readmeEntry.Extract(tempExtractFolderPath, ExtractExistingFileAction.OverwriteSilently); - } - } - } - else + using (var packageZip = ZipFile.Read(Path.Combine(tempExtractFolderPath, "package.zip"))) { - readmeEntry = zip["metadata/sc_readme.txt"]; + readmeEntry = packageZip["metadata/sc_readme.txt"]; if (readmeEntry != null) { readmeEntry.Extract(tempExtractFolderPath, ExtractExistingFileAction.OverwriteSilently); } } } - - string readmeText; - - var path = Path.Combine(tempExtractFolderPath, "metadata", "sc_readme.txt"); - try - { - readmeText = File.ReadAllText(path); - } - catch (Exception ex) + else { - Log.Warn(ex, $"An error occurred during extracting readme text from {path}"); - readmeText = string.Empty; + readmeEntry = zip["metadata/sc_readme.txt"]; + if (readmeEntry != null) + { + readmeEntry.Extract(tempExtractFolderPath, ExtractExistingFileAction.OverwriteSilently); + } } + } - Directory.Delete(tempExtractFolderPath, true); - return new Dictionary + string readmeText; + + var path = Path.Combine(tempExtractFolderPath, "metadata", "sc_readme.txt"); + try + { + readmeText = File.ReadAllText(path); + } + catch (Exception ex) + { + Log.Warn(ex, $"An error occurred during extracting readme text from {path}"); + readmeText = string.Empty; + } + + Directory.Delete(tempExtractFolderPath, true); + return new Dictionary { { false, readmeText } }; - } + } - #endregion + #endregion - #endregion + #endregion - #region Methods + #region Methods - private static bool TryParse([NotNull] string packagePath, [NotNull] Match match, [CanBeNull] out Product product) + private static bool TryParse([NotNull] string packagePath, [NotNull] Match match, [CanBeNull] out Product product) + { + Assert.ArgumentNotNull(packagePath, nameof(packagePath)); + Assert.ArgumentNotNull(match, nameof(match)); + + product = null; + if (!FileSystem.FileSystem.Local.File.Exists(packagePath)) { - Assert.ArgumentNotNull(packagePath, nameof(packagePath)); - Assert.ArgumentNotNull(match, nameof(match)); + packagePath = null; + } - product = null; - if (!FileSystem.FileSystem.Local.File.Exists(packagePath)) - { - packagePath = null; - } + var originalName = match.Groups[1].Value; + var name = originalName; + string shortName = null; + var version = match.Groups[2].Value; + var revision = match.Groups[5].Value; - var originalName = match.Groups[1].Value; - var name = originalName; - string shortName = null; - var version = match.Groups[2].Value; - var revision = match.Groups[5].Value; + if (name.EqualsIgnoreCase("sitecore") && version[0] == '5') + { + return false; + } - if (name.EqualsIgnoreCase("sitecore") && version[0] == '5') - { - return false; - } - - var arr = version.Split('.'); + var arr = version.Split('.'); - product = ProductManager.Products.FirstOrDefault(p => p.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase) && p.OriginalName.Equals(originalName) && p.ShortName.EqualsIgnoreCase(shortName) && p.Revision.EqualsIgnoreCase(revision)) - ?? new Product - { - OriginalName = originalName, - PackagePath = packagePath, - TwoVersion = $"{arr[0]}.{arr[1]}", - TriVersion = version, - Revision = revision - }; + product = ProductManager.Products.FirstOrDefault(p => p.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase) && p.OriginalName.Equals(originalName) && p.ShortName.EqualsIgnoreCase(shortName) && p.Revision.EqualsIgnoreCase(revision)) + ?? new Product + { + OriginalName = originalName, + PackagePath = packagePath, + TwoVersion = $"{arr[0]}.{arr[1]}", + TriVersion = version, + Revision = revision + }; + + return true; + } - return true; - } + public string TriVersion { get; set; } - public string TriVersion { get; set; } + [NotNull] + private string FormatString([NotNull] string pattern) + { + Assert.ArgumentNotNull(pattern, nameof(pattern)); - [NotNull] - private string FormatString([NotNull] string pattern) - { - Assert.ArgumentNotNull(pattern, nameof(pattern)); + return pattern.Replace("{ShortName}", ShortName).Replace("{Name}", Name).Replace("{ShortVersion}", ShortVersion).Replace("{Version}", TwoVersion).Replace("{Revision}", Revision) + .Replace("{UpdateOrRevision}", UpdateOrRevision); + } + + private bool RevisionMatch([NotNull] Product instanceProduct, [NotNull] string revision) + { + Assert.ArgumentNotNull(instanceProduct, nameof(instanceProduct)); + Assert.ArgumentNotNull(revision, nameof(revision)); - return pattern.Replace("{ShortName}", ShortName).Replace("{Name}", Name).Replace("{ShortVersion}", ShortVersion).Replace("{Version}", TwoVersion).Replace("{Revision}", Revision) - .Replace("{UpdateOrRevision}", UpdateOrRevision); + if (string.IsNullOrEmpty(revision)) + { + revision = Revision; } - private bool RevisionMatch([NotNull] Product instanceProduct, [NotNull] string revision) + var instanceRevision = instanceProduct.Revision; + if (instanceRevision == revision || string.IsNullOrEmpty(instanceRevision)) { - Assert.ArgumentNotNull(instanceProduct, nameof(instanceProduct)); - Assert.ArgumentNotNull(revision, nameof(revision)); + return true; + } - if (string.IsNullOrEmpty(revision)) - { - revision = Revision; - } + return false; + } - var instanceRevision = instanceProduct.Revision; - if (instanceRevision == revision || string.IsNullOrEmpty(instanceRevision)) - { - return true; - } + private bool VersionMatch([NotNull] Product instanceProduct, [NotNull] string version, [NotNull] XmlElement versionRule) + { + Assert.ArgumentNotNull(instanceProduct, nameof(instanceProduct)); + Assert.ArgumentNotNull(version, nameof(version)); + Assert.ArgumentNotNull(versionRule, nameof(versionRule)); - return false; + if (string.IsNullOrEmpty(version)) + { + version = TwoVersion; } - private bool VersionMatch([NotNull] Product instanceProduct, [NotNull] string version, [NotNull] XmlElement versionRule) + var instanceVersion = instanceProduct.TwoVersion; + if (instanceVersion == version) { - Assert.ArgumentNotNull(instanceProduct, nameof(instanceProduct)); - Assert.ArgumentNotNull(version, nameof(version)); - Assert.ArgumentNotNull(versionRule, nameof(versionRule)); - - if (string.IsNullOrEmpty(version)) + var rules = versionRule.SelectElements("revision").ToArray(); + if (rules.Length == 0) { - version = TwoVersion; + return true; } - var instanceVersion = instanceProduct.TwoVersion; - if (instanceVersion == version) + if (rules.Any(s => RevisionMatch(instanceProduct, s.GetAttribute("value")))) { - var rules = versionRule.SelectElements("revision").ToArray(); - if (rules.Length == 0) - { - return true; - } - - if (rules.Any(s => RevisionMatch(instanceProduct, s.GetAttribute("value")))) - { - return true; - } + return true; } - - return false; } - #endregion + return false; + } - #region Public methods + #endregion - public static Product GetFilePackageProduct(string packagePath) + #region Public methods + + public static Product GetFilePackageProduct(string packagePath) + { + return FileSystem.FileSystem.Local.File.Exists(packagePath) ? new Product { - return FileSystem.FileSystem.Local.File.Exists(packagePath) ? new Product - { - PackagePath = packagePath, - IsStandalone = false, - Name = Path.GetFileName(packagePath) - } : null; - } + PackagePath = packagePath, + IsStandalone = false, + Name = Path.GetFileName(packagePath) + } : null; + } - #endregion + #endregion - #region Private methods + #region Private methods + + private static bool GetIsPackage(string packagePath, XmlDocument manifest) + { + if (string.IsNullOrEmpty(packagePath)) + { + return false; + } - private static bool GetIsPackage(string packagePath, XmlDocument manifest) + const string CacheName = "IsPackage"; + var path = packagePath.ToLowerInvariant(); + using (new ProfileSection("Is it package or not")) { - if (string.IsNullOrEmpty(packagePath)) - { - return false; - } + ProfileSection.Argument("packagePath", packagePath); + ProfileSection.Argument("manifest", manifest); - const string CacheName = "IsPackage"; - var path = packagePath.ToLowerInvariant(); - using (new ProfileSection("Is it package or not")) + try { - ProfileSection.Argument("packagePath", packagePath); - ProfileSection.Argument("manifest", manifest); - - try + // cache + var entry = CacheManager.GetEntry(CacheName, path); + if (entry != null) { - // cache - var entry = CacheManager.GetEntry(CacheName, path); - if (entry != null) - { - var result = entry.EqualsIgnoreCase("true"); - - return ProfileSection.Result(result); - } + var result = entry.EqualsIgnoreCase("true"); - if ( - manifest.With( - x => - x.SelectSingleElement(ManifestPrefix + "standalone") ?? x.SelectSingleElement(ManifestPrefix + "archive")) != - null) - { - CacheManager.SetEntry(CacheName, path, "false"); - return ProfileSection.Result(false); - } - - if ( - manifest.With( - x => - x.SelectSingleElement(ManifestPrefix + "package")) != - null) - { - CacheManager.SetEntry(CacheName, path, "true"); - - return ProfileSection.Result(true); - } + return ProfileSection.Result(result); + } + if ( + manifest.With( + x => + x.SelectSingleElement(ManifestPrefix + "standalone") ?? x.SelectSingleElement(ManifestPrefix + "archive")) != + null) + { CacheManager.SetEntry(CacheName, path, "false"); - return ProfileSection.Result(false); } - catch (Exception e) + + if ( + manifest.With( + x => + x.SelectSingleElement(ManifestPrefix + "package")) != + null) { - Log.Warn(string.Format("Detecting if the '{0}' file is a Sitecore Package failed with exception.", path, e)); - CacheManager.SetEntry(CacheName, path, "false"); + CacheManager.SetEntry(CacheName, path, "true"); - return ProfileSection.Result(false); + return ProfileSection.Result(true); } + + CacheManager.SetEntry(CacheName, path, "false"); + + return ProfileSection.Result(false); } - } + catch (Exception e) + { + Log.Warn(string.Format("Detecting if the '{0}' file is a Sitecore Package failed with exception.", path, e)); + CacheManager.SetEntry(CacheName, path, "false"); - XmlSchema IXmlSerializable.GetSchema() - { - throw new NotImplementedException(); + return ProfileSection.Result(false); + } } + } - void IXmlSerializable.ReadXml(XmlReader reader) - { - throw new NotImplementedException(); - } + XmlSchema IXmlSerializable.GetSchema() + { + throw new NotImplementedException(); + } + + void IXmlSerializable.ReadXml(XmlReader reader) + { + throw new NotImplementedException(); + } - void IXmlSerializable.WriteXml(XmlWriter writer) + void IXmlSerializable.WriteXml(XmlWriter writer) + { + foreach (var property in GetType().GetProperties()) { - foreach (var property in GetType().GetProperties()) + object value = property.GetValue(this, new object[0]); + var xml = value as XmlDocument; + if (xml != null) { - object value = property.GetValue(this, new object[0]); - var xml = value as XmlDocument; - if (xml != null) - { - writer.WriteNode(new XmlNodeReader(XmlDocumentEx.LoadXml($"{xml.OuterXml}")), false); - continue; - } - - writer.WriteElementString(property.Name, string.Empty, (value ?? string.Empty).ToString()); + writer.WriteNode(new XmlNodeReader(XmlDocumentEx.LoadXml($"{xml.OuterXml}")), false); + continue; } + + writer.WriteElementString(property.Name, string.Empty, (value ?? string.Empty).ToString()); } + } - #endregion + #endregion - /// - /// Force manifest to be reloaded from disk, to reset variable replacements. - /// - public void ResetManifest() - { - _Manifest = null; - } + /// + /// Force manifest to be reloaded from disk, to reset variable replacements. + /// + public void ResetManifest() + { + _Manifest = null; } } +} diff --git a/src/SIM.Products/ProductManager.cs b/src/SIM.Products/ProductManager.cs index 8a5be7fe..6f08c5e9 100644 --- a/src/SIM.Products/ProductManager.cs +++ b/src/SIM.Products/ProductManager.cs @@ -1,4 +1,6 @@ -namespace SIM.Products +using SIM.Products.ProductParsers; + +namespace SIM.Products { using System; using System.Collections.Generic; @@ -22,6 +24,8 @@ public static class ProductManager #endregion + private static IProductParser[] _productParsers; + #region Properties [NotNull] @@ -33,6 +37,28 @@ public static IEnumerable StandaloneProducts } } + public static IEnumerable ContainerProducts + { + get + { + return Products.Where(p => p.IsContainer).OrderByDescending(p => p.SortOrder); + } + } + + public static IProductParser[] ProductParsers + { + get + { + if (_productParsers == null) + { + _productParsers = new IProductParser[] {new ContainerProductParser()}; + } + + return _productParsers; + } + + } + #endregion #region Public Methods @@ -216,5 +242,30 @@ public static Product FindProduct(ProductType type, [CanBeNull] string product, var distributive = products.FirstOrDefault(); return distributive; } + + public static Product GetOrCreateProduct( + string originalName, + string packagePath, + string twoVersion, + string triVersion, + string revision + ) + { + Assert.ArgumentNotNullOrEmpty(originalName, nameof(originalName)); + Assert.ArgumentNotNullOrEmpty(packagePath, nameof(packagePath)); + Assert.ArgumentNotNullOrEmpty(twoVersion, nameof(twoVersion)); + Assert.ArgumentNotNullOrEmpty(triVersion, nameof(triVersion)); + Assert.ArgumentNotNullOrEmpty(revision, nameof(revision)); + + return Products.FirstOrDefault(p => p.OriginalName.Equals(originalName) && p.Revision.EqualsIgnoreCase(revision)) + ?? new Product + { + OriginalName = originalName, + PackagePath = packagePath, + TwoVersion = twoVersion, + TriVersion = triVersion, + Revision = revision + }; + } } } \ No newline at end of file diff --git a/src/SIM.Products/ProductParsers/ContainerProductParser.cs b/src/SIM.Products/ProductParsers/ContainerProductParser.cs new file mode 100644 index 00000000..4151d684 --- /dev/null +++ b/src/SIM.Products/ProductParsers/ContainerProductParser.cs @@ -0,0 +1,86 @@ +using System.Text.RegularExpressions; +using Sitecore.Diagnostics.Base; + +namespace SIM.Products.ProductParsers +{ + public class ContainerProductParser : IProductParser + { + private const string ProductNamePattern = @"([a-zA-Z]{4,})"; + private const string ProductVersionPattern = @"(\d{1,2}\.\d{1,2}\.\d{1,2})"; + private const string ProductRevisionPattern = @"(\d{6}\.\d{1,6})"; + + public static string ProductFileNamePattern { get; } = $@"{ProductNamePattern}\.{ProductVersionPattern}\.{ProductRevisionPattern}.zip$"; + + public static Regex ProductRegex { get; } = new Regex(ProductFileNamePattern, RegexOptions.IgnoreCase); + + public bool TryParseName(string path, out string originalName) + { + string packagePath; + string twoVersion; + string triVersion; + string revision; + + if (DoParse(path, out originalName, out packagePath, out twoVersion, out triVersion, out revision)) + { + return true; + } + + return false; + } + + public bool TryParseProduct(string path, out Product product) + { + product = null; + + if (string.IsNullOrEmpty(path)) + { + return false; + } + + string originalName; + string packagePath; + string twoVersion; + string triVersion; + string revision; + + if (DoParse(path, out originalName, out packagePath, out twoVersion, out triVersion, out revision)) + { + product = GetOrCreateProduct(originalName, packagePath, twoVersion, triVersion, revision); + + return true; + } + + return false; + } + + protected internal virtual Product GetOrCreateProduct(string originalName, string packagePath, string twoVersion, string triVersion, string revision) + { + return ProductManager.GetOrCreateProduct(originalName, packagePath, twoVersion, triVersion, revision); + } + + protected virtual bool DoParse(string path, out string originalName, out string packagePath, out string twoVersion, out string triVersion, out string revision) + { + var match = ProductRegex.Match(path); + + if (!match.Success || match.Groups.Count < 4) + { + originalName = packagePath = twoVersion = triVersion = revision = default(string); + + return false; + } + + originalName = match.Groups[1].Value; + + packagePath = path; + + string[] versions = match.Groups[2].Value.Split('.'); + twoVersion = $"{versions[0]}.{versions[1]}"; + + triVersion = match.Groups[2].Value; + + revision = match.Groups[3].Value; + + return true; + } + } +} \ No newline at end of file diff --git a/src/SIM.Products/ProductParsers/IProductParser.cs b/src/SIM.Products/ProductParsers/IProductParser.cs new file mode 100644 index 00000000..3f333ee0 --- /dev/null +++ b/src/SIM.Products/ProductParsers/IProductParser.cs @@ -0,0 +1,9 @@ +namespace SIM.Products.ProductParsers +{ + public interface IProductParser + { + bool TryParseName(string packagePath, out string originalName); + + bool TryParseProduct(string packagePath, out Product product); + } +} diff --git a/src/SIM.Products/Properties/AssemblyInfo.cs b/src/SIM.Products/Properties/AssemblyInfo.cs index 25bdaf8b..e35db8e5 100644 --- a/src/SIM.Products/Properties/AssemblyInfo.cs +++ b/src/SIM.Products/Properties/AssemblyInfo.cs @@ -1,6 +1,7 @@ #region Usings using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; #endregion @@ -8,9 +9,10 @@ [assembly: AssemblyTitle("SIM.Kernel.Products")] [assembly: AssemblyCompany("Sitecore Corporation")] [assembly: AssemblyProduct("Sitecore Instance Manager")] -[assembly: AssemblyCopyright("Copyright © 2001-2014 Sitecore Corporation")] +[assembly: AssemblyCopyright("Copyright © 2001-2020 Sitecore Corporation")] [assembly: AssemblyTrademark("Sitecore® is a registered trademark of Sitecore Corporation")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] +[assembly: InternalsVisibleTo("SIM.Products.Tests")] -/*TeamCityPlaceholder*/ \ No newline at end of file +/*TeamCityPlaceholder*/ diff --git a/src/SIM.Products/SIM.Products.csproj b/src/SIM.Products/SIM.Products.csproj index 603aeffb..a2bd4560 100644 --- a/src/SIM.Products/SIM.Products.csproj +++ b/src/SIM.Products/SIM.Products.csproj @@ -74,6 +74,8 @@ + + @@ -91,6 +93,7 @@ +