diff --git a/CICD.Tools.CatalogUpload.Lib/CICD.Tools.CatalogUpload.Lib.csproj b/CICD.Tools.CatalogUpload.Lib/CICD.Tools.CatalogUpload.Lib.csproj index 91713f5..fd040fb 100644 --- a/CICD.Tools.CatalogUpload.Lib/CICD.Tools.CatalogUpload.Lib.csproj +++ b/CICD.Tools.CatalogUpload.Lib/CICD.Tools.CatalogUpload.Lib.csproj @@ -4,8 +4,8 @@ disable disable Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib - 1.0.1 - 1.0.1 + 1.0.1-alpaha1 + 1.0.1-alpaha1 Skyline;DataMiner https://skyline.be README.md diff --git a/CICD.Tools.CatalogUpload.Lib/CatalogArtifact.cs b/CICD.Tools.CatalogUpload.Lib/CatalogArtifact.cs index dcc6d78..915dc4c 100644 --- a/CICD.Tools.CatalogUpload.Lib/CatalogArtifact.cs +++ b/CICD.Tools.CatalogUpload.Lib/CatalogArtifact.cs @@ -5,19 +5,17 @@ using System.Threading; using System.Threading.Tasks; - using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Skyline.DataMiner.CICD.FileSystem; - using Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib.HttpArtifactUploadModels; /// /// Allows Uploading an artifact to the Catalog using one of the below in order of priority: /// - provided key in upload argument (unix/win) - /// - key stored as an Environment Variable called "dmcatalogkey". (unix/win) - /// - key configured using Skyline.DataMiner.CICD.Tools.WinEncryptedKeys called "dmcatalogkey_encrypted" (windows only) + /// - key stored as an Environment Variable called "dmcatalogtoken". (unix/win) + /// - key configured using Skyline.DataMiner.CICD.Tools.WinEncryptedKeys called "dmcatalogtoken_encrypted" (windows only) /// public class CatalogArtifact { @@ -26,7 +24,7 @@ public class CatalogArtifact /// /// Creates an instance of . - /// It searches for an optional dmcatalogkey in the "dmcatalogkey" or "dmcatalogkey_encrypted" Environment Variable. + /// It searches for an optional dmCatalogToken in the "dmcatalogtoken" or "dmcatalogtoken_encrypted" Environment Variable. /// /// Path to the ".dmapp" or ".dmprotocol" file. /// An instance of used for communication. @@ -46,7 +44,7 @@ public CatalogArtifact(string pathToArtifact, ICatalogService service, IFileSyst /// /// Creates an instance of using a default HttpCatalogService with a new HttpClient for communication. - /// It searches for an optional dmcatalogkey in the "dmcatalogkey" or "dmcatalogkey_encrypted" Environment Variable for authentication. + /// It searches for an optional dmCatalogToken in the "dmcatalogtoken" or "dmcatalogtoken_encrypted" Environment Variable for authentication. /// WARNING: when wishing to upload several Artifacts it's recommended to use the CatalogArtifact(string pathToArtifact, ICatalogService service, IFileSystem fileSystem, ILogger logger). /// /// Path to the ".dmapp" or ".dmprotocol" file. @@ -54,7 +52,6 @@ public CatalogArtifact(string pathToArtifact, ICatalogService service, IFileSyst /// Contains package metadata. public CatalogArtifact(string pathToArtifact, ILogger logger, CatalogMetaData metaData) : this(pathToArtifact, new HttpCatalogService(new System.Net.Http.HttpClient(), logger), FileSystem.Instance, logger, metaData) { - } /// @@ -80,16 +77,16 @@ public void CancelUpload() } /// - /// Uploads to the private catalog using the provided dmcatalogkey. + /// Uploads to the private catalog using the provided dmCatalogToken. /// - /// A provided token for the agent or organization as defined in https://admin.dataminer.services/. + /// A provided token for the agent or organization as defined in https://admin.dataminer.services/. /// If the upload was successful or not. - public async Task UploadAsync(string dmcatalogkey) + public async Task UploadAsync(string dmCatalogToken) { _logger.LogDebug($"Uploading {PathToArtifact}..."); byte[] packageData = Fs.File.ReadAllBytes(PathToArtifact); - var result = await catalogService.ArtifactUploadAsync(packageData, dmcatalogkey, metaData, cancellationTokenSource.Token).ConfigureAwait(false); + var result = await catalogService.ArtifactUploadAsync(packageData, dmCatalogToken, metaData, cancellationTokenSource.Token).ConfigureAwait(false); _logger.LogDebug($"Finished Uploading {PathToArtifact}"); _logger.LogInformation(JsonConvert.SerializeObject(result)); @@ -97,16 +94,16 @@ public async Task UploadAsync(string dmcatalogkey) } /// - /// Uploads to the private catalog using the dmcatalogkey or dmcatalogkey_encrypted environment variable as the token. + /// Uploads to the private catalog using the dmcatalogtoken or dmcatalogtoken environment variable as the token. /// /// If the upload was successful or not. /// Uploading failed. /// Uploading failed due to invalid Token. - public async Task UploadAsync() + public async Task UploadAsync() { if (String.IsNullOrWhiteSpace(keyFromEnv)) { - throw new InvalidOperationException("Uploading failed, missing token in environment variable dmcatalogkey or dmcatalogkey_encrypted."); + throw new InvalidOperationException("Uploading failed, missing token in environment variable dmcatalogtoken or dmcatalogtoken_encrypted."); } _logger.LogDebug($"Attempting upload with Environment Variable as token for artifact: {PathToArtifact}..."); @@ -115,40 +112,49 @@ public async Task UploadAsync() /// /// Attempts to find the necessary API key in Environment Variables. In order of priority: - /// - key stored as an Environment Variable called "dmcatalogkey". (unix/win) - /// - key configured using Skyline.DataMiner.CICD.Tools.WinEncryptedKeys called "dmcatalogkey_encrypted" (windows only) + /// - key stored as an Environment Variable called "dmcatalogtoken". (unix/win) + /// - key configured using Skyline.DataMiner.CICD.Tools.WinEncryptedKeys called "dmcatalogtoken_encrypted" (windows only) /// private void TryFindEnvironmentKey() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var encryptedKey = WinEncryptedKeys.Lib.Keys.RetrieveKey("dmcatalogkey_encrypted"); - if (encryptedKey != null) + try { - string keyFromWinEncryptedKeys = encryptedKey.ToString(); - - if (!String.IsNullOrWhiteSpace(keyFromWinEncryptedKeys)) + var encryptedKey = WinEncryptedKeys.Lib.Keys.RetrieveKey("dmcatalogtoken_encrypted"); + if (encryptedKey != null) { - _logger.LogDebug("OK: Found token in Env Variable: 'dmcatalogkey_encrypted' created by WinEncryptedKeys."); - keyFromEnv = keyFromWinEncryptedKeys; + string keyFromWinEncryptedKeys = new System.Net.NetworkCredential(string.Empty, encryptedKey).Password; + + if (!String.IsNullOrWhiteSpace(keyFromWinEncryptedKeys)) + { + _logger.LogDebug("OK: Found token in Env Variable: 'dmcatalogtoken_encrypted' created by WinEncryptedKeys."); + keyFromEnv = keyFromWinEncryptedKeys; + } } } + catch (InvalidOperationException) + { + // Gobble up, no key means we try the next thing. + } } - var config = new ConfigurationBuilder() - .AddUserSecrets() - .Build(); - string keyFromEnvironment = config["dmcatalogkey"]; + //var config = new ConfigurationBuilder() + // .AddUserSecrets() + // .Build(); + //string keyFromEnvironment = config["dmcatalogtoken"]; + + string keyFromEnvironment = Environment.GetEnvironmentVariable("dmcatalogtoken"); if (!String.IsNullOrWhiteSpace(keyFromEnvironment)) { if (!String.IsNullOrWhiteSpace(keyFromEnv)) { - _logger.LogDebug("OK: Overriding 'dmcatalogkey_encrypted' with found token in Env Variable: 'dmcatalogkey'."); + _logger.LogDebug("OK: Overriding 'dmcatalogtoken_encrypted' with found token in Env Variable: 'dmcatalogtoken'."); } else { - _logger.LogDebug("OK: Found token in Env Variable: 'dmcatalogkey'."); + _logger.LogDebug("OK: Found token in Env Variable: 'dmcatalogtoken'."); } keyFromEnv = keyFromEnvironment; diff --git a/CICD.Tools.CatalogUpload.Lib/CatalogService/CatalogMetaData.cs b/CICD.Tools.CatalogUpload.Lib/CatalogService/CatalogMetaData.cs index 29045f5..6e6cc3f 100644 --- a/CICD.Tools.CatalogUpload.Lib/CatalogService/CatalogMetaData.cs +++ b/CICD.Tools.CatalogUpload.Lib/CatalogService/CatalogMetaData.cs @@ -4,26 +4,58 @@ using System.IO; using System.IO.Compression; using System.Linq; + using System.Text.RegularExpressions; using System.Xml.Linq; + /// + /// Represents all metadata for a package. + /// public class CatalogMetaData { - public string Branch { get; set; } = ""; + private bool artifactHadBuildNumber; - public string CommitterMail { get; set; } = ""; + /// + /// The Branch/Range/Category this version belongs to. Defaults to "main" + /// + public string Branch { get; set; } = "main"; - public string ContentType { get; set; } = ""; + /// + /// The e-mail address of the Author, often the committer of a GIT Tag on the sourcecode making the package. + /// + public string CommitterMail { get; set; } - public string Identifier { get; set; } = ""; + /// + /// The type of content, as understood by the ArtifactUpload. + /// + public string ContentType { get; set; } - public bool IsPreRelease { get; set; } + /// + /// An global, readable, unique identifier for this package. This is often the URI to the sourcecode. + /// + public string Identifier { get; set; } - public string Name { get; set; } = ""; + /// + /// The Name of the Package + /// + public string Name { get; set; } - public string ReleaseUri { get; set; } = ""; + /// + /// A URI leading to the release notes of this package version. + /// + public string ReleaseUri { get; set; } - public string Version { get; set; } = ""; + /// + /// The version of the package. + /// + public string Version { get; set; } + /// + /// Creates a partial CataLogMetaData using any information it can from the artifact itself. Check the items for null and complete. + /// + /// Path to the artifact. + /// An instance of .> + /// Provided path should not be null + /// Expected data was not present in the Artifact. public static CatalogMetaData FromArtifact(string pathToArtifact) { CatalogMetaData meta; @@ -49,13 +81,16 @@ public static CatalogMetaData FromArtifact(string pathToArtifact) return meta; } - // Used during unit testing to assert data. - public override bool Equals(object? obj) + /// + /// Used during unit testing to assert data. + /// + /// + /// + public override bool Equals(object obj) { return obj is CatalogMetaData data && Version == data.Version && Branch == data.Branch && - IsPreRelease == data.IsPreRelease && Identifier == data.Identifier && Name == data.Name && CommitterMail == data.CommitterMail && @@ -63,10 +98,40 @@ public override bool Equals(object? obj) ContentType == data.ContentType; } - // Needed to match with Equals + /// + /// Needed to match with Equals + /// + /// public override int GetHashCode() { - return HashCode.Combine(Version, Branch, IsPreRelease, Identifier, Name, ContentType, CommitterMail, ReleaseUri); + return HashCode.Combine(Version, Branch, Identifier, Name, ContentType, CommitterMail, ReleaseUri); + } + + /// + /// Whether this is a pre-release or a full release. This is automatically decided from the Version and artifact content. + /// + public bool IsPreRelease() + { + // Might need to check for protocols how to handle this. "_BX" probably added to versions? + // Should we allow a force set/override? + + if (!artifactHadBuildNumber) + { + // Not a version from .dmapp --> assume semantic versioning + if (!Regex.IsMatch(Version, "^[0-9]+.[0-9]+.[0-9]+(-CU[0-9]+)?$")) + { + return Version.Contains('-'); + } + else + { + // Versioning that dataminer uses with the -CU. + return Version.StartsWith("0.0.0-"); + } + } + else + { + return true; + } } private static CatalogMetaData FromDmapp(string pathToDmapp) @@ -97,10 +162,11 @@ private static CatalogMetaData FromDmapp(string pathToDmapp) */ string appInfoRaw; + string contentType; using (var zipFile = ZipFile.OpenRead(pathToDmapp)) { - var foundFile = zipFile.Entries.FirstOrDefault(x => x.Name.Equals("AppInfo.xml", StringComparison.InvariantCulture)); + ZipArchiveEntry foundFile = zipFile.GetEntry("AppInfo.xml"); if (foundFile == null) throw new InvalidOperationException("Could not find AppInfo.xml in the .dmapp."); using (var stream = foundFile.Open()) @@ -110,12 +176,41 @@ private static CatalogMetaData FromDmapp(string pathToDmapp) appInfoRaw = memoryStream.ReadToEnd(); } } + + ContentType contentFromPackagContent = new ContentType(zipFile); + contentType = contentFromPackagContent.Value; } CatalogMetaData meta = new CatalogMetaData(); - var appInfo = XDocument.Parse(appInfoRaw); + var appInfo = XDocument.Parse(appInfoRaw).Root; meta.Name = appInfo.Element("DisplayName")?.Value; - meta.Version = appInfo.Element("Version")?.Value; + + var buildNumber = appInfo.Element("Build")?.Value; + + if (!String.IsNullOrWhiteSpace(buildNumber)) + { + meta.artifactHadBuildNumber = true; + // Throw away the CU version. If we have a build number it's a pre-release. + string version = appInfo.Element("Version")?.Value; + + if (version != null) + { + if (version.Contains("-CU")) + { + // Throw away the CU version. If we have a build number it's a pre-release. + version = version.Split('-')[0]; + } + + meta.Version = version + "-B" + buildNumber; + } + } + else + { + meta.artifactHadBuildNumber = false; + meta.Version = appInfo.Element("Version")?.Value; + } + + meta.ContentType = contentType; return meta; } @@ -123,8 +218,8 @@ private static CatalogMetaData FromDmprotocol(string pathToDmprotocol) { // Description.txt /* -Protocol Name: Microsoft Platform -Protocol Version: 6.0.0.4_B2 + Protocol Name: Microsoft Platform + Protocol Version: 6.0.0.4_B2 * */ string descriptionText; @@ -145,7 +240,7 @@ private static CatalogMetaData FromDmprotocol(string pathToDmprotocol) CatalogMetaData meta = new CatalogMetaData(); - using(var reader = new StringReader(descriptionText)) + using (var reader = new StringReader(descriptionText)) { var line = reader.ReadLine(); var splitLine = line.Split(':'); @@ -155,14 +250,17 @@ private static CatalogMetaData FromDmprotocol(string pathToDmprotocol) case "Protocol Name": meta.Name = splitLine[1]; break; + case "Protocol Version": meta.Version = splitLine[1]; break; + default: break; } } + meta.ContentType = "protocol"; return meta; } } diff --git a/CICD.Tools.CatalogUpload.Lib/CatalogService/CatalogServiceFactory.cs b/CICD.Tools.CatalogUpload.Lib/CatalogService/CatalogServiceFactory.cs index aff2adb..e48542c 100644 --- a/CICD.Tools.CatalogUpload.Lib/CatalogService/CatalogServiceFactory.cs +++ b/CICD.Tools.CatalogUpload.Lib/CatalogService/CatalogServiceFactory.cs @@ -1,9 +1,9 @@ -using System.Net.Http; +namespace Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib +{ + using System.Net.Http; -using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging; -namespace Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib.CatalogService -{ /// /// Creates instances of to communicate with the Skyline DataMiner Catalog (https://catalog.dataminer.services/). /// diff --git a/CICD.Tools.CatalogUpload.Lib/CatalogService/ContentType.cs b/CICD.Tools.CatalogUpload.Lib/CatalogService/ContentType.cs new file mode 100644 index 0000000..f0ef057 --- /dev/null +++ b/CICD.Tools.CatalogUpload.Lib/CatalogService/ContentType.cs @@ -0,0 +1,147 @@ +namespace Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib +{ + using System.Collections.Generic; + using System.IO.Compression; + using System.Linq; + + /// + /// As known by the Azure Artifact Uploader + /// + internal enum ArtifactContentType + { + Unknown = 0, + DmScript = 1, + Package = 2, + Visio = 3, + Function = 4, + Dashboard = 5, + CustomSolution = 6, + Example = 7, + CompanionFile = 8, + ProfileLoadScript = 9, + GQIOperator = 10, + ProcessActivity = 11, + DataGrabber = 12, + AdHocDataSource = 13, + } + + [System.Flags] + internal enum Content + { + None = 0b_0000_0000, // 0 + HasAutomation = 0b_0000_0001, // 1 + HasDashboards = 0b_0000_0010, // 2 + HasProtocols = 0b_0000_0100, // 4 + HasOtherAppPackages = 0b_0000_1000, // 8 + HasCompanionFiles = 0b_0001_0000, // 16 + HasFunctions = 0b_0010_0000, // 32 + HasVisios = 0b_0100_0000 // 64 + } + + internal class ContentType + { + private readonly IEnumerable allContentFiles; + + private readonly FileSystem.IPathIO path; + private readonly ZipArchive zipFile; + + public ContentType(ZipArchive zipFile) + { + path = FileSystem.FileSystem.Instance.Path; + + this.zipFile = zipFile; + this.allContentFiles = zipFile.Entries.Where(p => p.FullName.StartsWith("AppInstallContent")); + + // Consider this a best effort currently. + Content content = Content.None; + + if (HasAutomationScripts()) content |= Content.HasAutomation; + if (HasDashboards()) content |= Content.HasDashboards; + if (HasProtocols()) content |= Content.HasProtocols; + if (HasOtherAppPackages()) content |= Content.HasOtherAppPackages; + if (HasCompanionFiles()) content |= Content.HasCompanionFiles; + if (HasFunctions()) content |= Content.HasFunctions; + if (HasVisios()) content |= Content.HasVisios; + + switch (content) + { + case Content.HasAutomation: + case Content.HasAutomation | Content.HasCompanionFiles: + Value = ArtifactContentType.DmScript.ToString(); + break; + + case Content.HasDashboards: + case Content.HasDashboards | Content.HasCompanionFiles: + Value = ArtifactContentType.Dashboard.ToString(); + break; + + case Content.HasOtherAppPackages: + Value = ArtifactContentType.Package.ToString(); + break; + + case Content.HasCompanionFiles: + Value = ArtifactContentType.CompanionFile.ToString(); + break; + + case Content.HasFunctions: + case Content.HasFunctions | Content.HasCompanionFiles: + Value = ArtifactContentType.Function.ToString(); + break; + + case Content.HasVisios: + case Content.HasVisios | Content.HasCompanionFiles: + Value = ArtifactContentType.Visio.ToString(); + break; + + case Content.HasProtocols: + case Content.HasProtocols | Content.HasCompanionFiles: + Value = ArtifactContentType.Package.ToString(); + break; + + default: + // Everything else is going to be a combination of more than one item so we can consider that to be a "package" + Value = ArtifactContentType.Package.ToString(); + break; + } + } + + public string Value { get; set; } = "Unknown"; + + private bool HasAutomationScripts() + { + // Note: ongoing discussions on better defining the different automationscripts ProfileLoadScript, GQIOperator, ProcessActivity, DataGrabber, AdHocDataSource, ... + return allContentFiles.FirstOrDefault(p => p.FullName.StartsWith(path.Combine("AppInstallContent", "Scripts"))) != null; + } + + private bool HasCompanionFiles() + { + return allContentFiles.FirstOrDefault(p => p.FullName.StartsWith(path.Combine("AppInstallContent", "CompanionFiles"))) != null; + } + + private bool HasDashboards() + { + return allContentFiles.FirstOrDefault(p => p.FullName.StartsWith(path.Combine("AppInstallContent", "Dashboards"))) != null; + } + + private bool HasFunctions() + { + return allContentFiles.FirstOrDefault(p => p.FullName.StartsWith(path.Combine("AppInstallContent", "Functions"))) != null; + } + + private bool HasOtherAppPackages() + { + return allContentFiles.FirstOrDefault(p => p.FullName.StartsWith(path.Combine("AppInstallContent", "AppPackages"))) != null; + } + + private bool HasProtocols() + { + // Need to check deeper to make sure it doesn't only have a .vsdx. That would turn it into a Visio. + return allContentFiles.FirstOrDefault(p => p.FullName.EndsWith(".xml") && p.FullName.StartsWith(path.Combine("AppInstallContent", "Protocols"))) != null; + } + + private bool HasVisios() + { + return allContentFiles.FirstOrDefault(p => p.Name.EndsWith(".vsdx")) != null; + } + } +} \ No newline at end of file diff --git a/CICD.Tools.CatalogUpload.Lib/CatalogService/HttpCatalogService.cs b/CICD.Tools.CatalogUpload.Lib/CatalogService/HttpCatalogService.cs index 07550b0..eefd260 100644 --- a/CICD.Tools.CatalogUpload.Lib/CatalogService/HttpCatalogService.cs +++ b/CICD.Tools.CatalogUpload.Lib/CatalogService/HttpCatalogService.cs @@ -12,8 +12,6 @@ using Newtonsoft.Json; - using Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib.HttpArtifactUploadModels; - internal sealed class HttpCatalogService : ICatalogService, IDisposable { private const string UploadPath = "api/key-artifact-upload/v1-0/private/artifact"; @@ -26,7 +24,7 @@ public HttpCatalogService(HttpClient httpClient, ILogger logger) _httpClient = httpClient; } - public async Task ArtifactUploadAsync(byte[] package, string key, CatalogMetaData catalog, CancellationToken cancellationToken) + public async Task ArtifactUploadAsync(byte[] package, string key, CatalogMetaData catalog, CancellationToken cancellationToken) { using var formData = new MultipartFormDataContent(); formData.Headers.Add("Ocp-Apim-Subscription-Key", key); @@ -35,7 +33,7 @@ public async Task ArtifactUploadAsync(byte[] package, string key, formData.Add(new StringContent(catalog.ContentType), "contentType"); formData.Add(new StringContent(catalog.Branch), "branch"); formData.Add(new StringContent(catalog.Identifier), "identifier"); - formData.Add(new StringContent(catalog.IsPreRelease ? "true" : "false"), "isprerelease"); + formData.Add(new StringContent(catalog.IsPreRelease() ? "true" : "false"), "isprerelease"); formData.Add(new StringContent(catalog.CommitterMail), "developer"); formData.Add(new StringContent(catalog.ReleaseUri), "releasepath"); @@ -55,7 +53,7 @@ public async Task ArtifactUploadAsync(byte[] package, string key, if (response.IsSuccessStatusCode) { _logger.LogDebug($"The upload api returned a {response.StatusCode} response. Body: {response.Content}"); - return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(cancellationToken)); + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(cancellationToken)); } _logger.LogError($"The upload api returned a {response.StatusCode} response. Body: {response.Content}"); diff --git a/CICD.Tools.CatalogUpload.Lib/CatalogService/ICatalogService.cs b/CICD.Tools.CatalogUpload.Lib/CatalogService/ICatalogService.cs index 00fdabb..a9979e4 100644 --- a/CICD.Tools.CatalogUpload.Lib/CatalogService/ICatalogService.cs +++ b/CICD.Tools.CatalogUpload.Lib/CatalogService/ICatalogService.cs @@ -3,10 +3,19 @@ using System.Threading; using System.Threading.Tasks; - using Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib.HttpArtifactUploadModels; - + /// + /// Service interface used to actually upload and artifact. + /// public interface ICatalogService { - Task ArtifactUploadAsync(byte[] package, string key, CatalogMetaData catalog, CancellationToken cancellationToken); + /// + /// Uploads an artifact to an external store. + /// + /// A byte array with the package content. + /// A unique token used for communications. + /// An instance of containing additional data for upload and registration. + /// An instance of to cancel an ongoing upload. + /// + Task ArtifactUploadAsync(byte[] package, string key, CatalogMetaData catalog, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/CICD.Tools.CatalogUpload.Lib/HttpArtifactUploadModels/ArtifactModel.cs b/CICD.Tools.CatalogUpload.Lib/HttpArtifactUploadModels/ArtifactUploadResult.cs similarity index 77% rename from CICD.Tools.CatalogUpload.Lib/HttpArtifactUploadModels/ArtifactModel.cs rename to CICD.Tools.CatalogUpload.Lib/HttpArtifactUploadModels/ArtifactUploadResult.cs index 6b1b064..77dbb52 100644 --- a/CICD.Tools.CatalogUpload.Lib/HttpArtifactUploadModels/ArtifactModel.cs +++ b/CICD.Tools.CatalogUpload.Lib/HttpArtifactUploadModels/ArtifactUploadResult.cs @@ -1,11 +1,13 @@ -namespace Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib.HttpArtifactUploadModels +#nullable enable + +namespace Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib { using Newtonsoft.Json; /// /// Artifact information returned from uploading an artifact to the catalog. /// - public class ArtifactModel + public class ArtifactUploadResult { /// /// The GUID that represents the ID of the artifact in our cloud storage database. Can be used to download or deploy the artifact. diff --git a/CICD.Tools.CatalogUpload.LibTests/CICD.Tools.CatalogUpload.LibTests.csproj b/CICD.Tools.CatalogUpload.LibTests/CICD.Tools.CatalogUpload.LibTests.csproj new file mode 100644 index 0000000..99895e4 --- /dev/null +++ b/CICD.Tools.CatalogUpload.LibTests/CICD.Tools.CatalogUpload.LibTests.csproj @@ -0,0 +1,41 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/CICD.Tools.CatalogUpload.LibTests/CatalogArtifactTests.cs b/CICD.Tools.CatalogUpload.LibTests/CatalogArtifactTests.cs new file mode 100644 index 0000000..29de22b --- /dev/null +++ b/CICD.Tools.CatalogUpload.LibTests/CatalogArtifactTests.cs @@ -0,0 +1,207 @@ +namespace Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib.Tests +{ + using System; + using System.Threading.Tasks; + + using FluentAssertions; + + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Mock; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + using Moq; + + using Skyline.DataMiner.CICD.FileSystem; + using Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib; + + [TestClass()] + public class CatalogArtifactTests + { + private Mock fakeLogger; + private ILogger logger; + + [TestInitialize()] + public void Initialize() + { + fakeLogger = new Mock(); + IServiceCollection services = new ServiceCollection(); + + services.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddConsole(); + builder.AddMock(fakeLogger); + }); + + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + logger = loggerFactory.CreateLogger("TestLogger"); + } + + [TestMethod()] + public async Task UploadAsyncTest_NoToken() + { + string pathToArtifact = ""; + Mock fakeService = new Mock(); + Mock fakeFileSystem = new Mock(); + + CatalogMetaData metaData = new CatalogMetaData() + { + Branch = "1.0.0.X", + CommitterMail = "thunder@skyline.be", + ContentType = "DMScript", + Identifier = "uniqueIdentifier", + Name = "Name", + ReleaseUri = "pathToNotes", + Version = "1.0.0.1-alpha" + }; + + CatalogArtifact artifactModel = new CatalogArtifact(pathToArtifact, fakeService.Object, fakeFileSystem.Object, logger, metaData); + Func uploadAction = async () => { await artifactModel.UploadAsync(); }; + await uploadAction.Should().ThrowAsync().WithMessage("*missing token*"); + } + + [TestMethod()] + public async Task UploadAsyncTest_ProvidedEncryptedTokenEnvironment() + { + // Arrange + string pathToArtifact = ""; + Mock fakeService = new Mock(); + Mock fakeFileSystem = new Mock(); + + CatalogMetaData metaData = new CatalogMetaData() + { + Branch = "1.0.0.X", + CommitterMail = "thunder@skyline.be", + ContentType = "DMScript", + Identifier = "uniqueIdentifier", + Name = "Name", + ReleaseUri = "pathToNotes", + Version = "1.0.0.1-alpha" + }; + + Mock fakeFile = new Mock(); + fakeFile.Setup(p => p.ReadAllBytes(It.IsAny())).Returns(new byte[0]); + fakeFileSystem.Setup(p => p.File).Returns(fakeFile.Object); + + ArtifactUploadResult model = new ArtifactUploadResult(); + model.ArtifactId = "10"; + + fakeService.Setup(p => p.ArtifactUploadAsync(It.IsAny(), "encryptedFake", metaData, It.IsAny())).ReturnsAsync(model); + + try + { + WinEncryptedKeys.Lib.Keys.SetKey("dmcatalogtoken_encrypted", "encryptedFake"); + + // Act + CatalogArtifact artifactModel = new CatalogArtifact(pathToArtifact, fakeService.Object, fakeFileSystem.Object, logger, metaData); + var result = await artifactModel.UploadAsync(); + + // Assert + result.ArtifactId.Should().Be("10"); + + fakeLogger.VerifyLog().InformationWasCalled().MessageEquals(@"{""artifactId"":""10""}"); + + fakeService.VerifyAll(); + fakeFileSystem.VerifyAll(); + } + finally + { + Environment.SetEnvironmentVariable("dmcatalogtoken_encrypted", "", EnvironmentVariableTarget.Machine); + } + } + + [TestMethod()] + public async Task UploadAsyncTest_ProvidedTokenArgument() + { + // Arrange + string pathToArtifact = ""; + Mock fakeService = new Mock(); + Mock fakeFileSystem = new Mock(); + + CatalogMetaData metaData = new CatalogMetaData() + { + Branch = "1.0.0.X", + CommitterMail = "thunder@skyline.be", + ContentType = "DMScript", + Identifier = "uniqueIdentifier", + Name = "Name", + ReleaseUri = "pathToNotes", + Version = "1.0.0.1-alpha" + }; + + Mock fakeFile = new Mock(); + fakeFile.Setup(p => p.ReadAllBytes(It.IsAny())).Returns(new byte[0]); + fakeFileSystem.Setup(p => p.File).Returns(fakeFile.Object); + + ArtifactUploadResult model = new ArtifactUploadResult(); + model.ArtifactId = "10"; + + fakeService.Setup(p => p.ArtifactUploadAsync(It.IsAny(), "token", metaData, It.IsAny())).ReturnsAsync(model); + + // Act + CatalogArtifact artifactModel = new CatalogArtifact(pathToArtifact, fakeService.Object, fakeFileSystem.Object, logger, metaData); + var result = await artifactModel.UploadAsync("token"); + + // Assert + result.ArtifactId.Should().Be("10"); + + fakeLogger.VerifyLog().InformationWasCalled().MessageEquals(@"{""artifactId"":""10""}"); + + fakeService.VerifyAll(); + fakeFileSystem.VerifyAll(); + } + + [TestMethod()] + public async Task UploadAsyncTest_ProvidedTokenEnvironment() + { + // Arrange + string pathToArtifact = ""; + Mock fakeService = new Mock(); + Mock fakeFileSystem = new Mock(); + + CatalogMetaData metaData = new CatalogMetaData() + { + Branch = "1.0.0.X", + CommitterMail = "thunder@skyline.be", + ContentType = "DMScript", + Identifier = "uniqueIdentifier", + Name = "Name", + ReleaseUri = "pathToNotes", + Version = "1.0.0.1-alpha" + }; + + Mock fakeFile = new Mock(); + fakeFile.Setup(p => p.ReadAllBytes(It.IsAny())).Returns(new byte[0]); + fakeFileSystem.Setup(p => p.File).Returns(fakeFile.Object); + + ArtifactUploadResult model = new ArtifactUploadResult(); + model.ArtifactId = "10"; + + fakeService.Setup(p => p.ArtifactUploadAsync(It.IsAny(), "fake", metaData, It.IsAny())).ReturnsAsync(model); + + try + { + Environment.SetEnvironmentVariable("dmcatalogtoken", "fake"); + + // Act + CatalogArtifact artifactModel = new CatalogArtifact(pathToArtifact, fakeService.Object, fakeFileSystem.Object, logger, metaData); + var result = await artifactModel.UploadAsync(); + + // Assert + result.ArtifactId.Should().Be("10"); + + fakeLogger.VerifyLog().InformationWasCalled().MessageEquals(@"{""artifactId"":""10""}"); + + fakeService.VerifyAll(); + fakeFileSystem.VerifyAll(); + } + finally + { + Environment.SetEnvironmentVariable("dmcatalogtoken", String.Empty); + } + } + } +} \ No newline at end of file diff --git a/CICD.Tools.CatalogUpload.LibTests/CatalogService/CatalogMetaDataTests.cs b/CICD.Tools.CatalogUpload.LibTests/CatalogService/CatalogMetaDataTests.cs new file mode 100644 index 0000000..6ca8bcf --- /dev/null +++ b/CICD.Tools.CatalogUpload.LibTests/CatalogService/CatalogMetaDataTests.cs @@ -0,0 +1,98 @@ +using FluentAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib.Tests +{ + [TestClass()] + public class CatalogMetaDataTests + { + [TestMethod()] + public void FromArtifactTest_BuildPreReleaseDmappWithProtocols() + { + // Arrange + string pathToArtifact = "TestData/SLNetSubscriptionsBenchmarking 1.0.1_B15.dmapp"; + + // Act + CatalogMetaData result = CatalogMetaData.FromArtifact(pathToArtifact); + + // Assert + + CatalogMetaData expected = new CatalogMetaData() + { + ContentType = "Package", + Name = "SLNetSubscriptionsBenchmarking", + Version = "1.0.1-B15", + }; + + result.Should().Be(expected); + result.IsPreRelease().Should().BeTrue(); + } + + [TestMethod()] + public void FromArtifactTest_ReleaseAutomation() + { + // Arrange + string pathToArtifact = "TestData/Demo InterAppCalls 1.0.0-CU1.dmapp"; + + // Act + CatalogMetaData result = CatalogMetaData.FromArtifact(pathToArtifact); + + // Assert + + CatalogMetaData expected = new CatalogMetaData() + { + ContentType = "DmScript", + Name = "Demo InterAppCalls", + Version = "1.0.0-CU1", + }; + + result.Should().Be(expected); + result.IsPreRelease().Should().BeFalse(); + } + + [TestMethod()] + public void FromArtifactTest_ReleaseDashboard() + { + // Arrange + string pathToArtifact = "TestData/Tandberg RX1290 1.0.0-CU1.dmapp"; + + // Act + CatalogMetaData result = CatalogMetaData.FromArtifact(pathToArtifact); + + // Assert + + CatalogMetaData expected = new CatalogMetaData() + { + ContentType = "Dashboard", + Name = "Tandberg RX1290", + Version = "1.0.0-CU1", + }; + + result.Should().Be(expected); + result.IsPreRelease().Should().BeFalse(); + } + + [TestMethod()] + public void FromArtifactTest_ReleaseProtocolVisio() + { + // Arrange + string pathToArtifact = "TestData/Microsoft Platform 1.0.0-CU4.dmapp"; + + // Act + CatalogMetaData result = CatalogMetaData.FromArtifact(pathToArtifact); + + // Assert + + CatalogMetaData expected = new CatalogMetaData() + { + ContentType = "Visio", + Name = "Microsoft Platform", + Version = "1.0.0-CU4", + }; + + result.Should().Be(expected); + result.IsPreRelease().Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/CICD.Tools.CatalogUpload.LibTests/TestData/Demo InterAppCalls 1.0.0-CU1.dmapp b/CICD.Tools.CatalogUpload.LibTests/TestData/Demo InterAppCalls 1.0.0-CU1.dmapp new file mode 100644 index 0000000..3e151a2 Binary files /dev/null and b/CICD.Tools.CatalogUpload.LibTests/TestData/Demo InterAppCalls 1.0.0-CU1.dmapp differ diff --git a/CICD.Tools.CatalogUpload.LibTests/TestData/Microsoft Platform 1.0.0-CU4.dmapp b/CICD.Tools.CatalogUpload.LibTests/TestData/Microsoft Platform 1.0.0-CU4.dmapp new file mode 100644 index 0000000..4b20ac9 Binary files /dev/null and b/CICD.Tools.CatalogUpload.LibTests/TestData/Microsoft Platform 1.0.0-CU4.dmapp differ diff --git a/CICD.Tools.CatalogUpload.LibTests/TestData/SLNetSubscriptionsBenchmarking 1.0.1_B15.dmapp b/CICD.Tools.CatalogUpload.LibTests/TestData/SLNetSubscriptionsBenchmarking 1.0.1_B15.dmapp new file mode 100644 index 0000000..bfcbe4e Binary files /dev/null and b/CICD.Tools.CatalogUpload.LibTests/TestData/SLNetSubscriptionsBenchmarking 1.0.1_B15.dmapp differ diff --git a/CICD.Tools.CatalogUpload.LibTests/TestData/Tandberg RX1290 1.0.0-CU1.dmapp b/CICD.Tools.CatalogUpload.LibTests/TestData/Tandberg RX1290 1.0.0-CU1.dmapp new file mode 100644 index 0000000..e1b9d1e Binary files /dev/null and b/CICD.Tools.CatalogUpload.LibTests/TestData/Tandberg RX1290 1.0.0-CU1.dmapp differ diff --git a/CICD.Tools.CatalogUpload/CICD.Tools.CatalogUpload.csproj b/CICD.Tools.CatalogUpload/CICD.Tools.CatalogUpload.csproj index 6bf2641..5fd5bd1 100644 --- a/CICD.Tools.CatalogUpload/CICD.Tools.CatalogUpload.csproj +++ b/CICD.Tools.CatalogUpload/CICD.Tools.CatalogUpload.csproj @@ -7,8 +7,8 @@ disable disable Skyline.DataMiner.CICD.Tools.CatalogUpload - 1.0.1 - 1.0.1 + 1.0.1-alpaha1 + 1.0.1-alpaha1 Skyline;DataMiner https://skyline.be README.md diff --git a/CICD.Tools.CatalogUpload/Program.cs b/CICD.Tools.CatalogUpload/Program.cs index 7690fb2..b4bbca2 100644 --- a/CICD.Tools.CatalogUpload/Program.cs +++ b/CICD.Tools.CatalogUpload/Program.cs @@ -28,9 +28,9 @@ public static async Task Main(string[] args) IsRequired = true }; - var dmCatalogKey = new Option( - name: "--dmCatalogKey", - description: "The key to upload to the catalog as defined in admin.dataminer.services. This is Optional: the key can also be provided using the 'dmcatalogkey' environment variable (unix/win) or using 'dmcatalogkey_encrypted' configured with Skyline.DataMiner.CICD.Tools.WinEncryptedKeys (windows).") + var dmCatalogToken = new Option( + name: "--dmCatalogToken", + description: "The key to upload to the catalog as defined in admin.dataminer.services. This is optional if the key can also be provided using the 'dmcatalogtoken' environment variable (unix/win) or using 'dmcatalogtoken_encrypted' configured with Skyline.DataMiner.CICD.Tools.WinEncryptedKeys (windows).") { IsRequired = false }; @@ -42,19 +42,70 @@ public static async Task Main(string[] args) IsRequired = false, }; - var rootCommand = new RootCommand("Uploads artifacts to the Skyline DataMiner catalog (https://catalog.dataminer.services)") + var rootCommand = new RootCommand("Uploads artifacts to the artifact cloud. (The default upload has no additional registration and no visibility on the catalog. Use the returned Artifact ID for deployment or download.)"); + rootCommand.AddGlobalOption(pathToArtifact); + rootCommand.AddGlobalOption(dmCatalogToken); + rootCommand.AddGlobalOption(isDebug); + + // subcommand "WithRegistration with the required sourcecode then and optional other arguments. + var registrationIdentifier = new Option( + name: "--sourcecode", + description: "A Uri for the globally unique location of your sourcecode. This is used as a unique identifier. e.g. https://github.com/SkylineCommunications/MyTestRepo") + { + IsRequired = true, + }; + + var overrideVersion = new Option( + name: "--version", + description: "Optional but recommended, include a different version then the internal package version to register your package under (this can be a pre-release version). e.g. '1.0.1', '1.0.1-prerelease1', '1.0.0.1'") + { + IsRequired = false, + }; + + var branch = new Option( + name: "--branch", + description: "Defaults to 'main' when not provided. What branch does this version of your package belong to? e.g. 'main', '1.0.0.X', '1.0.X', 'dev/somefeature', ...") { - pathToArtifact, + IsRequired = false, }; - rootCommand.SetHandler(Process, pathToArtifact, dmCatalogKey, isDebug); + var committerMail = new Option( + name: "--authorMail", + description: "Optionally include the e-mail of the uploader.") + { + IsRequired = false, + }; + var releaseUri = new Option( + name: "--releaseNotes", + description: "Optionally include a uri to the release notes. e.g. https://github.com/SkylineCommunications/MyTestRepo/releases/tag/1.0.3") + { + IsRequired = false, + }; + + var withRegistrationCommand = new Command("withRegistration", "Uploads artifacts to become visible in the Skyline DataMiner catalog (https://catalog.dataminer.services") + { + registrationIdentifier, + overrideVersion, + branch, + committerMail, + releaseUri + }; + + rootCommand.SetHandler(Process, pathToArtifact, dmCatalogToken, isDebug); + withRegistrationCommand.SetHandler(ProcessWithRegistration, pathToArtifact, dmCatalogToken, isDebug, registrationIdentifier, overrideVersion, branch, committerMail, releaseUri); + + rootCommand.Add(withRegistrationCommand); return await rootCommand.InvokeAsync(args); } - private static async Task Process(string pathToArtifact, string dmCatalogKey, bool isDebug) + private static async Task Process(string pathToArtifact, string dmCatalogToken, bool isDebug) + { + return await ProcessWithRegistration(pathToArtifact, dmCatalogToken, isDebug, null, null, null, null, null); + } + + private static async Task ProcessWithRegistration(string pathToArtifact, string dmCatalogToken, bool isDebug, string registrationIdentifier, string overrideVersion, string branch, string committerMail, string releaseUri) { - bool success; LoggerConfiguration logConfig; if (isDebug) @@ -75,15 +126,27 @@ private static async Task Process(string pathToArtifact, string dmCatalogKe CatalogMetaData metaData = CatalogMetaData.FromArtifact(pathToArtifact); + if (registrationIdentifier != null) + { + // Registration as a whole is optional. If there is no Identifier provided there will be no registration. + metaData.Identifier = registrationIdentifier; // Need from user <- optional unique identifier. Usually the path to the sourcecode on github/gitlab/git. + + // These are optional. Only override if not null. + if (overrideVersion != null) metaData.Version = overrideVersion; + if (branch != null) metaData.Branch = branch; + if (committerMail != null) metaData.CommitterMail = committerMail; + if (releaseUri != null) metaData.ReleaseUri = releaseUri; + } + CatalogArtifact artifact = new CatalogArtifact(pathToArtifact, logger, metaData); - - if (string.IsNullOrWhiteSpace(dmCatalogKey)) + + if (string.IsNullOrWhiteSpace(dmCatalogToken)) { await artifact.UploadAsync(); } else { - await artifact.UploadAsync(dmCatalogKey); + await artifact.UploadAsync(dmCatalogToken); } return 0; diff --git a/CICD.Tools.CatalogUpload/README.md b/CICD.Tools.CatalogUpload/README.md index b3ccba5..08b2d75 100644 --- a/CICD.Tools.CatalogUpload/README.md +++ b/CICD.Tools.CatalogUpload/README.md @@ -2,7 +2,7 @@ ## About -Uploads artifacts to the Skyline DataMiner catalog (https://catalog.dataminer.services) +Uploads artifacts to the Skyline DataMiner catalog (https://catalog.dataminer.services) visible or invisible. ### About DataMiner @@ -21,8 +21,72 @@ At Skyline Communications, we deal in world-class solutions that are deployed by ## Getting Started In commandline: + +```console dotnet tool install -g Skyline.DataMiner.CICD.Tools.CatalogUpload +``` Then run the command + +```console dataminer-catalog-upload help +``` +## Common Commands + +### Volatile Uploads +The most basic command will upload but not register a package. +This allows further usage only with the returned Artifact ID. The package will not show up in the Catalog. +Nothing will be registered but your cloud-connected agent will be able to get deployed with the package using the returned identifier. + +```console +dataminer-catalog-upload --pathToArtifact "pathToPackage.dmapp" --dmCatalogToken "cloudConnectedToken" +``` + +### Authentication and Tokens + +You can choose to add the dmcatalogtoken to an environment variable instead and skip having to pass along the secure token. +```console + dataminer-catalog-upload --pathToArtifact "pathToPackage.dmapp" +``` + + There are 2 options to store the key in an environment variable: +- key stored as an Environment Variable called "dmcatalogtoken". (unix/win) +- key configured one-time using Skyline.DataMiner.CICD.Tools.WinEncryptedKeys called "dmcatalogtoken_encrypted" (windows only) + +The first option is commonplace for environment setups in cloud-based CI/CD Pipelines (github, gitlab, azure, ...) +The second option can be beneficial on a static server such as Jenkins or your local machine (windows only). It adds additional encryption to the environment variable only allowing decryption on the same machine. +for example: + +```console +dotnet tool install -g Skyline.DataMiner.CICD.Tools.WinEncryptedKeys +WinEncryptedKeys --name "dmcatalogtoken_encrypted" --value "MyTokenHere" +``` + +> **Note** +> Make sure you close your commandline tool so it clears the history. +> This only works on windows machines. + +You can review and make suggestions to the sourcecode of this encryption tool here: +https://github.com/SkylineCommunications/Skyline.DataMiner.CICD.Tools.WinEncryptedKeys + + ### Registered Uploads + +If you want to make your package visible on the catalog and provide the ability to create combined Installation Packages (Currently only available through internal tools at Skyline Communications) you'll need to provide additional registration meta-data. + +The most basic command will be default anonymous and try to use the 'main' branch and the version defined in the artifact (either protocol version or dmapp version) + +```console + dataminer-catalog-upload WithRegistration --pathToArtifact "pathToPackage.dmapp" --sourcecode "https://github.com/SkylineCommunications/MyTestRepo" +``` + +Though optional, it is however highly recommended (due to current restrictions to the internal dmapp version syntax) to provide your own version tag. + +```console + dataminer-catalog-upload WithRegistration --pathToArtifact "pathToPackage.dmapp" --sourcecode "https://github.com/SkylineCommunications/MyTestRepo" --version "1.0.1-alpha1" +``` + +In addition you can provide additional optional information: +```console + dataminer-catalog-upload WithRegistration --pathToArtifact "pathToPackage.dmapp" --sourcecode "https://github.com/SkylineCommunications/MyTestRepo" --version "1.0.1-alpha1" --branch "dev/MyFeature" --authorMail "thunder@skyline.be" --releaseNotes "https://github.com/SkylineCommunications/MyTestRepo/releases/tag/1.0.3" +``` diff --git a/README.md b/README.md index e9eba77..1235c46 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Uploads artifacts to the Skyline DataMiner catalog (https://catalog.dataminer.se ## Projects * For more information about Skyline.DataMiner.CICD.Tools.CatalogUpload, see [CICD.Tools.CatalogUpload/README.md](CICD.Tools.CatalogUpload/README.md). +* For more information about Skyline.DataMiner.CICD.Tools.CatalogUpload.Lib, see [CICD.Tools.CatalogUpload.Lib/README.md](CICD.Tools.CatalogUpload.Lib/README.md). + ### About DataMiner diff --git a/Skyline.DataMiner.CICD.Tools.CatalogUpload.sln b/Skyline.DataMiner.CICD.Tools.CatalogUpload.sln index 2d32373..0a72db9 100644 --- a/Skyline.DataMiner.CICD.Tools.CatalogUpload.sln +++ b/Skyline.DataMiner.CICD.Tools.CatalogUpload.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CICD.Tools.CatalogUpload.Lib", "CICD.Tools.CatalogUpload.Lib\CICD.Tools.CatalogUpload.Lib.csproj", "{16042C8C-17CF-47D3-A963-8C4ED1ED64C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CICD.Tools.CatalogUpload.LibTests", "CICD.Tools.CatalogUpload.LibTests\CICD.Tools.CatalogUpload.LibTests.csproj", "{F3E6F66F-7E12-41C7-8E8F-F6248BC72D5A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {16042C8C-17CF-47D3-A963-8C4ED1ED64C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {16042C8C-17CF-47D3-A963-8C4ED1ED64C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {16042C8C-17CF-47D3-A963-8C4ED1ED64C2}.Release|Any CPU.Build.0 = Release|Any CPU + {F3E6F66F-7E12-41C7-8E8F-F6248BC72D5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3E6F66F-7E12-41C7-8E8F-F6248BC72D5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3E6F66F-7E12-41C7-8E8F-F6248BC72D5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3E6F66F-7E12-41C7-8E8F-F6248BC72D5A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE