@@ -12,9 +12,11 @@ namespace VirtualClient
1212 using System . IO . Abstractions ;
1313 using System . Linq ;
1414 using System . Net ;
15+ using System . Runtime . InteropServices ;
1516 using System . Text . RegularExpressions ;
1617 using Microsoft . CodeAnalysis ;
1718 using Microsoft . Extensions . Logging ;
19+ using VirtualClient . Common . Contracts ;
1820 using VirtualClient . Common . Extensions ;
1921 using VirtualClient . Contracts ;
2022 using VirtualClient . Contracts . Extensibility ;
@@ -29,6 +31,7 @@ public static class OptionFactory
2931 internal const string HtmlQuote = """ ;
3032 private static readonly ICertificateManager defaultCertificateManager = new CertificateManager ( ) ;
3133 private static readonly IFileSystem defaultFileSystem = new FileSystem ( ) ;
34+ private static readonly PlatformSpecifics defaultPlatformSpecifics = new PlatformSpecifics ( Environment . OSVersion . Platform , RuntimeInformation . ProcessArchitecture ) ;
3235 private static readonly char [ ] argumentTrimChars = new char [ ] { '\' ' , '"' , ' ' } ;
3336
3437 /// <summary>
@@ -671,21 +674,28 @@ public static Option CreateKeyVaultStoreOption(bool required = false, object def
671674 }
672675
673676 /// <summary>
674- /// Command line option defines the path to the environment layout file.
677+ /// Command line option defines the environment layout or a path to the layout file.
675678 /// </summary>
676679 /// <param name="required">Sets this option as required.</param>
677680 /// <param name="defaultValue">Sets the default value when none is provided.</param>
678- public static Option CreateLayoutPathOption ( bool required = true , object defaultValue = null )
681+ /// <param name="fileSystem">Optional parameter to use to validate file system paths.</param>
682+ /// <param name="platformSpecifics">Optional parameter defines the certificate manager to use for accessing certificates on the system.</param>
683+ public static Option CreateLayoutOption ( bool required = true , object defaultValue = null , IFileSystem fileSystem = null , PlatformSpecifics platformSpecifics = null )
679684 {
680685 // Note:
681686 // Only the first 3 of these will display in help output (i.e. --help).
682- Option < string > option = new Option < string > ( new string [ ] { "--layout" , "--layout-path" , } )
683- {
684- Name = "LayoutPath" ,
685- Description = "The path to the environment layout .json file required for client/server operations. The contents of this " +
686- "file are used by the self-hosted API service for example to enable individual instances of the application running on different " +
687- "systems to synchronize with each other." ,
688- ArgumentHelpName = "path" ,
687+ Option < EnvironmentLayout > option = new Option < EnvironmentLayout > (
688+ new string [ ] { "--layout" , "--layout-path" , } ,
689+ parseArgument : result => OptionFactory . ParseEnvironmentLayout (
690+ result ,
691+ fileSystem ?? OptionFactory . defaultFileSystem ,
692+ platformSpecifics ?? OptionFactory . defaultPlatformSpecifics ) )
693+ {
694+ Name = "Layout" ,
695+ Description =
696+ "An environment layout definition or path to a *.json file defining the set of systems associated with client/server operations. This definition " +
697+ "enable individual instances of the application running on different systems to synchronize with each other." ,
698+ ArgumentHelpName = "definition" ,
689699 AllowMultipleArgumentsPerToken = false
690700 } ;
691701
@@ -1682,6 +1692,65 @@ private static DependencyStore ParseBlobStore(ArgumentResult parsedResult, strin
16821692 return store ;
16831693 }
16841694
1695+ private static EnvironmentLayout ParseEnvironmentLayout ( ArgumentResult parsedResult , IFileSystem fileSystem , PlatformSpecifics platformSpecifics )
1696+ {
1697+ EnvironmentLayout layout = null ;
1698+
1699+ // A layout can be a path to a file or an inline definition:
1700+ //
1701+ // e.g. inline
1702+ // --layout "client01,10.1.0.1,Client;client02,10.1.0.2,Server"
1703+ //
1704+ // e.g. file path
1705+ // --layout-path="C:\Users\Any\VirtualClient\layout.json"
1706+ //
1707+ string layoutValue = parsedResult . Tokens ? . FirstOrDefault ( ) ? . Value ;
1708+ if ( ! string . IsNullOrWhiteSpace ( layoutValue ) && Regex . IsMatch ( layoutValue , "[,;]+" ) )
1709+ {
1710+ List < ClientInstance > clientsInstances = new List < ClientInstance > ( ) ;
1711+ string [ ] clients = layoutValue . Split ( ';' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
1712+
1713+ if ( clients ? . Any ( ) == true )
1714+ {
1715+ foreach ( string client in clients )
1716+ {
1717+ // e.g.
1718+ // client01,10.1.0.1,Client
1719+ // client02,10.1.0.2,Server
1720+ string [ ] clientParts = client . Split ( ',' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
1721+
1722+ if ( clientParts ? . Length == 3 )
1723+ {
1724+ clientsInstances . Add ( new ClientInstance ( clientParts [ 0 ] , ipAddress : clientParts [ 1 ] , role : clientParts [ 2 ] ) ) ;
1725+ }
1726+ }
1727+ }
1728+
1729+ if ( ! clientsInstances . Any ( ) )
1730+ {
1731+ throw new ArgumentException (
1732+ "Invalid layout definition. The environment layout definition provided is not in a valid format: " +
1733+ "{client_name},{ip_address},{role};{client_name},{ip_address},{role} (e.g. client01,10.1.0.1,Client;client02,10.1.0.2,Server)." ) ;
1734+ }
1735+
1736+ layout = new EnvironmentLayout ( clientsInstances ) ;
1737+ }
1738+ else
1739+ {
1740+ string layoutFullPath = platformSpecifics . StandardizePath ( Path . GetFullPath ( layoutValue ) ) ;
1741+
1742+ if ( ! fileSystem . File . Exists ( layoutFullPath ) )
1743+ {
1744+ throw new ArgumentException ( $ "Invalid path specified. An environment layout file does not exist at path '{ layoutFullPath } '.") ;
1745+ }
1746+
1747+ string layoutContent = RetryPolicies . Synchronous . FileOperations . Execute ( ( ) => fileSystem . File . ReadAllText ( layoutFullPath ) ) ;
1748+ layout = layoutContent . FromJson < EnvironmentLayout > ( ) ;
1749+ }
1750+
1751+ return layout ;
1752+ }
1753+
16851754 private static DependencyStore ParseKeyVaultStore ( ArgumentResult parsedResult , string storeName , ICertificateManager certificateManager , IFileSystem fileSystem )
16861755 {
16871756 string endpoint = OptionFactory . GetValue ( parsedResult ) ;
0 commit comments