diff --git a/.nox/design/SampleCurrency.solution.nox.yaml b/.nox/design/SampleCurrency.solution.nox.yaml new file mode 100755 index 00000000..7d4249db --- /dev/null +++ b/.nox/design/SampleCurrency.solution.nox.yaml @@ -0,0 +1,58 @@ +# +# Cli Sample Solution definition +# +# yaml-language-server: $schema=https://raw.githubusercontent.com/NoxOrg/Nox/main/src/Nox.Core/Schemas/NoxConfiguration.json +# + +# + +name: CliDemo + +description: A Cli Demo Nox Configuration +environments: + + - name: dev + description: Used for development and testing + + - name: test + description: Test environment + + - name: uat + description: For them end users to check it works + + - name: prod + description: Production environment used for, well - the real thing! + isProduction: true + +versionControl: + provider: azureDevops + host: https://dev.azure.com/iwgplc + folders: + sourceCode: /src + containers: /docker + +team: + + - name: Andre Sharpe + userName: andre.sharpe@iwgplc.com + roles: [architect, owner, administrator, developer, manager] + + - name: Jan Schutte + userName: jan.schutte@iwgplc.com + roles: [architect, administrator, developer, devOpsEngineer] + + - name: Anton Du Plessis + userName: anton.duplessis@iwgplc.com + roles: [projectManager] + + - name: Morne Van Zyl + userName: morne.vanzyl@iwgplc.com + roles: [technicalWriter] + + - name: Dmytro Dorodnykh + userName: dmytro.dorodnykh@iwgplc.com + roles: [developer] + + - name: Oleksandr Vlasenko + userName: oleksandr.vlasenko@regus.com + roles: [architect, developer] \ No newline at end of file diff --git a/samples/Design/SampleCurrency.service.nox.yaml b/samples/Design/SampleCurrency.service.nox.yaml deleted file mode 100755 index 8707500d..00000000 --- a/samples/Design/SampleCurrency.service.nox.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# -# Service definition -# -# yaml-language-server: $schema=https://raw.githubusercontent.com/NoxOrg/Nox/main/src/Nox.Core/Schemas/NoxConfiguration.json -# - -# - -name: CliDemo - -description: A Cli Demo Nox Configuration - -autoMigrations: false - -secrets: - validFor: - hours: 12 - providers: - - provider: azure-keyvault - url: https://svc-6A9B1182FC66686F.vault.azure.net/ - -database: - name: SampleCurrencyDb - server: localhost - provider: postgres - options: Trusted_Connection=no;connection timeout=120; - user: sa - password: Developer*123 - -messagingProviders: - - - name: AppServiceBus - provider: rabbitMQ - connectionString: rabbitmq://guest:guest@localhost/ - -dataSources: - - name: JsonSeedData - provider: json - options: Source=File;Path=../../docs/sample-data/; - -versionControl: - provider: azureDevOps - server: https://dev.azure.com/iwgplc - project: Nox.CliDemo - repository: CliDemo.Project.V1 - -team: - developers: - - name: Dionisis Stoubos - userName: dionisis.stoubos@iwgplc.com - isAdmin: true - email: dionisis.stoubos@iwgplc.com - - name: Andre Sharpe - userName: andre.sharpe@iwgplc.com - mobilePhoneNumber: +41789461056 - isAdmin: true - - name: Jan Schutte - userName: jan.schutte@iwgplc.com - isAdmin: true - email: jan.schutte@iwgplc.com - - name: Anton Du Plessis - userName: anton.duplessis@iwgplc.com - isAdmin: false - email: anton.duplessis@iwgplc.com - - name: Morne Van Zyl - userName: morne.vanzyl@iwgplc.com - isAdmin: false - email: morne.vanzyl@iwgplc.com - - name: Dmytro Dorodnykh - userName: dmytro.dorodnykh@iwgplc.com - isAdmin: false - email: dmytro.dorodnykh@iwgplc.com - - name: Oleksandr Vlasenko - userName: oleksandr.vlasenko@regus.com - isAdmin: false - email: oleksandr.vlasenko@regus.com diff --git a/src/CoreTests/CoreTests.csproj b/src/CoreTests/CoreTests.csproj index 9de48379..543b7824 100755 --- a/src/CoreTests/CoreTests.csproj +++ b/src/CoreTests/CoreTests.csproj @@ -10,9 +10,9 @@ - + - + all diff --git a/src/Nox.Cli.Abstractions/Constants/FileExtension.cs b/src/Nox.Cli.Abstractions/Constants/FileExtension.cs new file mode 100644 index 00000000..f5ab3392 --- /dev/null +++ b/src/Nox.Cli.Abstractions/Constants/FileExtension.cs @@ -0,0 +1,6 @@ +namespace Nox.Cli.Abstractions.Constants; + +public static class FileExtension +{ + public const string WorkflowDefinition = @"*.workflow.nox.yaml"; +} \ No newline at end of file diff --git a/src/Nox.Cli.Abstractions/Helpers/YamlHelper.cs b/src/Nox.Cli.Abstractions/Helpers/YamlHelper.cs new file mode 100644 index 00000000..a9fb1ce2 --- /dev/null +++ b/src/Nox.Cli.Abstractions/Helpers/YamlHelper.cs @@ -0,0 +1,89 @@ +using System.Text.RegularExpressions; +using Nox.Cli.Abstractions.Exceptions; + +namespace Nox.Cli.Abstractions.Helpers; + +public static class YamlHelper +{ + private static readonly Regex _referenceRegex = new(@"\$ref\S*:\s*(?[\w:\.\/\\]+\b[\w\-\.\/]+)\s*", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(5)); + + /// + /// Resolve $ref <path> tags in a yaml source yaml file.
+ /// Loads the source yaml file, resolves the $ref's and replaces them with the yaml from the files specified in <path>
+ /// Note: This call is recursive, all $refs in the yaml hierarchy will be resolved. + /// Note: child nodes are added at the same indentation as the $ref tag. + ///
+ /// Full or relative path to the source yaml file. + /// + public static string ResolveYamlReferences(string path) + { + var sourceFullPath = Path.GetFullPath(path); + if (!File.Exists(sourceFullPath)) throw new NoxCliException($"Yaml file {path} does not exist!"); + var sourcePath = Path.GetDirectoryName(sourceFullPath); + + var sourceLines = File.ReadAllLines(path); + var outputLines = ResolveYamlReferences(sourceLines.ToList(), sourcePath!).Result; + + return string.Join('\n', outputLines.ToArray()); + } + + /// + /// Resolve $ref <path> tags in a yaml source yaml file.
+ /// Loads the source yaml file, resolves the $ref's and replaces them with the yaml from the files specified in <path>
+ /// Note: This call is recursive, all $refs in the yaml hierarchy will be resolved. + /// Note: child nodes are added at the same indentation as the $ref tag. + ///
+ /// Full or relative path to the source yaml file. + /// + public static async Task ResolveYamlReferencesAsync(string path) + { + var sourceFullPath = Path.GetFullPath(path); + if (!File.Exists(sourceFullPath)) throw new NoxCliException($"Yaml file {path} does not exist!"); + var sourcePath = Path.GetDirectoryName(sourceFullPath); + + var sourceLines = await File.ReadAllLinesAsync(path); + var outputLines = await ResolveYamlReferences(sourceLines.ToList(), sourcePath!); + + return string.Join('\n', outputLines.ToArray()); + } + + private static async Task> ResolveYamlReferences(List sourceLines, string path) + { + var outputLines = new List(); + foreach (var sourceLine in sourceLines) + { + if (!sourceLine.TrimStart().StartsWith('#')) + { + var match = _referenceRegex.Match(sourceLine); + if (match.Success) + { + var padding = new string(' ', match.Index); + var childPath = match.Groups[1].Value; + if (!Path.IsPathRooted(childPath)) childPath = Path.Combine(path!, childPath); + if (!File.Exists(childPath)) throw new NoxCliException($"Referenced yaml file does not exist for reference: {match.Groups[1].Value}"); + var childLines = await File.ReadAllLinesAsync(childPath); + foreach (var childLine in childLines) + { + outputLines.Add(padding + childLine); + } + } + else + { + outputLines.Add(sourceLine); + } + } + else + { + outputLines.Add(sourceLine); + } + } + + if (outputLines.Any(ol => ol.Contains("$ref:") && !ol.TrimStart().StartsWith('#'))) + { + outputLines = await ResolveYamlReferences(outputLines, path); + } + + return outputLines; + } + +} \ No newline at end of file diff --git a/src/Nox.Cli.Abstractions/INoxWorkflowContext.cs b/src/Nox.Cli.Abstractions/INoxWorkflowContext.cs index 4167b84b..c48ee071 100755 --- a/src/Nox.Cli.Abstractions/INoxWorkflowContext.cs +++ b/src/Nox.Cli.Abstractions/INoxWorkflowContext.cs @@ -1,5 +1,6 @@ using Nox.Cli.Abstractions.Caching; -using Nox.Core.Interfaces; +using Nox.Secrets.Abstractions; +using Nox.Solution; namespace Nox.Cli.Abstractions { @@ -15,6 +16,7 @@ public interface INoxWorkflowContext void SetState(ActionState state); INoxCliCacheManager? CacheManager { get; } - void SetProjectConfiguration(IProjectConfiguration projectConfiguration); + INoxSecretsResolver? NoxSecretsResolver { get; } + void SetProjectConfiguration(NoxSolution projectConfiguration); } } \ No newline at end of file diff --git a/src/Nox.Cli.Abstractions/Nox.Cli.Abstractions.csproj b/src/Nox.Cli.Abstractions/Nox.Cli.Abstractions.csproj index 21b9aa02..75209238 100755 --- a/src/Nox.Cli.Abstractions/Nox.Cli.Abstractions.csproj +++ b/src/Nox.Cli.Abstractions/Nox.Cli.Abstractions.csproj @@ -8,6 +8,7 @@ - + + diff --git a/src/Nox.Cli.Authentication.Azure/Nox.Cli.Authentication.Azure.csproj b/src/Nox.Cli.Authentication.Azure/Nox.Cli.Authentication.Azure.csproj index 2f9a6b29..acc0e39a 100755 --- a/src/Nox.Cli.Authentication.Azure/Nox.Cli.Authentication.Azure.csproj +++ b/src/Nox.Cli.Authentication.Azure/Nox.Cli.Authentication.Azure.csproj @@ -12,6 +12,6 @@
- + diff --git a/src/Nox.Cli.Authentication.Tests/Nox.Cli.Authentication.Tests.csproj b/src/Nox.Cli.Authentication.Tests/Nox.Cli.Authentication.Tests.csproj index b23472dc..2f68d317 100755 --- a/src/Nox.Cli.Authentication.Tests/Nox.Cli.Authentication.Tests.csproj +++ b/src/Nox.Cli.Authentication.Tests/Nox.Cli.Authentication.Tests.csproj @@ -10,9 +10,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Nox.Cli.Authentication/Nox.Cli.Authentication.csproj b/src/Nox.Cli.Authentication/Nox.Cli.Authentication.csproj index 3813de4b..128c4417 100755 --- a/src/Nox.Cli.Authentication/Nox.Cli.Authentication.csproj +++ b/src/Nox.Cli.Authentication/Nox.Cli.Authentication.csproj @@ -9,10 +9,10 @@ - + - - + + diff --git a/src/Nox.Cli.Caching/Nox.Cli.Caching.csproj b/src/Nox.Cli.Caching/Nox.Cli.Caching.csproj index 0353f585..9a00cca4 100755 --- a/src/Nox.Cli.Caching/Nox.Cli.Caching.csproj +++ b/src/Nox.Cli.Caching/Nox.Cli.Caching.csproj @@ -12,10 +12,11 @@ - - + + + diff --git a/src/Nox.Cli.Caching/NoxCliCacheManager.cs b/src/Nox.Cli.Caching/NoxCliCacheManager.cs index e8ec1424..fd004ef4 100755 --- a/src/Nox.Cli.Caching/NoxCliCacheManager.cs +++ b/src/Nox.Cli.Caching/NoxCliCacheManager.cs @@ -1,20 +1,21 @@ +using System.Net; using System.Net.NetworkInformation; using System.Text.Json; using Newtonsoft.Json; using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Caching; using Nox.Cli.Abstractions.Configuration; +using Nox.Cli.Abstractions.Constants; using Nox.Cli.Abstractions.Exceptions; +using Nox.Cli.Abstractions.Helpers; using Nox.Cli.Configuration; -using Nox.Core.Constants; -using Nox.Core.Exceptions; -using Nox.Core.Helpers; using Nox.Utilities.Configuration; using Nox.Utilities.Credentials; using RestSharp; using Spectre.Console; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; +using IDeserializer = YamlDotNet.Serialization.IDeserializer; using JsonSerializer = System.Text.Json.JsonSerializer; namespace Nox.Cli.Caching; @@ -77,6 +78,7 @@ public bool IsOnline { try { var uri = new Uri(_remoteUrl); + if (uri.Host == "localhost") return true; var reply = ping.Send(uri.Host, 3000); if (reply.Status == IPStatus.Success) { @@ -279,7 +281,7 @@ private void GetOnlineWorkflowsAndManifest(IDictionary yamlFiles Directory.CreateDirectory(_workflowCachePath); var existingCacheList = Directory - .GetFiles(_workflowCachePath, FileExtension.WorflowDefinition) + .GetFiles(_workflowCachePath, FileExtension.WorkflowDefinition) .Select(f => (new FileInfo(f)).Name).ToHashSet(); foreach (var file in onlineFiles!) @@ -394,7 +396,7 @@ private void ResolveManifest(IDeserializer deserializer, Dictionary yamlFiles) { _workflows = new List(); - foreach (var yaml in yamlFiles.Where(kv => kv.Key.EndsWith(FileExtension.WorflowDefinition.TrimStart('*')))) + foreach (var yaml in yamlFiles.Where(kv => kv.Key.EndsWith(FileExtension.WorkflowDefinition.TrimStart('*')))) { try { @@ -402,14 +404,14 @@ private void ResolveWorkflows(IDeserializer deserializer, Dictionary {onlineFilesJson.ErrorException?.Message}"); } + if (onlineFilesJson.StatusCode != HttpStatusCode.OK) return; + var onlineFiles = JsonSerializer.Deserialize>(onlineFilesJson.Content, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase diff --git a/src/Nox.Cli.Configuration/Nox.Cli.Configuration.csproj b/src/Nox.Cli.Configuration/Nox.Cli.Configuration.csproj index 305d1d58..78238c47 100755 --- a/src/Nox.Cli.Configuration/Nox.Cli.Configuration.csproj +++ b/src/Nox.Cli.Configuration/Nox.Cli.Configuration.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Nox.Cli.PersonalAccessToken/Nox.Cli.PersonalAccessToken.csproj b/src/Nox.Cli.PersonalAccessToken/Nox.Cli.PersonalAccessToken.csproj index 77f31165..37c1b919 100755 --- a/src/Nox.Cli.PersonalAccessToken/Nox.Cli.PersonalAccessToken.csproj +++ b/src/Nox.Cli.PersonalAccessToken/Nox.Cli.PersonalAccessToken.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Arm/ArmConnect_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Arm/ArmConnect_v1.cs index 7d5d8973..36cc1bad 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Arm/ArmConnect_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Arm/ArmConnect_v1.cs @@ -1,8 +1,8 @@ using Azure.Identity; using Azure.ResourceManager; using Nox.Cli.Abstractions; +using Nox.Cli.Abstractions.Exceptions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Exceptions; namespace Nox.Cli.Plugin.Arm; @@ -64,7 +64,7 @@ public async Task> ProcessAsync(INoxWorkflowContext var subs = client.GetSubscriptions(); var subResponse = await subs.GetAsync(_subscriptionId); var sub = subResponse.Value; - outputs["subscription"] = sub ?? throw new NoxException($"Unable to connect to subscription {_subscriptionId}"); + outputs["subscription"] = sub ?? throw new NoxCliException($"Unable to connect to subscription {_subscriptionId}"); ctx.SetState(ActionState.Success); } catch (Exception ex) diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Arm/Nox.Cli.Plugin.Arm.csproj b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Arm/Nox.Cli.Plugin.Arm.csproj index 543d3af6..169f8487 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Arm/Nox.Cli.Plugin.Arm.csproj +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Arm/Nox.Cli.Plugin.Arm.csproj @@ -7,11 +7,11 @@ - + + - + - diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/AzDevOpsMergeFolder_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/AzDevOpsMergeFolder_v1.cs index 005d8787..a130cd4c 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/AzDevOpsMergeFolder_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/AzDevOpsMergeFolder_v1.cs @@ -1,9 +1,9 @@ using Microsoft.TeamFoundation.SourceControl.WebApi; using Microsoft.VisualStudio.Services.WebApi; using Nox.Cli.Abstractions; +using Nox.Cli.Abstractions.Exceptions; using Nox.Cli.Abstractions.Extensions; using Nox.Cli.Plugin.AzDevOps.Helpers; -using Nox.Core.Exceptions; namespace Nox.Cli.Plugin.AzDevOps; @@ -148,7 +148,7 @@ private GitCommitRef CreateCommit() private async Task CreatePush(string branchName, GitCommitRef commit) { var serverBranches = await _gitClient!.GetRefsAsync(_repoId!.Value.ToString(), filter: $"heads/{branchName}"); - if (serverBranches.Count != 1) throw new NoxException($"Unable to locate branch {branchName}"); + if (serverBranches.Count != 1) throw new NoxCliException($"Unable to locate branch {branchName}"); var branch = new GitRefUpdate { diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/AzDevopsCreateBuildDefinition_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/AzDevopsCreateBuildDefinition_v1.cs index e0979779..49978310 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/AzDevopsCreateBuildDefinition_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/AzDevopsCreateBuildDefinition_v1.cs @@ -2,8 +2,8 @@ using Microsoft.TeamFoundation.SourceControl.WebApi; using Microsoft.VisualStudio.Services.WebApi; using Nox.Cli.Abstractions; +using Nox.Cli.Abstractions.Exceptions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Exceptions; namespace Nox.Cli.Plugin.AzDevOps; @@ -121,7 +121,7 @@ public async Task> ProcessAsync(INoxWorkflowContext try { var repo = await _repoClient.GetRepositoryAsync(_repoId!.Value.ToString()); - if (repo == null) throw new NoxException("Unable to locate the source repository!"); + if (repo == null) throw new NoxCliException("Unable to locate the source repository!"); var ymlProcess = new YamlProcess { diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/Nox.Cli.Plugin.AzDevOps.csproj b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/Nox.Cli.Plugin.AzDevOps.csproj index bca357d1..e228d2bb 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/Nox.Cli.Plugin.AzDevOps.csproj +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/Nox.Cli.Plugin.AzDevOps.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzureAd/Nox.Cli.Plugin.AzureAd.csproj b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzureAd/Nox.Cli.Plugin.AzureAd.csproj index 9763be3a..e59a635d 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzureAd/Nox.Cli.Plugin.AzureAd.csproj +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzureAd/Nox.Cli.Plugin.AzureAd.csproj @@ -8,8 +8,7 @@ - - + diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/ConsolePromptSchema_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/ConsolePromptSchema_v1.cs index 6600f5d7..cf78b660 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/ConsolePromptSchema_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/ConsolePromptSchema_v1.cs @@ -4,6 +4,7 @@ using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Exceptions; using Nox.Cli.Abstractions.Extensions; +using Nox.Cli.Plugin.Console.JsonSchema; using RestSharp; using Spectre.Console; @@ -79,6 +80,8 @@ public NoxActionMetaData Discover() private string? _schema = null!; + private IDictionary? _schemaCache; + private string[]? _includedPrompts; private string[]? _excludedPrompts; @@ -91,10 +94,11 @@ public NoxActionMetaData Discover() private readonly RestClient _client = new(); - private readonly Regex resolveRefs = new("\"\\$ref\\\"\\s*:\\s*\\\"(?[\\w:/\\.]+)\\\"", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex _resolveRefs = new("\"\\$ref\\\"\\s*:\\s*\\\"(?[\\w:/\\.]+)\\\"", RegexOptions.Compiled | RegexOptions.IgnoreCase); private readonly Dictionary _responses = new(); + private bool _isArrayStart = false; public Task BeginAsync(IDictionary inputs) { @@ -104,7 +108,7 @@ public Task BeginAsync(IDictionary inputs) if (Uri.IsWellFormedUriString(schemaUrl, UriKind.Absolute)) { _schemaUrl = (new Uri(schemaUrl)).AbsoluteUri; - }; + } _schema = inputs.Value("schema"); @@ -137,18 +141,35 @@ public async Task> ProcessAsync(INoxWorkflowContext { try { + var json = _schema; - var json = _schema ?? await AnsiConsole.Status() - .Spinner(Spinner.Known.Clock) - .StartAsync("Reading schemas...", ctx => - ReadSchemaFromUrl(_schemaUrl!, ctx) - ); - + if (string.IsNullOrWhiteSpace(json)) + { + var baseUrl = ""; + var schemaName = ""; + var lastIndex = _schemaUrl!.LastIndexOf('/'); + if (lastIndex != -1) + { + baseUrl = _schemaUrl![..lastIndex]; + schemaName = _schemaUrl!.Substring(lastIndex + 1); + } + json = await AnsiConsole.Status() + .Spinner(Spinner.Known.Clock) + .StartAsync("Reading schemas...", fn => + ReadSchemaFromUrl(baseUrl, schemaName, fn) + ); + } + if (json != null) { - var jsonSchema = JsonSerializer.Deserialize(json, new JsonSerializerOptions { + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); + }; + serializeOptions.Converters.Add(new JsonSchemaTypeConverter()); + + var jsonSchema = JsonSerializer.Deserialize(json, serializeOptions); if (jsonSchema != null) { @@ -160,13 +181,14 @@ public async Task> ProcessAsync(INoxWorkflowContext _sbYaml.AppendLine($"#"); _sbYaml.AppendLine($""); - await AskForProperties(jsonSchema); + await ProcessSchema(jsonSchema); foreach(var (key,value) in _responses) { outputs[key] = value; } + if (_fileOptions != null && _fileOptions.ContainsKey("filename")) { _sbYaml.Insert(0,$"# {Path.GetFileName(_fileOptions["filename"])}{Environment.NewLine}"); @@ -174,6 +196,8 @@ public async Task> ProcessAsync(INoxWorkflowContext var contents = _sbYaml.ToString(); + //await File.WriteAllTextAsync("/home/jan/Downloads/solution.yaml", contents); + var outputFilePath = _fileOptions["filename"]; if (_fileOptions.TryGetValue("folder", out var folder)) { @@ -205,243 +229,299 @@ public Task EndAsync() return Task.CompletedTask; } - private async Task ReadSchemaFromUrl(string url, StatusContext ctx) + private async Task ReadSchemaFromUrl(string baseUrl, string schemaName, StatusContext ctx, bool isRecursiveCall = false) { - ctx.Status = $"Reading schema {url}..."; + ctx.Status = $"Reading schema {schemaName}..."; - var request = new RestRequest(url) { Method = Method.Get }; + if (!isRecursiveCall) _schemaCache = new Dictionary(); - request.AddHeader("Accept", "application/json"); + var json = ""; - var onlineFilesJson = await _client.ExecuteAsync(request); + if (!_schemaCache!.ContainsKey(schemaName)) + { - var json = onlineFilesJson.Content; + var request = new RestRequest($"{baseUrl}/{schemaName}") { Method = Method.Get }; - if (json == null) - { - return null; - } + request.AddHeader("Accept", "application/json"); - var matches = resolveRefs.Matches(json); + var onlineFilesJson = await _client.ExecuteAsync(request); - foreach(Match match in matches) - { - var subRef = await ReadSchemaFromUrl(match.Groups["url"].Value, ctx); - if (subRef != null) + json = onlineFilesJson.Content; + + _schemaCache![schemaName] = json; + + if (json == null) + { + return null; + } + + var matches = _resolveRefs.Matches(json); + + foreach (Match match in matches) { - subRef = subRef.Trim(); - subRef = subRef.Substring(1, subRef.Length - 2); - json = json.Replace(match.Value, subRef); + var subRef = await ReadSchemaFromUrl(baseUrl, match.Groups["url"].Value, ctx, true); + if (!string.IsNullOrWhiteSpace(subRef)) + { + subRef = subRef.Trim(); + subRef = subRef.Substring(1, subRef.Length - 2); + json = json.Replace(match.Value, subRef); + } } } return json; } - private async Task AskForProperties(JsonSchema.JsonSchema jsonSchema, string indent = "", string fullKey = "") + /// + /// This method is recursive through the schema, it is quite complicated, proceed with caution. + /// + /// The input schema + /// the root path of the current key + /// The current key to process + private async Task ProcessSchema(JsonSchema.JsonSchema schema, string rootKey = "", string key = "") { - if (!string.IsNullOrWhiteSpace(jsonSchema.Description)) - { - AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine($"[yellow]{jsonSchema.Description.EscapeMarkup()}[/]"); - } - - var yamlSpacing = indent.Replace('.', ' '); + var newKey = $"{rootKey}.{key}".TrimStart('.'); + var prefix = $"[grey]{newKey.PadRight(40, '.').EscapeMarkup()}[/] "; + var yamlSpacing = new string(' ', newKey.Count(d => d == '.') * 2); + var yamlSpacingPostfix = ""; - if (fullKey.EndsWith(']')) + if (_isArrayStart) { yamlSpacingPostfix = "- "; } - - indent += ".."; - - foreach (var (key,prop) in jsonSchema.Properties) + else { - var newFullKey = $"{fullKey}.{key}".TrimStart('.'); + if (rootKey.EndsWith(']')) yamlSpacing += " "; + } - if (_includedPrompts != null && !_includedPrompts.Any(f => newFullKey.StartsWith(f,StringComparison.OrdinalIgnoreCase))) + if (!string.IsNullOrWhiteSpace(key) && _includedPrompts != null && !_includedPrompts.Any(f => newKey.StartsWith(f, StringComparison.OrdinalIgnoreCase))) + { + if (_defaults != null && _defaults.Any(d => key.Equals(d.Key, StringComparison.OrdinalIgnoreCase))) { - if (_defaults != null && _defaults.Any(d => newFullKey.Equals(d.Key, StringComparison.OrdinalIgnoreCase))) - { - _sbYaml.AppendLine($"{yamlSpacing}{yamlSpacingPostfix}{key}: {_defaults[newFullKey]}"); - _responses[newFullKey] = _defaults[newFullKey]; - } - continue; + _sbYaml.AppendLine($"{key}: {_defaults[key]}"); + _responses[newKey] = _defaults[newKey]; } - if (_excludedPrompts != null && _excludedPrompts.Any(f => newFullKey.StartsWith(f, StringComparison.OrdinalIgnoreCase))) + return; + } + + if (!string.IsNullOrWhiteSpace(key) && _excludedPrompts != null && _excludedPrompts.Any(f => newKey.StartsWith(f, StringComparison.OrdinalIgnoreCase))) + { + if (_defaults != null && _defaults.Any(d => newKey.Equals(d.Key, StringComparison.OrdinalIgnoreCase))) { - if (_defaults != null && _defaults.Any(d => newFullKey.Equals(d.Key, StringComparison.OrdinalIgnoreCase))) - { - _sbYaml.AppendLine($"{yamlSpacing}{yamlSpacingPostfix}{key}: {_defaults[newFullKey]}"); - _responses[newFullKey] = _defaults[newFullKey]; - } - continue; + _sbYaml.AppendLine($"{key}: {_defaults[newKey]}"); + _responses[prefix] = _defaults[newKey]; } - if (prop.Type.Equals("object", StringComparison.OrdinalIgnoreCase)) + return; + } + + if (!string.IsNullOrWhiteSpace(schema.Description)) + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine($"[yellow]{schema.Description.EscapeMarkup()}[/]"); + } + + var message = (schema.Description ?? newKey).EscapeMarkup(); + var prompt = $"{prefix}[bold]{message}[/]:"; + var isRequired = schema.Required != null && schema.Required.Contains(newKey); + + if (schema.JsonSchemaType == null) + { + if (schema.AnyOf != null) { - _sbYaml.AppendLine($"{yamlSpacing}{key}:"); - await AskForProperties(prop, indent, newFullKey); + await ProcessSchema(schema.AnyOf[0], rootKey, key); } - - else if (prop.Type.Equals("array", StringComparison.OrdinalIgnoreCase)) + } + else + { + switch (schema.JsonSchemaType.Type) { - var index = 0; - if (prop.Items != null) - { - _sbYaml.AppendLine($"{yamlSpacing}{key}:"); - - do + case SchemaType.Boolean: + _isArrayStart = false; + PromptBoolean(prompt, rootKey, key, yamlSpacing + yamlSpacingPostfix); + break; + case SchemaType.Integer: + _isArrayStart = false; + PromptInteger(prompt, rootKey, key, yamlSpacing + yamlSpacingPostfix, isRequired); + break; + case SchemaType.String: + _isArrayStart = false; + if (schema.Enum != null) { - await AskForProperties(prop.Items, indent, $"{newFullKey}[{index}]"); - AnsiConsole.WriteLine(); - index++; + PromptEnum(prompt, rootKey, key, yamlSpacing + yamlSpacingPostfix, schema.Enum); } - while ( - AnsiConsole.Prompt( - new TextPrompt($"[grey]{new string('.',30)}[/] [bold]Add another[/]?") - .DefaultValueStyle(Style.Parse("mediumpurple3_1")) - .ChoicesStyle(Style.Parse("mediumpurple3_1")) - .PromptStyle(Style.Parse("seagreen1")) - .DefaultValue('n') - .AddChoice('y') - .AddChoice('n') - ) == 'y'); - } - } - else - { - var prefix = $"[grey]{newFullKey.PadRight(30, '.').EscapeMarkup()}[/] "; - var message = (prop.Description ?? newFullKey).EscapeMarkup(); - var prompt = $"{prefix}[bold]{message}[/]:"; - - AnsiConsole.WriteLine(); - - switch (prop.Type.ToLower()) - { - case "boolean": - var responseBool = AnsiConsole.Prompt( - new TextPrompt(prompt) - .DefaultValueStyle(Style.Parse("mediumpurple3_1")) - .ChoicesStyle(Style.Parse("mediumpurple3_1")) - .PromptStyle(Style.Parse("seagreen1")) - .DefaultValue('y') - .AddChoice('y') - .AddChoice('n') - ) == 'y'; - - _sbYaml.AppendLine($"{yamlSpacing}{yamlSpacingPostfix}{key}: {responseBool.ToString().ToLower()}"); - _responses[newFullKey] = responseBool; - - break; - - case "integer": - var promptObjInt = new TextPrompt(prompt) - .PromptStyle(Style.Parse("seagreen1")) - .DefaultValueStyle(Style.Parse("mediumpurple3_1")); - - var defaultValueInt = GetDefaultInt(prop, newFullKey); - - if (defaultValueInt != 0) - { - promptObjInt.DefaultValue(defaultValueInt); - } - var responseInt = AnsiConsole.Prompt(promptObjInt); - - _sbYaml.AppendLine($"{yamlSpacing}{yamlSpacingPostfix}{key}: {responseInt}"); - _responses[newFullKey] = responseInt; - - break; - - default: - if (prop.OneOf == null) + else + { + PromptString(prompt, rootKey, key, yamlSpacing + yamlSpacingPostfix, isRequired); + } + break; + case SchemaType.Object: + if (!key.EndsWith(']')) + { + _sbYaml.AppendLine(); + AppendKey(yamlSpacing, key); + } + foreach (var prop in schema.Properties!) + { + await ProcessSchema(prop.Value, newKey, prop.Key); + } + break; + case SchemaType.Array: + _isArrayStart = false; + if (schema.Items != null) + { + if (schema.Items.AnyOf != null) { - var promptObjString = new TextPrompt(prompt) - .PromptStyle(Style.Parse("seagreen1")) - .DefaultValueStyle(Style.Parse("mediumpurple3_1")); - - - promptObjString.AllowEmpty = !jsonSchema.Required.Contains(key); - - var defaultValueString = GetDefaultString(prop, newFullKey); - - if (!string.IsNullOrWhiteSpace(defaultValueString)) - { - promptObjString.DefaultValue(defaultValueString); - } + _sbYaml.AppendLine(); + AppendKey(yamlSpacing, key); - var responseString = AnsiConsole.Prompt(promptObjString); - if (!string.IsNullOrEmpty(responseString)) + var index = 0; + do { - _sbYaml.AppendLine($"{yamlSpacing}{yamlSpacingPostfix}{key}: {responseString}"); - _responses[newFullKey] = responseString; - } - } - else + _isArrayStart = true; + await ProcessSchema(schema.Items.AnyOf[0], rootKey, $"{key}[{index}]"); + _sbYaml.AppendLine(); + AnsiConsole.WriteLine(); + index++; + } while ( + AnsiConsole.Prompt( + new TextPrompt($"[grey]{yamlSpacing}[/] [bold]Add another[/]?") + .DefaultValueStyle(Style.Parse("mediumpurple3_1")) + .ChoicesStyle(Style.Parse("mediumpurple3_1")) + .PromptStyle(Style.Parse("seagreen1")) + .DefaultValue('n') + .AddChoice('y') + .AddChoice('n') + ) == 'y'); + } else if (schema.Items.Enum != null) { - var responseChoice = AnsiConsole.Prompt( - new SelectionPrompt() - .Title(prompt) - .HighlightStyle(Style.Parse("mediumpurple3_1")) - .AddChoices(prop.OneOf.Select(c => c.Const).ToArray()) - ); - - _responses[newFullKey] = responseChoice; - _sbYaml.AppendLine($"{yamlSpacing}{yamlSpacingPostfix}{key}: {responseChoice}"); - AnsiConsole.MarkupLine($"{prompt} [seagreen1]{_responses[newFullKey]}[/]"); - + schema.Items.JsonSchemaType!.Type = SchemaType.EnumList; + await ProcessSchema(schema.Items, rootKey, key); } - break; - } + } + break; + case SchemaType.EnumList: + _isArrayStart = false; + PromptMultipleEnum(prompt, rootKey, key, yamlSpacing + yamlSpacingPostfix, schema.Enum!); + break; } + } + } + + private void PromptBoolean(string prompt, string rootKey, string key, string yamlPrefix) + { + var newKey = $"{rootKey}.{key}".TrimStart('.'); + var response = AnsiConsole.Prompt( + new TextPrompt(prompt) + .DefaultValueStyle(Style.Parse("mediumpurple3_1")) + .ChoicesStyle(Style.Parse("mediumpurple3_1")) + .PromptStyle(Style.Parse("seagreen1")) + .DefaultValue('y') + .AddChoice('y') + .AddChoice('n') + ) == 'y'; + + _sbYaml.AppendLine($"{yamlPrefix}{key}: {response.ToString().ToLower()}"); + _responses[newKey] = response; + } + + private void PromptInteger(string prompt, string rootKey, string key, string yamlPrefix, bool isRequired) + { + var newKey = $"{rootKey}.{key}".TrimStart('.'); + var spectrePrompt = new TextPrompt(prompt) + .PromptStyle(Style.Parse("seagreen1")) + .DefaultValueStyle(Style.Parse("mediumpurple3_1")); - if (fullKey.EndsWith(']')) - { - yamlSpacingPostfix = " "; - } + spectrePrompt.AllowEmpty = !isRequired; + + var defaultValue = GetDefault(newKey); + if (defaultValue != 0) + { + spectrePrompt.DefaultValue(defaultValue); } + + var response = AnsiConsole.Prompt(spectrePrompt); + + _sbYaml.AppendLine($"{yamlPrefix}{key}: {response}"); + _responses[newKey] = response; } - private string GetDefaultString(JsonSchema.JsonSchema prop, string key) + private void PromptString(string prompt, string rootKey, string key, string yamlPrefix, bool isRequired) { - string? defaultValue = null; - - if (_defaults?.ContainsKey(key) ?? false) + var newKey = $"{rootKey}.{key}".TrimStart('.'); + var spectrePrompt = new TextPrompt(prompt) + .PromptStyle(Style.Parse("seagreen1")) + .DefaultValueStyle(Style.Parse("mediumpurple3_1")); + + spectrePrompt.AllowEmpty = !isRequired; + + var defaultValue = GetDefault(newKey); + + if (!string.IsNullOrWhiteSpace(defaultValue)) { - defaultValue = (string)_defaults[key]; + spectrePrompt.DefaultValue(defaultValue); } - if (string.IsNullOrWhiteSpace(defaultValue)) + var response = AnsiConsole.Prompt(spectrePrompt); + if (!string.IsNullOrEmpty(response)) { - if (!string.IsNullOrWhiteSpace(prop.Default?.ToString())) - { - defaultValue = prop.Default.ToString(); - } + _sbYaml.AppendLine($"{yamlPrefix}{key}: {response}"); + _responses[newKey] = response; } - return defaultValue ?? string.Empty; } - private int GetDefaultInt(JsonSchema.JsonSchema prop, string key) + private void PromptEnum(string prompt, string rootKey, string key, string yamlPrefix, List enumList) + { + var newKey = $"{rootKey}.{key}".TrimStart('.'); + var response = AnsiConsole.Prompt( + new SelectionPrompt() + .Title(prompt) + .HighlightStyle(Style.Parse("mediumpurple3_1")) + .AddChoices(enumList.ToArray()) + ); + + _responses[newKey] = response; + _sbYaml.AppendLine($"{yamlPrefix}{key}: {response}"); + AnsiConsole.MarkupLine($"{prompt} [seagreen1]{_responses[newKey]}[/]"); + } + + private void PromptMultipleEnum(string prompt, string rootKey, string key, string yamlPrefix, List enumList) { - int? defaultValue = null; + var newKey = $"{rootKey}.{key}".TrimStart('.'); + var response = AnsiConsole.Prompt( + new MultiSelectionPrompt() + .Title(prompt) + .HighlightStyle(Style.Parse("mediumpurple3_1")) + .AddChoices(enumList.ToArray()) + ); + + var responseValue = String.Join(',', response); + _responses[newKey] = responseValue; + _sbYaml.AppendLine($"{yamlPrefix}{key}: [{responseValue}]"); + AnsiConsole.MarkupLine($"{prompt} [seagreen1]{_responses[newKey]}[/]"); + } - if (_defaults?.ContainsKey(key) ?? false) + private void AppendKey(string yamlSpacing, string key) + { + if (!string.IsNullOrWhiteSpace(key)) { - defaultValue = int.Parse(_defaults[key]?.ToString() ?? "0"); + _sbYaml.AppendLine($"{yamlSpacing}{key}:"); } - - if (defaultValue == 0) + } + + + private T? GetDefault(string key) + { + if (_defaults?.ContainsKey(key) ?? false) { - if (!string.IsNullOrWhiteSpace(prop.Default?.ToString())) - { - defaultValue = int.Parse(prop.Default.ToString() ?? "0"); - } + return (T)_defaults[key]; } - return defaultValue ?? 0; - } + return default; + } + } diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchema.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchema.cs index 91b2cf0b..fd292171 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchema.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchema.cs @@ -1,21 +1,28 @@ -namespace Nox.Cli.Plugin.Console.JsonSchema; +using System.Text.Json.Serialization; + +namespace Nox.Cli.Plugin.Console.JsonSchema; + internal class JsonSchema { - public string Title { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public string Type { get; set; } = "object"; + public string? Title { get; set; } = string.Empty; + public string? Description { get; set; } = string.Empty; + + [JsonConverter(typeof(JsonSchemaTypeConverter))] + [JsonPropertyName("type")] + public JsonSchemaType? JsonSchemaType { get; set; } + public object? Default { get; set; } = null; - public string[] Required { get; set; } = new string[0]; - public Dictionary Properties { get; set; } = null!; + public List? Required { get; set; } = new(); + + public Dictionary? Properties { get; set; } = null!; + public JsonSchema? Items { get; set; } = null!; - public OneOfEntry[]? OneOf { get; set; } = null!; + + public JsonSchema[]? AnyOf { get; set; } + + public List? Enum { get; set; } } -internal class OneOfEntry -{ - public string Const { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchemaType.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchemaType.cs new file mode 100644 index 00000000..1f75ff29 --- /dev/null +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchemaType.cs @@ -0,0 +1,7 @@ +namespace Nox.Cli.Plugin.Console.JsonSchema; + +internal class JsonSchemaType +{ + public SchemaType? Type { get; set; } + public object? Default { get; set; } +} \ No newline at end of file diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchemaTypeConverter.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchemaTypeConverter.cs new file mode 100644 index 00000000..c848eb0d --- /dev/null +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/JsonSchemaTypeConverter.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Nox.Cli.Plugin.Console.JsonSchema; + +internal class JsonSchemaTypeConverter : JsonConverter +{ + public override JsonSchemaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var result = new JsonSchemaType(); + + switch (reader.TokenType) + { + case JsonTokenType.String: + result.Type = GetSchemaTypeFromString(reader.GetString()); + break; + case JsonTokenType.StartArray: + reader.Read(); + result.Type = GetSchemaTypeFromString(reader.GetString()); + reader.Read(); + result.Default = reader.GetString()!; + reader.Read(); + break; + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, JsonSchemaType? value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + private SchemaType GetSchemaTypeFromString(string? source) + { + return source?.ToLower() switch + { + "object" => SchemaType.Object, + "array" => SchemaType.Array, + "boolean" => SchemaType.Boolean, + "integer" => SchemaType.Integer, + _ => SchemaType.String + }; + } +} \ No newline at end of file diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/SchemaType.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/SchemaType.cs new file mode 100644 index 00000000..685a8ec4 --- /dev/null +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Console/JsonSchema/SchemaType.cs @@ -0,0 +1,11 @@ +namespace Nox.Cli.Plugin.Console.JsonSchema; + +public enum SchemaType +{ + String, + Boolean, + Integer, + Array, + Object, + EnumList +} \ No newline at end of file diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Core/CoreLoadNoxConfiguration_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Core/CoreLoadNoxConfiguration_v1.cs index c75d4ac4..7490ae51 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Core/CoreLoadNoxConfiguration_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Core/CoreLoadNoxConfiguration_v1.cs @@ -1,6 +1,8 @@ +using System.Reflection; using Nox.Cli.Abstractions; +using Nox.Cli.Abstractions.Exceptions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Builders; +using Nox.Solution; namespace Nox.Cli.Plugin.Core; @@ -36,6 +38,7 @@ public Task BeginAsync(IDictionary inputs) public Task> ProcessAsync(INoxWorkflowContext ctx) { + if (ctx.IsServer) throw new NoxCliException("This action cannot be executed on a server. remove the run-at-server attribute for this step in your Nox workflow."); var outputs = new Dictionary(); ctx.SetState(ActionState.Error); @@ -49,18 +52,20 @@ public Task> ProcessAsync(INoxWorkflowContext ctx) try { var fullPath = Path.GetFullPath(_path); - var projectConfig = new ProjectConfigurationBuilder(fullPath) + var solution = new NoxSolutionBuilder() + .OnResolveSecrets((_, args) => + { + var secretsConfig = args.SecretsConfig; + var secretKeys = args.Variables; + var resolver = ctx.NoxSecretsResolver; + if (resolver == null) throw new NoxCliException("Cannot load Nox solution definition, Secrets resolved has not been initialized."); + resolver.Configure(secretsConfig!, Assembly.GetEntryAssembly()); + args.Secrets = resolver.Resolve(secretKeys!); + }) .Build(); - if (projectConfig == null) - { - ctx.SetErrorMessage($"Unable to load Nox project configuration from {fullPath}"); - } - else - { - ctx.SetProjectConfiguration(projectConfig); + ctx.SetProjectConfiguration(solution); - ctx.SetState(ActionState.Success); - } + ctx.SetState(ActionState.Success); } catch (Exception ex) { diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Core/Nox.Cli.Plugin.Core.csproj b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Core/Nox.Cli.Plugin.Core.csproj index bf385e6a..4a020c5f 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Core/Nox.Cli.Plugin.Core.csproj +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Core/Nox.Cli.Plugin.Core.csproj @@ -11,8 +11,7 @@ - - + diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclAddValue_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclAddValue_v1.cs index cb52f2bd..359b0fce 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclAddValue_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclAddValue_v1.cs @@ -1,6 +1,5 @@ using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Exceptions; using Octopus.CoreParsers.Hcl; using Sprache; diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclAddValues_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclAddValues_v1.cs index 72bd3bdd..f6ed0184 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclAddValues_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclAddValues_v1.cs @@ -1,6 +1,5 @@ using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Exceptions; using Octopus.CoreParsers.Hcl; using Sprache; diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclHelpers.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclHelpers.cs index b76c77c9..71d8ef56 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclHelpers.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/HclHelpers.cs @@ -1,5 +1,4 @@ using Nox.Cli.Abstractions.Exceptions; -using Nox.Core.Exceptions; using Octopus.CoreParsers.Hcl; namespace Nox.Cli.Plugin.Hcl; @@ -9,7 +8,7 @@ public static class HclHelpers public static bool ValueExists(HclElement template, string valuePath) { var pathValues = valuePath.Split('/'); - if (pathValues.Length == 0) throw new NoxException("Node path is invalid!"); + if (pathValues.Length == 0) throw new NoxCliException("Node path is invalid!"); var foundNode = template.Child; if (foundNode == null) throw new NoxCliException("HCL template contains no children."); for (var i = 0; i < pathValues.Length; i++) @@ -52,7 +51,7 @@ public static void AddValue(HclElement template, string path, string value) } catch (Exception ex) { - throw new NoxException(ex.Message); + throw new NoxCliException(ex.Message); } } diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/Nox.Cli.Plugin.Hcl.csproj b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/Nox.Cli.Plugin.Hcl.csproj index 7c98a64b..542d8200 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/Nox.Cli.Plugin.Hcl.csproj +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Hcl/Nox.Cli.Plugin.Hcl.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Powershell/Nox.Cli.Plugin.Powershell.csproj b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Powershell/Nox.Cli.Plugin.Powershell.csproj index 403cd1cd..33832cd1 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Powershell/Nox.Cli.Plugin.Powershell.csproj +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Powershell/Nox.Cli.Plugin.Powershell.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/Nox.Cli.Plugin.Project.csproj b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/Nox.Cli.Plugin.Project.csproj index b0001435..df74af94 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/Nox.Cli.Plugin.Project.csproj +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/Nox.Cli.Plugin.Project.csproj @@ -5,10 +5,6 @@ enable enable - - - - diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetAdminEmails_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetAdminEmails_v1.cs old mode 100755 new mode 100644 index f50c8947..3bbff105 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetAdminEmails_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetAdminEmails_v1.cs @@ -1,7 +1,6 @@ using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Configuration; -using Nox.Core.Models; +using Nox.Solution; namespace Nox.Cli.Plugin.Project; @@ -72,15 +71,18 @@ public Task> ProcessAsync(INoxWorkflowContext ctx) var result = ""; foreach (var item in _members) { - if (!string.IsNullOrEmpty(item.Email) && item.IsAdmin) + if (!string.IsNullOrEmpty(item.UserName) && item.Roles != null && item.Roles.Contains(TeamRole.Administrator)) { - if (string.IsNullOrEmpty(result)) + if (item.UserName.Contains("@")) { - result = item.Email; - } - else - { - result += _delimiter + item.Email; + if (string.IsNullOrEmpty(result)) + { + result = item.UserName; + } + else + { + result += _delimiter + item.UserName; + } } } } diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetAdminUserNames_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetAdminUserNames_v1.cs index ce040afb..9aa4e08f 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetAdminUserNames_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetAdminUserNames_v1.cs @@ -1,7 +1,6 @@ using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Configuration; -using Nox.Core.Models; +using Nox.Solution; namespace Nox.Cli.Plugin.Project; @@ -72,7 +71,7 @@ public Task> ProcessAsync(INoxWorkflowContext ctx) var result = ""; foreach (var item in _members) { - if (!string.IsNullOrEmpty(item.UserName) && item.IsAdmin) + if (!string.IsNullOrEmpty(item.UserName) && item.Roles != null && item.Roles.Contains(TeamRole.Administrator)) { if (string.IsNullOrEmpty(result)) { diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetOwnerEmails_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetOwnerEmails_v1.cs index b5cb77e6..b29aeb0c 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetOwnerEmails_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetOwnerEmails_v1.cs @@ -1,7 +1,6 @@ using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Configuration; -using Nox.Core.Models; +using Nox.Solution; namespace Nox.Cli.Plugin.Project; @@ -72,15 +71,18 @@ public Task> ProcessAsync(INoxWorkflowContext ctx) var result = ""; foreach (var item in _members) { - if (!string.IsNullOrEmpty(item.Email) && item.IsProductOwner) + if (!string.IsNullOrEmpty(item.UserName) && item.Roles != null && item.Roles.Contains(TeamRole.Owner)) { - if (string.IsNullOrEmpty(result)) + if (item.UserName.Contains('@')) { - result = item.Email; - } - else - { - result += _delimiter + item.Email; + if (string.IsNullOrEmpty(result)) + { + result = item.UserName; + } + else + { + result += _delimiter + item.UserName; + } } } } diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetTeamEmails_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetTeamEmails_v1.cs index a741c56b..917abb0f 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetTeamEmails_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetTeamEmails_v1.cs @@ -1,7 +1,6 @@ using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Configuration; -using Nox.Core.Models; +using Nox.Solution; namespace Nox.Cli.Plugin.Project; @@ -81,16 +80,19 @@ public Task> ProcessAsync(INoxWorkflowContext ctx) var result = ""; foreach (var item in _members) { - if (item.IsAdmin && !_includeAdmin == true) continue; - if (!string.IsNullOrEmpty(item.Email)) + if (item.Roles != null && item.Roles.Contains(TeamRole.Administrator) && !_includeAdmin == true) continue; + if (!string.IsNullOrEmpty(item.UserName)) { - if (string.IsNullOrEmpty(result)) + if (item.UserName.Contains('@')) { - result = item.Email; - } - else - { - result += _delimiter + item.Email; + if (string.IsNullOrEmpty(result)) + { + result = item.UserName; + } + else + { + result += _delimiter + item.UserName; + } } } } diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetTeamUserNames_v1.cs b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetTeamUserNames_v1.cs index 059490cc..a29f53dc 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetTeamUserNames_v1.cs +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Project/ProjectGetTeamUserNames_v1.cs @@ -1,7 +1,6 @@ using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Extensions; -using Nox.Core.Configuration; -using Nox.Core.Models; +using Nox.Solution; namespace Nox.Cli.Plugin.Project; @@ -81,7 +80,7 @@ public Task> ProcessAsync(INoxWorkflowContext ctx) var result = ""; foreach (var item in _members) { - if (item.IsAdmin && !_includeAdmin == true) continue; + if (item.Roles != null && item.Roles.Contains(TeamRole.Administrator) && !_includeAdmin == true) continue; if (!string.IsNullOrEmpty(item.UserName)) { if (string.IsNullOrEmpty(result)) diff --git a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Teams/Nox.Cli.Plugin.Teams.csproj b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Teams/Nox.Cli.Plugin.Teams.csproj index dea05dc7..1da09389 100755 --- a/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Teams/Nox.Cli.Plugin.Teams.csproj +++ b/src/Nox.Cli.Plugins/Nox.Cli.Plugin.Teams/Nox.Cli.Plugin.Teams.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Nox.Cli.Secrets/IProjectSecretResolver.cs b/src/Nox.Cli.Secrets/IProjectSecretResolver.cs deleted file mode 100755 index 632f41a0..00000000 --- a/src/Nox.Cli.Secrets/IProjectSecretResolver.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Nox.Core.Interfaces; -using Nox.Core.Interfaces.Configuration; - -namespace Nox.Cli.Secrets; - -public interface IProjectSecretResolver -{ - Task Resolve(IDictionary variables, IProjectConfiguration config); -} \ No newline at end of file diff --git a/src/Nox.Cli.Secrets/Nox.Cli.Secrets.csproj b/src/Nox.Cli.Secrets/Nox.Cli.Secrets.csproj index 042a474c..192b75d7 100755 --- a/src/Nox.Cli.Secrets/Nox.Cli.Secrets.csproj +++ b/src/Nox.Cli.Secrets/Nox.Cli.Secrets.csproj @@ -9,10 +9,9 @@ - - + - + diff --git a/src/Nox.Cli.Secrets/ProjectSecretResolver.cs b/src/Nox.Cli.Secrets/ProjectSecretResolver.cs deleted file mode 100755 index 31175909..00000000 --- a/src/Nox.Cli.Secrets/ProjectSecretResolver.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Nox.Core.Interfaces; -using Nox.Core.Interfaces.Configuration; -using Nox.Utilities.Secrets; - -namespace Nox.Cli.Secrets; - -public class ProjectSecretResolver: IProjectSecretResolver -{ - private readonly IPersistedSecretStore _store; - - public ProjectSecretResolver(IPersistedSecretStore store) - { - _store = store; - } - - public async Task Resolve(IDictionary variables, IProjectConfiguration projectConfig) - { - var secretKeys = variables - .Where(kv => kv.Value == null) - .Select(kv => kv.Key) - .Where(e => e.StartsWith("project.secrets.", StringComparison.OrdinalIgnoreCase)) - .Select(e => e[16..]) - .ToList(); - - if (projectConfig.Secrets == null) return; - - //Default secret ttl to 30 minutes if not set - var validFor = projectConfig.Secrets.ValidFor; - TimeSpan ttl = TimeSpan.Zero; - if (validFor != null) - { - ttl = new TimeSpan(validFor.Days ?? 0, validFor.Hours ?? 0, validFor.Minutes ?? 0, validFor.Seconds ?? 0); - } - if (ttl == TimeSpan.Zero) - { - ttl = new TimeSpan(0, 30, 0); - } - - var resolvedSecrets = new List>(); - foreach (var key in secretKeys) - { - var cachedSecret = await _store.LoadAsync($"{projectConfig.Name}.{key}", ttl); - resolvedSecrets.Add(new KeyValuePair(key, cachedSecret ?? "")); - } - - //Resolve any remaining secrets from the vaults - var unresolvedSecrets = resolvedSecrets.Where(s => s.Value == "").ToList(); - if (unresolvedSecrets.Any() && projectConfig.Secrets.Providers != null) - { - foreach (var vault in projectConfig.Secrets.Providers) - { - if (!unresolvedSecrets.Any()) break; - switch (vault.Provider.ToLower()) - { - case "azure-keyvault": - var azureVault = new AzureSecretProvider(vault.Url); - var azureSecrets = azureVault.GetSecretsAsync(unresolvedSecrets.Select(k => k.Key).ToArray()).Result; - if (azureSecrets != null) - { - if (azureSecrets.Any()) - { - resolvedSecrets.AddRange(azureSecrets); - } - foreach (var azureSecret in azureSecrets) - { - await _store.SaveAsync($"{projectConfig.Name}.{azureSecret.Key}", azureSecret.Value); - } - } - break; - } - unresolvedSecrets = resolvedSecrets.Where(s => s.Value == "").ToList(); - } - } - - if (!resolvedSecrets.Any()) return; - - foreach (var kv in resolvedSecrets) - { - variables[$"project.secrets.{kv.Key}"] = kv.Value; - } - } -} \ No newline at end of file diff --git a/src/Nox.Cli.Secrets/ServiceExtensions.cs b/src/Nox.Cli.Secrets/ServiceExtensions.cs index 7378e1d0..35eba071 100755 --- a/src/Nox.Cli.Secrets/ServiceExtensions.cs +++ b/src/Nox.Cli.Secrets/ServiceExtensions.cs @@ -5,12 +5,6 @@ namespace Nox.Cli.Secrets; public static class ServiceExtensions { - public static IServiceCollection AddProjectSecretResolver(this IServiceCollection services) - { - services.AddSingleton(); - return services; - } - public static IServiceCollection AddOrgSecretResolver(this IServiceCollection services) { services.AddSingleton(); diff --git a/src/Nox.Cli.Server/Nox.Cli.Server.csproj b/src/Nox.Cli.Server/Nox.Cli.Server.csproj index 235bbec0..ddb1901e 100755 --- a/src/Nox.Cli.Server/Nox.Cli.Server.csproj +++ b/src/Nox.Cli.Server/Nox.Cli.Server.csproj @@ -9,14 +9,14 @@ - - - - + + + + - - + + diff --git a/src/Nox.Cli.Server/Services/WorkflowContext.cs b/src/Nox.Cli.Server/Services/WorkflowContext.cs index 9bde6f83..a2e5180c 100755 --- a/src/Nox.Cli.Server/Services/WorkflowContext.cs +++ b/src/Nox.Cli.Server/Services/WorkflowContext.cs @@ -1,11 +1,11 @@ using System.Text.RegularExpressions; using Nox.Cli.Abstractions; using Nox.Cli.Abstractions.Caching; -using Nox.Cli.Abstractions.Configuration; using Nox.Cli.Abstractions.Helpers; using Nox.Cli.Secrets; using Nox.Cli.Variables; -using Nox.Core.Interfaces; +using Nox.Secrets.Abstractions; +using Nox.Solution; namespace Nox.Cli.Server.Services; @@ -107,8 +107,11 @@ public void SetState(ActionState state) } public INoxCliCacheManager? CacheManager { get => _cachManager; } - public void SetProjectConfiguration(IProjectConfiguration projectConfiguration) + + public INoxSecretsResolver? NoxSecretsResolver => throw new NotImplementedException(); + + public void SetProjectConfiguration(NoxSolution projectConfiguration) { - throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Nox.Cli.Server/appsettings.Production.json b/src/Nox.Cli.Server/appsettings.Production.json index 62603d88..864d1e30 100755 --- a/src/Nox.Cli.Server/appsettings.Production.json +++ b/src/Nox.Cli.Server/appsettings.Production.json @@ -7,7 +7,7 @@ "Microsoft.AspNetCore": "Debug" } }, - "NoxScriptsUrl": "https://nox-cli-scripts-test.ingena-int.work", + "NoxScriptsUrl": "https://noxorg.dev", "ElasticApm": { "ServerUrl": "http://localhost:8200", "SecretToken": "", diff --git a/src/Nox.Cli.Variables/ClientVariableProvider.cs b/src/Nox.Cli.Variables/ClientVariableProvider.cs index b7941e49..35357a41 100755 --- a/src/Nox.Cli.Variables/ClientVariableProvider.cs +++ b/src/Nox.Cli.Variables/ClientVariableProvider.cs @@ -4,8 +4,6 @@ using Nox.Cli.Abstractions.Caching; using Nox.Cli.Abstractions.Configuration; using Nox.Cli.Secrets; -using Nox.Core.Interfaces; -using Nox.Core.Interfaces.Configuration; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -16,23 +14,20 @@ public class ClientVariableProvider: IClientVariableProvider private readonly Regex _variableRegex = new(@"\$\{\{\s*(?[\w\.\-_:]+)\s*\}\}", RegexOptions.Compiled | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); private readonly Dictionary _variables; - private readonly IProjectSecretResolver _projectSecretResolver; private readonly IOrgSecretResolver _orgSecretResolver; - private IProjectConfiguration? _projectConfig; + private Solution.Solution? _projectConfig; private readonly INoxCliCache? _cache; private readonly ILocalTaskExecutorConfiguration? _lteConfig; public ClientVariableProvider( IWorkflowConfiguration workflow, - IProjectSecretResolver projectSecretResolver, IOrgSecretResolver orgSecretResolver, - IProjectConfiguration? projectConfig = null, + Solution.Solution? projectConfig = null, ILocalTaskExecutorConfiguration? lteConfig = null, INoxCliCache? cache = null) { _variables = new Dictionary(StringComparer.OrdinalIgnoreCase); - _projectSecretResolver = projectSecretResolver; _orgSecretResolver = orgSecretResolver; _projectConfig = projectConfig; _lteConfig = lteConfig; @@ -97,7 +92,7 @@ public void StoreOutputVariables(INoxAction action, IDictionary ResolveAllVariables(action); } - public void SetProjectConfiguration(IProjectConfiguration projectConfig) + public void SetProjectConfiguration(Solution.Solution projectConfig) { _projectConfig = projectConfig; } @@ -115,11 +110,6 @@ public async Task ResolveForServer() await _orgSecretResolver.Resolve(_variables, _lteConfig); } - if (_projectConfig != null) - { - await _projectSecretResolver.Resolve(_variables, _projectConfig); - } - await ResolveProjectVariables(); await _variables.ResolveEnvironmentVariables(); diff --git a/src/Nox.Cli.Variables/IClientVariableProvider.cs b/src/Nox.Cli.Variables/IClientVariableProvider.cs index 73386f1c..a6303747 100755 --- a/src/Nox.Cli.Variables/IClientVariableProvider.cs +++ b/src/Nox.Cli.Variables/IClientVariableProvider.cs @@ -1,5 +1,4 @@ using Nox.Cli.Abstractions; -using Nox.Core.Interfaces; namespace Nox.Cli.Variables; @@ -10,7 +9,7 @@ public interface IClientVariableProvider IDictionary GetInputVariables(INoxAction action); IDictionary GetUnresolvedInputVariables(INoxAction action); void StoreOutputVariables(INoxAction action, IDictionary outputs); - void SetProjectConfiguration(IProjectConfiguration projectConfig); + void SetProjectConfiguration(Solution.Solution projectConfig); Task ResolveAll(); Task ResolveForServer(); diff --git a/src/Nox.Cli.Variables/Nox.Cli.Variables.csproj b/src/Nox.Cli.Variables/Nox.Cli.Variables.csproj index 2718cdd0..ef7af58e 100755 --- a/src/Nox.Cli.Variables/Nox.Cli.Variables.csproj +++ b/src/Nox.Cli.Variables/Nox.Cli.Variables.csproj @@ -14,9 +14,10 @@ - + + - + diff --git a/src/Nox.Cli.Variables/ObjectExtensions.cs b/src/Nox.Cli.Variables/ObjectExtensions.cs index cc443c46..1bb650c0 100755 --- a/src/Nox.Cli.Variables/ObjectExtensions.cs +++ b/src/Nox.Cli.Variables/ObjectExtensions.cs @@ -10,7 +10,7 @@ public static class ObjectExtensions /// The object to walk the properties of. /// The action to perform on each property. The action will be passed the full path of the property and its value as arguments. /// The current path of the object being walked. This parameter is for internal use and should not be specified when calling the method. - public static void WalkProperties(this object obj, Action propertyAction, string path = "") + public static void WalkProperties(this object? obj, Action propertyAction, string path = "") { if (obj == null) { diff --git a/src/Nox.Cli.Variables/ProjectVariableResolver.cs b/src/Nox.Cli.Variables/ProjectVariableResolver.cs index b00eab6d..d4ddf93b 100755 --- a/src/Nox.Cli.Variables/ProjectVariableResolver.cs +++ b/src/Nox.Cli.Variables/ProjectVariableResolver.cs @@ -1,20 +1,17 @@ -using Nox.Core.Interfaces; -using Nox.Core.Interfaces.Configuration; - namespace Nox.Cli.Variables; public static class ProjectVariableResolver { - public static Task ResolveProjectVariables(this IDictionary variables, IProjectConfiguration config) + public static Task ResolveProjectVariables(this IDictionary variables, Solution.Solution config) { - var projectKeys = variables + var solutionKeys = variables //.Where(pk => pk.Value == null) .Select(pk => pk.Key) - .Where(pk => pk.StartsWith("project.", StringComparison.OrdinalIgnoreCase)) - .Select(pk => pk[8..]) + .Where(pk => pk.StartsWith("solution.", StringComparison.OrdinalIgnoreCase)) + .Select(pk => pk[9..]) .ToArray(); - config.WalkProperties( (name, value) => { if (projectKeys.Contains(name, StringComparer.OrdinalIgnoreCase)) { variables[$"project.{name}"] = value; } }); + config.WalkProperties( (name, value) => { if (solutionKeys.Contains(name, StringComparer.OrdinalIgnoreCase)) { variables[$"solution.{name}"] = value; } }); return Task.CompletedTask; } diff --git a/src/Nox.Cli.Variables/TypeExtensions.cs b/src/Nox.Cli.Variables/TypeExtensions.cs index 043f1063..d32f70a8 100755 --- a/src/Nox.Cli.Variables/TypeExtensions.cs +++ b/src/Nox.Cli.Variables/TypeExtensions.cs @@ -9,13 +9,15 @@ public static bool IsSimpleType( { return type.IsPrimitive || + type.IsEnum || new Type[] { typeof(String), typeof(Decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), - typeof(Guid) + typeof(Guid), + typeof(Uri) }.Contains(type) || Convert.GetTypeCode(type) != TypeCode.Object; } diff --git a/src/Nox.Cli/Actions/NoxWorkflowContext.cs b/src/Nox.Cli/Actions/NoxWorkflowContext.cs index 2d64cc7e..573fbb11 100755 --- a/src/Nox.Cli/Actions/NoxWorkflowContext.cs +++ b/src/Nox.Cli/Actions/NoxWorkflowContext.cs @@ -6,8 +6,9 @@ using Nox.Cli.Variables; using System.Diagnostics; using Nox.Cli.Abstractions.Caching; -using Nox.Core.Exceptions; -using Nox.Core.Interfaces; +using Nox.Cli.Abstractions.Exceptions; +using Nox.Secrets.Abstractions; +using Nox.Solution; namespace Nox.Cli.Actions; @@ -17,6 +18,7 @@ public class NoxWorkflowContext : INoxWorkflowContext private readonly IDictionary _steps; private readonly IClientVariableProvider _varProvider; private readonly INoxCliCacheManager _cacheManager; + private readonly INoxSecretsResolver? _secretsResolver; private int _currentActionSequence = 0; @@ -30,16 +32,17 @@ public class NoxWorkflowContext : INoxWorkflowContext public NoxWorkflowContext( IWorkflowConfiguration workflow, - IProjectConfiguration projectConfig, - IProjectSecretResolver projectSecretResolver, + NoxSolution projectConfig, IOrgSecretResolver orgSecretResolver, INoxCliCacheManager cacheManager, - ILocalTaskExecutorConfiguration? lteConfig) + ILocalTaskExecutorConfiguration? lteConfig, + INoxSecretsResolver? secretsResolver) { WorkflowId = Guid.NewGuid(); _workflow = workflow; - _varProvider = new ClientVariableProvider(workflow, projectSecretResolver, orgSecretResolver, projectConfig, lteConfig, cacheManager.Cache); + _varProvider = new ClientVariableProvider(workflow, orgSecretResolver, projectConfig, lteConfig, cacheManager.Cache); _cacheManager = cacheManager; + _secretsResolver = secretsResolver; _steps = ParseSteps(); _currentActionSequence = 0; NextStep(); @@ -66,12 +69,11 @@ public void SetState(ActionState state) } } - public INoxCliCacheManager? CacheManager - { - get => _cacheManager; - } + public INoxCliCacheManager? CacheManager => _cacheManager; + + public INoxSecretsResolver? NoxSecretsResolver => _secretsResolver; - public void SetProjectConfiguration(IProjectConfiguration projectConfiguration) + public void SetProjectConfiguration(NoxSolution projectConfiguration) { _varProvider.SetProjectConfiguration(projectConfiguration); _varProvider.ResolveProjectVariables(); @@ -134,7 +136,7 @@ private Dictionary ParseSteps() { if (steps.ContainsKey(step.Id)) { - throw new NoxException($"Step Id {step.Id} exists more than once in your workflow configuration. Step Ids must be unique in a workflow configuration"); + throw new NoxCliException($"Step Id {step.Id} exists more than once in your workflow configuration. Step Ids must be unique in a workflow configuration"); } sequence++; diff --git a/src/Nox.Cli/Actions/NoxWorkflowExecutor.cs b/src/Nox.Cli/Actions/NoxWorkflowExecutor.cs index 9fd2a888..6a3abb95 100755 --- a/src/Nox.Cli/Actions/NoxWorkflowExecutor.cs +++ b/src/Nox.Cli/Actions/NoxWorkflowExecutor.cs @@ -4,7 +4,8 @@ using Nox.Cli.Abstractions.Configuration; using Nox.Cli.Secrets; using Nox.Cli.Server.Integration; -using Nox.Core.Interfaces; +using Nox.Secrets.Abstractions; +using Nox.Solution; using Spectre.Console; namespace Nox.Cli.Actions; @@ -14,31 +15,28 @@ public class NoxWorkflowExecutor: INoxWorkflowExecutor private readonly INoxCliServerIntegration? _serverIntegration; private readonly List _processedActions = new(); private readonly IAnsiConsole _console; - private readonly IProjectConfiguration _noxConfig; - private readonly IConfiguration _appConfig; - private readonly IProjectSecretResolver _projectSecretResolver; + private readonly NoxSolution _noxConfig; private readonly IOrgSecretResolver _orgSecretResolver; private readonly ILocalTaskExecutorConfiguration? _lteConfig; private readonly INoxCliCacheManager _cacheManager; + private readonly INoxSecretsResolver? _noxSecretsResolver; public NoxWorkflowExecutor( IAnsiConsole console, - IProjectConfiguration noxConfig, - IConfiguration appConfig, - IProjectSecretResolver projectSecretResolver, + NoxSolution noxConfig, IOrgSecretResolver orgSecretResolver, INoxCliCacheManager cacheManager, ILocalTaskExecutorConfiguration? lteConfig = null, - INoxCliServerIntegration? serverIntegration = null) + INoxCliServerIntegration? serverIntegration = null, + INoxSecretsResolver? noxSecretsResolver = null) { _serverIntegration = serverIntegration; _console = console; _noxConfig = noxConfig; - _appConfig = appConfig; _lteConfig = lteConfig; - _projectSecretResolver = projectSecretResolver; _orgSecretResolver = orgSecretResolver; _cacheManager = cacheManager; + _noxSecretsResolver = noxSecretsResolver; } public async Task Execute(IWorkflowConfiguration workflow) @@ -51,7 +49,7 @@ public async Task Execute(IWorkflowConfiguration workflow) var ctx = _console.Status() .Spinner(Spinner.Known.Clock) - .Start("Verifying the workflow script...", _ => new NoxWorkflowContext(workflow, _noxConfig, _projectSecretResolver, _orgSecretResolver, _cacheManager, _lteConfig)); + .Start("Verifying the workflow script...", _ => new NoxWorkflowContext(workflow, _noxConfig, _orgSecretResolver, _cacheManager, _lteConfig, _noxSecretsResolver)); bool success = true; diff --git a/src/Nox.Cli/Commands/Base/NoxCliCommand.cs b/src/Nox.Cli/Commands/Base/NoxCliCommand.cs index 026804be..08a6274a 100755 --- a/src/Nox.Cli/Commands/Base/NoxCliCommand.cs +++ b/src/Nox.Cli/Commands/Base/NoxCliCommand.cs @@ -1,5 +1,5 @@ using Nox.Cli.Helpers; -using Nox.Core.Interfaces; +using Nox.Solution; namespace Nox.Cli.Commands; @@ -11,34 +11,30 @@ public abstract class NoxCliCommand : AsyncCommand where T { protected readonly IAnsiConsole _console; protected readonly IConsoleWriter _consoleWriter; - protected readonly IProjectConfiguration _noxConfiguration; - protected readonly IConfiguration _configuration; + protected readonly NoxSolution _solution; public NoxCliCommand(IAnsiConsole console, IConsoleWriter consoleWriter, - IProjectConfiguration noxConfiguration, IConfiguration configuration) + NoxSolution solution) { _console = console; _consoleWriter = consoleWriter; - _noxConfiguration = noxConfiguration; - _configuration = configuration; + _solution = solution; } public override Task ExecuteAsync(CommandContext context, TSettings settings) { _console.WriteLine(); _consoleWriter.WriteInfo($"Design folder:"); - _console.WriteLine(_configuration["NoxCli:DesignFolder"]!); - if (string.IsNullOrEmpty(_noxConfiguration.Name)) + if (string.IsNullOrEmpty(_solution.Name)) { return Task.FromResult(0); } - if (_noxConfiguration.Team is null - || _noxConfiguration.Team.Developers is null - || _noxConfiguration.Team.Developers.Count == 0) + if (_solution.Team is null + || _solution.Team.Count == 0) { - throw new Exception($"The nox definition contains no 'Developers' in the 'Team' section. This section is required."); + throw new Exception($"The nox definition contains no members in the 'Team' section. This section is required."); } _console.WriteLine(); @@ -46,7 +42,7 @@ public override Task ExecuteAsync(CommandContext context, TSettings setting _console.WriteLine(); _consoleWriter.WriteInfo($"Project:"); - _console.WriteLine(_noxConfiguration.Name); + _console.WriteLine(_solution.Name); return Task.FromResult(0); } diff --git a/src/Nox.Cli/Commands/DynamicCommand.cs b/src/Nox.Cli/Commands/DynamicCommand.cs index 4be2e4ae..094cc8b9 100755 --- a/src/Nox.Cli/Commands/DynamicCommand.cs +++ b/src/Nox.Cli/Commands/DynamicCommand.cs @@ -1,5 +1,5 @@ using Nox.Cli.Abstractions; -using Nox.Core.Interfaces; +using Nox.Solution; namespace Nox.Cli.Commands; @@ -17,9 +17,8 @@ public DynamicCommand( INoxWorkflowExecutor executor, IAnsiConsole console, IConsoleWriter consoleWriter, - IProjectConfiguration noxConfiguration, - IConfiguration configuration) - : base(console, consoleWriter, noxConfiguration, configuration) + NoxSolution solution) + : base(console, consoleWriter, solution) { _executor = executor; } diff --git a/src/Nox.Cli/Extensions/ConfiguratorExtensions.cs b/src/Nox.Cli/Extensions/ConfiguratorExtensions.cs index dd90fde7..d4d15848 100755 --- a/src/Nox.Cli/Extensions/ConfiguratorExtensions.cs +++ b/src/Nox.Cli/Extensions/ConfiguratorExtensions.cs @@ -15,8 +15,10 @@ namespace Nox.Cli; internal static class ConfiguratorExtensions { - public static IConfigurator AddNoxCommands(this IConfigurator cliConfig, IServiceCollection services, bool isOnline, string onlineCacheUrl = "") + public static IConfigurator AddNoxCommands(this IConfigurator cliConfig, IServiceCollection services, bool isOnline, string? remoteUrl = null) { + var onlineCacheUrl = "https://noxorg.dev"; + if (!string.IsNullOrWhiteSpace(remoteUrl)) onlineCacheUrl = remoteUrl; var persistedTokenCache = services.BuildServiceProvider().GetRequiredService(); var cacheBuilder = new NoxCliCacheBuilder(onlineCacheUrl, persistedTokenCache) .WithBuildEventHandler((sender, args) => diff --git a/src/Nox.Cli/Extensions/ServiceCollectionExtensions.cs b/src/Nox.Cli/Extensions/ServiceCollectionExtensions.cs index db77aa4a..ede053ec 100755 --- a/src/Nox.Cli/Extensions/ServiceCollectionExtensions.cs +++ b/src/Nox.Cli/Extensions/ServiceCollectionExtensions.cs @@ -1,65 +1,33 @@ -using Nox.Core.Extensions; -using Nox.Core.Helpers; -using Nox.Core.Interfaces; -using Nox.Core.Models; -using Spectre.Console; +using System.Reflection; +using Nox.Secrets; +using Nox.Secrets.Abstractions; +using Nox.Solution; namespace Nox.Cli; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Nox.Core.Constants; -using Nox.Core.Exceptions; -using System.IO; public static class ServiceCollectionExtensions { public static IServiceCollection AddNoxCliServices(this IServiceCollection services, string[] args) { - var configuration = ConfigurationHelper.GetNoxAppSettings(); - - if (configuration == null) - { - throw new ConfigurationException("Could not load Nox configuration."); - } - - var designPath = ResolveDesignPath(args, configuration); - - configuration["NoxCli:DesignFolder"] = designPath; - - services.AddSingleton(configuration); - - if (Directory.GetFiles(designPath, FileExtension.ServiceDefinition, SearchOption.AllDirectories).Length > 0) - { - services.AddNoxConfiguration(designPath); - AnsiConsole.MarkupLine($"Found solution configuration in {Path.GetFullPath(designPath)}"); - } - else - { - services.AddSingleton(new ProjectConfiguration()); - } - return services; + return services + .AddSingleton(typeof(NoxSolution), CreateSolution) + .AddSecretsResolver(); } - - private static string ResolveDesignPath(string[] args, IConfiguration configuration) + + private static NoxSolution CreateSolution(IServiceProvider serviceProvider) { - string? path = null; - - for (var i = args.Length-1; i >= 0; i--) - { - if (args[i].Equals("--path", StringComparison.OrdinalIgnoreCase)) + return new NoxSolutionBuilder() + .AllowMissingSolutionYaml() + .OnResolveSecrets((_, args) => { - if (i + 1 < args.Length) - { - path = args[i + 1]; - } - } - } - - path ??= configuration["Nox:DefinitionRootPath"]; - - path ??= Directory.GetCurrentDirectory(); - - return path; + var secretsConfig = args.SecretsConfig; + var secretKeys = args.Variables; + var resolver = serviceProvider.GetRequiredService(); + resolver.Configure(secretsConfig!, Assembly.GetEntryAssembly()); + args.Secrets = resolver.Resolve(secretKeys!); + }) + .Build(); } } \ No newline at end of file diff --git a/src/Nox.Cli/Nox.Cli.csproj b/src/Nox.Cli/Nox.Cli.csproj index d5f9821a..ddaa7244 100755 --- a/src/Nox.Cli/Nox.Cli.csproj +++ b/src/Nox.Cli/Nox.Cli.csproj @@ -18,9 +18,9 @@ false Copyright (c) Andre Sharpe 2022 true - 1.0.18.0 - 1.0.18.0 - 1.0.18 + 1.0.19.0 + 1.0.19.0 + 1.0.19 MIT https://github.com/NoxOrg/Nox.Cli https://github.com/NoxOrg/Nox.Cli.git @@ -29,28 +29,26 @@ - - Always - - - + + - + - + + + - - + @@ -75,4 +73,9 @@ + + + design\SampleCurrency.solution.nox.yaml + + \ No newline at end of file diff --git a/src/Nox.Cli/Program.cs b/src/Nox.Cli/Program.cs index 452378e5..d98fac1a 100755 --- a/src/Nox.Cli/Program.cs +++ b/src/Nox.Cli/Program.cs @@ -2,14 +2,11 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Text; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Spectre.Console; using Spectre.Console.Cli; - using Nox.Cli; using Nox.Cli.Abstractions; -using Nox.Cli.Abstractions.Caching; using Nox.Cli.Abstractions.Exceptions; using Nox.Cli.Actions; using Nox.Cli.Caching; @@ -20,14 +17,18 @@ using Nox.Cli.Secrets; using Nox.Utilities.Secrets; -var appConfig = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - var isLoggingOut = (args.Length > 0 && args[0].ToLower().Equals("logout")); var isGettingVersion = (args.Length > 0 && args[0].ToLower().Equals("version")); +var remoteUrl = string.Empty; + +var remoteUrlArg = args.FirstOrDefault(arg => arg.StartsWith("--remoteUrl=")); +if (remoteUrlArg != null) +{ + remoteUrl = remoteUrlArg.Replace("--remoteUrl=", ""); +} + if (!isGettingVersion || args.Length == 0) { var installedVersion = VersionChecker.GetInstalledNoxCliVersion(); @@ -39,17 +40,15 @@ } var isOnline = InternetChecker.CheckForInternet(); - var services = new ServiceCollection(); services.AddSingleton(); services.AddSingleton(); -services.AddTransient(); services.AddNoxTokenCache(); services.AddNoxCliServices(args); services.AddPersistedSecretStore(); -services.AddProjectSecretResolver(); services.AddOrgSecretResolver(); +services.AddTransient(); services.AddAutoMapper(Assembly.GetExecutingAssembly()); var registrar = new TypeRegistrar(services); @@ -68,7 +67,7 @@ if(!isGettingVersion && !isLoggingOut) { - config.AddNoxCommands(services, isOnline, appConfig["OnlineScriptsUrl"]!); + config.AddNoxCommands(services, isOnline, remoteUrl); } config.AddCommand("logout") diff --git a/src/Nox.Cli/Properties/launchSettings.json b/src/Nox.Cli/Properties/launchSettings.json index b861262e..da3c438d 100755 --- a/src/Nox.Cli/Properties/launchSettings.json +++ b/src/Nox.Cli/Properties/launchSettings.json @@ -3,7 +3,7 @@ "Nox.Cli": { "commandName": "Project", "commandLineArgs": "init solution", - "workingDirectory": "$(ProjectDir)", + "workingDirectory": "/home/jan/demo/CliDemo", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Nox.Cli/Properties/launchSettings.json.default b/src/Nox.Cli/Properties/launchSettings.json.default deleted file mode 100755 index b861262e..00000000 --- a/src/Nox.Cli/Properties/launchSettings.json.default +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "Nox.Cli": { - "commandName": "Project", - "commandLineArgs": "init solution", - "workingDirectory": "$(ProjectDir)", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file diff --git a/src/Nox.Cli/Properties/launchSettings.json.init-solution b/src/Nox.Cli/Properties/launchSettings.json.init-solution deleted file mode 100755 index 0660aab5..00000000 --- a/src/Nox.Cli/Properties/launchSettings.json.init-solution +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "Nox.Cli": { - "commandName": "Project", - "commandLineArgs": "init solution", - "workingDirectory": "/home/jan/demo/Nox.WeatherTwo", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file diff --git a/src/Nox.Cli/Properties/launchSettings.json.test-cname b/src/Nox.Cli/Properties/launchSettings.json.test-cname deleted file mode 100755 index 0ef7b534..00000000 --- a/src/Nox.Cli/Properties/launchSettings.json.test-cname +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "Nox.Cli": { - "commandName": "Project", - "commandLineArgs": "test find-cname-record --path /home/jan/Projects/IWG/Nox.WeatherOne/.nox/design", - "workingDirectory": "$(ProjectDir)", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file diff --git a/src/Nox.Cli/Services/VersionChecker.cs b/src/Nox.Cli/Services/VersionChecker.cs index 10200401..e56c5f95 100755 --- a/src/Nox.Cli/Services/VersionChecker.cs +++ b/src/Nox.Cli/Services/VersionChecker.cs @@ -26,7 +26,10 @@ public static void CheckForLatestVersion() if (latestVersion.FirstOrDefault() == 'v') latestVersion = latestVersion[1..]; // remove the 'v' prefix. equivalent to `latest.Substring(1, latest.Length - 1)` - if (installedVersion != latestVersion) + var installedVersionNo = Convert.ToInt32(installedVersion.Replace(".", "")); + var latestVersionNo = Convert.ToInt32(latestVersion.Replace(".", "")); + + if (installedVersionNo < latestVersionNo) AnsiConsole.MarkupLine(@$"{Environment.NewLine}[bold underline seagreen1]This version of NOX.Cli ({installedVersion}) is older than that of the latest version ({latestVersion}) Update the tools for the latest features and bug fixes (`dotnet tool update -g Nox.Cli`).[/]{Environment.NewLine}"); } catch (Exception) diff --git a/src/Nox.Cli/appsettings.Development.json b/src/Nox.Cli/appsettings.Development.json deleted file mode 100755 index e56cb33d..00000000 --- a/src/Nox.Cli/appsettings.Development.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Nox": { - "DefinitionRootPath": "./", - "KeyVaultUri": "https://we-key-Nox-02.vault.azure.net/" - }, - "OnlineScriptsUrl": "http://localhost:9000" -} \ No newline at end of file diff --git a/src/Nox.Cli/appsettings.Production.json b/src/Nox.Cli/appsettings.Production.json deleted file mode 100755 index 287263a1..00000000 --- a/src/Nox.Cli/appsettings.Production.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Nox": { - "DefinitionRootPath": "./", - "KeyVaultUri": "https://we-key-Nox-02.vault.azure.net/" - }, - "OnlineScriptsUrl": "https://nox-cli-scripts-test.ingena.work/" -} \ No newline at end of file diff --git a/src/Nox.Cli/appsettings.json b/src/Nox.Cli/appsettings.json deleted file mode 100755 index b1f1384e..00000000 --- a/src/Nox.Cli/appsettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Nox": { - "DefinitionRootPath": "./", - "KeyVaultUri": "https://we-key-Nox-02.vault.azure.net/" - }, - "OnlineScriptsUrl": "http://localhost:9000" -} \ No newline at end of file diff --git a/src/Nox.Cli/appsettings.nox-cli-scripts.json b/src/Nox.Cli/appsettings.nox-cli-scripts.json deleted file mode 100755 index a42fc427..00000000 --- a/src/Nox.Cli/appsettings.nox-cli-scripts.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Nox": { - "DefinitionRootPath": "../../samples/Nox.Cli.Scripts", - "KeyVaultUri": "https://we-key-Nox-02.vault.azure.net/" - }, - "OnlineWorkflowUrl": "http://localhost:9000/workflows" -} \ No newline at end of file diff --git a/src/Nox.Cli/appsettings.nox-demo.json b/src/Nox.Cli/appsettings.nox-demo.json deleted file mode 100755 index d517b60d..00000000 --- a/src/Nox.Cli/appsettings.nox-demo.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Nox": { - "DefinitionRootPath": "/home/jan/NoxDemo/Design", - "KeyVaultUri": "https://we-key-Nox-02.vault.azure.net/" - }, - "OnlineWorkflowUrl": "http://localhost:9000/workflows" -} \ No newline at end of file diff --git a/tests/Nox.Cli.Server.Tests/Nox.Cli.Server.Tests.csproj b/tests/Nox.Cli.Server.Tests/Nox.Cli.Server.Tests.csproj index d004176a..cced5a6e 100755 --- a/tests/Nox.Cli.Server.Tests/Nox.Cli.Server.Tests.csproj +++ b/tests/Nox.Cli.Server.Tests/Nox.Cli.Server.Tests.csproj @@ -10,9 +10,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive