Skip to content

Commit 042439e

Browse files
Support catalog yml (#2)
* Testing multiple tasks * Fixed having multiple tasks. * Moved CatalogUpload logic to happen on doing a Publish. TODO: fix catalog API returning Error it could not parse the manifest.yml * Catalog Publish Initial support added. * Cleanup for SQ * Cleanup * Small tweaks * SQ remarks * SQ remarks --------- Co-authored-by: Michiel Oda <michiel.oda@skyline.be>
1 parent 2692797 commit 042439e

12 files changed

+604
-20
lines changed
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Skyline.DataMiner.Sdk.CatalogService
2+
{
3+
using Newtonsoft.Json;
4+
5+
/// <summary>
6+
/// Artifact information returned from uploading an artifact to the catalog.
7+
/// </summary>
8+
public class ArtifactUploadResult
9+
{
10+
/// <summary>
11+
/// The GUID that represents the ID of the artifact in our cloud storage database. Can be used to download or deploy the artifact.
12+
/// </summary>
13+
[JsonProperty("artifactId")]
14+
public string ArtifactId { get; set; }
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Skyline.DataMiner.Sdk.CatalogService
2+
{
3+
using System;
4+
using System.Net.Http;
5+
6+
/// <summary>
7+
/// Creates instances of <see cref="ICatalogService"/> to communicate with the Skyline DataMiner Catalog (https://catalog.dataminer.services/).
8+
/// </summary>
9+
public static class CatalogServiceFactory
10+
{
11+
/// <summary>
12+
/// Creates instances of <see cref="ICatalogService"/> to communicate with the Skyline DataMiner Catalog (https://catalog.dataminer.services/) using HTTP for communication.
13+
/// </summary>
14+
/// <param name="httpClient">An instance of <see cref="HttpClient"/> used for communication with the catalog.</param>
15+
/// <returns>An instance of <see cref="ICatalogService"/> to communicate with the Skyline DataMiner Catalog (https://catalog.dataminer.services/).</returns>
16+
public static ICatalogService CreateWithHttp(HttpClient httpClient)
17+
{
18+
var environment = Environment.GetEnvironmentVariable("override-api-dataminer-services");
19+
20+
string apiBaseUrl;
21+
if (environment != null)
22+
{
23+
apiBaseUrl = $"https://api-{environment}.dataminer.services/{environment}";
24+
}
25+
else
26+
{
27+
apiBaseUrl = "https://api.dataminer.services";
28+
}
29+
30+
httpClient.BaseAddress = new Uri($"{apiBaseUrl}/");
31+
return new HttpCatalogService(httpClient);
32+
}
33+
}
34+
}
+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
namespace Skyline.DataMiner.Sdk.CatalogService
2+
{
3+
using System;
4+
using System.IO;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Net.Http.Headers;
8+
using System.Security.Authentication;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
using Newtonsoft.Json;
13+
14+
internal sealed class HttpCatalogService : ICatalogService, IDisposable
15+
{
16+
/// <summary>
17+
/// Artifact information returned from uploading an artifact to the catalog using the non-volatile upload.
18+
/// </summary>
19+
private sealed class CatalogUploadResult
20+
{
21+
[JsonProperty("catalogId")]
22+
public string CatalogId { get; set; }
23+
24+
[JsonProperty("catalogVersionNumber")]
25+
public string CatalogVersionNumber { get; set; }
26+
27+
[JsonProperty("azureStorageId")]
28+
public string AzureStorageId { get; set; }
29+
}
30+
31+
/// <summary>
32+
/// Artifact information returned from registering an artifact to the catalog.
33+
/// </summary>
34+
private sealed class CatalogRegisterResult
35+
{
36+
[JsonProperty("catalogId")]
37+
public string CatalogId { get; set; }
38+
}
39+
40+
/// <summary>
41+
/// Represents an exception that is thrown when a version already exists.
42+
/// </summary>
43+
internal sealed class VersionAlreadyExistsException : Exception
44+
{
45+
/// <summary>
46+
/// Gets the version that caused the exception.
47+
/// </summary>
48+
public string Version { get; }
49+
50+
/// <summary>
51+
/// Initializes a new instance of the <see cref="VersionAlreadyExistsException"/> class.
52+
/// </summary>
53+
public VersionAlreadyExistsException()
54+
: base("The specified version already exists.")
55+
{
56+
}
57+
58+
/// <summary>
59+
/// Initializes a new instance of the <see cref="VersionAlreadyExistsException"/> class
60+
/// with a specified error message.
61+
/// </summary>
62+
/// <param name="version">The conflicting version.</param>
63+
public VersionAlreadyExistsException(string version)
64+
: base($"The specified version '{version}' already exists.")
65+
{
66+
Version = version;
67+
}
68+
69+
/// <summary>
70+
/// Initializes a new instance of the <see cref="VersionAlreadyExistsException"/> class
71+
/// with a specified error message and a reference to the inner exception that is the cause of this exception.
72+
/// </summary>
73+
/// <param name="version">The conflicting version.</param>
74+
/// <param name="innerException">The exception that is the cause of the current exception.</param>
75+
public VersionAlreadyExistsException(string version, Exception innerException)
76+
: base($"The specified version '{version}' already exists.")
77+
{
78+
Version = version;
79+
}
80+
}
81+
82+
private const string RegistrationPath = "api/key-catalog/v2-0/catalogs/register";
83+
private const string VersionUploadPathEnd = "/register/version";
84+
private const string VersionUploadPathStart = "api/key-catalog/v2-0/catalogs/";
85+
private readonly HttpClient _httpClient;
86+
87+
public HttpCatalogService(HttpClient httpClient)
88+
{
89+
_httpClient = httpClient;
90+
}
91+
92+
public void Dispose()
93+
{
94+
_httpClient.Dispose();
95+
}
96+
97+
public async Task<ArtifactUploadResult> RegisterCatalogAsync(byte[] catalogDetailsZip, string key, CancellationToken cancellationToken)
98+
{
99+
using (var formData = new MultipartFormDataContent())
100+
{
101+
formData.Headers.Add("Ocp-Apim-Subscription-Key", key);
102+
103+
// Add file
104+
using (MemoryStream ms = new MemoryStream(catalogDetailsZip))
105+
{
106+
ms.Write(catalogDetailsZip, 0, catalogDetailsZip.Length);
107+
ms.Position = 0;
108+
formData.Add(new StreamContent(ms), "file", "catalogDetails.zip");
109+
110+
// Make PUT request
111+
var response = await _httpClient.PutAsync(RegistrationPath, formData, cancellationToken).ConfigureAwait(false);
112+
113+
// Get the response body
114+
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
115+
116+
if (response.IsSuccessStatusCode)
117+
{
118+
var returnedResult = JsonConvert.DeserializeObject<CatalogRegisterResult>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
119+
return new ArtifactUploadResult { ArtifactId = returnedResult.CatalogId };
120+
}
121+
122+
if (response.StatusCode is HttpStatusCode.Forbidden || response.StatusCode is HttpStatusCode.Unauthorized)
123+
{
124+
throw new AuthenticationException($"The registration api returned a {response.StatusCode} response. Body: {body}");
125+
}
126+
127+
throw new InvalidOperationException($"The registration api returned a {response.StatusCode} response. Body: {body}");
128+
}
129+
}
130+
}
131+
132+
public Task<ArtifactUploadResult> UploadVersionAsync(byte[] package, string fileName, string key, string catalogId, string version, string description, CancellationToken cancellationToken)
133+
{
134+
if (String.IsNullOrWhiteSpace(fileName)) throw new ArgumentNullException(nameof(fileName));
135+
if (String.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
136+
if (String.IsNullOrWhiteSpace(catalogId)) throw new ArgumentNullException(nameof(catalogId));
137+
if (String.IsNullOrWhiteSpace(version)) throw new ArgumentNullException(nameof(version));
138+
139+
return UploadVersionInternalAsync(package, fileName, key, catalogId, version, description, cancellationToken);
140+
}
141+
142+
private async Task<ArtifactUploadResult> UploadVersionInternalAsync(byte[] package, string fileName, string key, string catalogId, string version, string description, CancellationToken cancellationToken)
143+
{
144+
string versionUploadPath = $"{VersionUploadPathStart}{catalogId}{VersionUploadPathEnd}";
145+
using (var formData = new MultipartFormDataContent())
146+
{
147+
formData.Headers.Add("Ocp-Apim-Subscription-Key", key);
148+
149+
// Add the package (zip file) to the form data
150+
using (MemoryStream ms = new MemoryStream(package))
151+
{
152+
ms.Position = 0; // Reset the stream position after writing
153+
154+
// Set up StreamContent with correct headers for the file
155+
var fileContent = new StreamContent(ms);
156+
157+
fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
158+
{
159+
Name = "\"file\"",
160+
FileName = "\"" + fileName + "\""
161+
};
162+
formData.Add(fileContent);
163+
164+
// Add version information to the form data
165+
formData.Add(new StringContent(version), "versionNumber");
166+
formData.Add(new StringContent(description), "versionDescription");
167+
168+
// Make the HTTP POST request
169+
var response = await _httpClient.PostAsync(versionUploadPath, formData, cancellationToken).ConfigureAwait(false);
170+
171+
// Read and log the response body
172+
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
173+
174+
if (response.IsSuccessStatusCode)
175+
{
176+
var returnedResult = JsonConvert.DeserializeObject<CatalogUploadResult>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
177+
return new ArtifactUploadResult { ArtifactId = returnedResult.AzureStorageId };
178+
}
179+
180+
if (response.StatusCode is HttpStatusCode.Forbidden || response.StatusCode is HttpStatusCode.Unauthorized)
181+
{
182+
throw new AuthenticationException($"The version upload api returned a {response.StatusCode} response. Body: {body}");
183+
}
184+
185+
if (response.StatusCode is HttpStatusCode.Conflict && body.Contains("already exists."))
186+
{
187+
throw new VersionAlreadyExistsException(version);
188+
}
189+
190+
throw new InvalidOperationException($"The version upload api returned a {response.StatusCode} response. Body: {body}");
191+
}
192+
}
193+
}
194+
}
195+
}

Sdk/CatalogService/ICatalogService.cs

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace Skyline.DataMiner.Sdk.CatalogService
2+
{
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
/// <summary>
7+
/// Service interface used to actually upload and artifact.
8+
/// </summary>
9+
public interface ICatalogService
10+
{
11+
/// <summary>
12+
/// Registers the catalog by uploading the catalog details as a zip file.
13+
/// </summary>
14+
/// <param name="catalogDetailsZip">A byte array containing the zipped catalog details.</param>
15+
/// <param name="key">A unique token used for authentication.</param>
16+
/// <param name="cancellationToken">A token used to cancel the ongoing registration if needed.</param>
17+
/// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous operation, returning an <see cref="ArtifactUploadResult"/>.</returns>
18+
Task<ArtifactUploadResult> RegisterCatalogAsync(byte[] catalogDetailsZip, string key, CancellationToken cancellationToken);
19+
20+
/// <summary>
21+
/// Uploads a specific version of the artifact to the catalog.
22+
/// </summary>
23+
/// <param name="package">A byte array containing the package content.</param>
24+
/// <param name="fileName">The name of the file being uploaded.</param>
25+
/// <param name="key">A unique token used for authentication.</param>
26+
/// <param name="catalogId">The unique catalog identifier for the artifact.</param>
27+
/// <param name="version">The version of the artifact being uploaded.</param>
28+
/// <param name="description">A description of the artifact version.</param>
29+
/// <param name="cancellationToken">A token used to cancel the ongoing upload if needed.</param>
30+
/// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous operation, returning an <see cref="ArtifactUploadResult"/>.</returns>
31+
Task<ArtifactUploadResult> UploadVersionAsync(byte[] package, string fileName, string key, string catalogId, string version, string description, CancellationToken cancellationToken);
32+
}
33+
}

Sdk/Helpers/CatalogReferencesHelper.cs

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public static bool TryResolveCatalogReferences(Project packageProject, out List<
1818
includedPackages = null;
1919
errorMessage = null;
2020

21-
string rootFolder = FileSystem.Instance.Directory.GetParentDirectory(packageProject.ProjectDirectory);
2221
const string xmlFileName = "CatalogReferences.xml";
2322
string xmlFilePath = FileSystem.Instance.Path.Combine(packageProject.ProjectDirectory, "PackageContent", xmlFileName);
2423

Sdk/Helpers/ProjectReferencesHelper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private static List<string> ResolveProjectReferences(string xmlFilePath, string
7373
JsonDocument jsonFilter = JsonDocument.Parse(json);
7474

7575
if (jsonFilter.RootElement.TryGetProperty("solution", out JsonElement solution) && solution.TryGetProperty("projects", out JsonElement projects)
76-
&& projects.ValueKind== JsonValueKind.Array)
76+
&& projects.ValueKind == JsonValueKind.Array)
7777
{
7878
foreach (JsonElement project in projects.EnumerateArray())
7979
{

Sdk/Sdk.csproj

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@
3636

3737
<ItemGroup>
3838
<PackageReference Include="Microsoft.Build.Tasks.Core" VersionOverride="$(MicrosoftBuildMinimumPackageVersion)" PrivateAssets="all" ExcludeAssets="Runtime" IncludeAssets="compile; build; native; contentfiles; analyzers; buildtransitive" Version="14.3.0" />
39+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.1" />
40+
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.0" />
3941
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" PrivateAssets="all" />
4042
<PackageReference Include="Skyline.DataMiner.CICD.Common" Version="1.0.5-alpha" PrivateAssets="all" />
4143
<PackageReference Include="Skyline.DataMiner.CICD.Parsers.Common" Version="1.0.13-foxtrot" PrivateAssets="all" />
4244
<PackageReference Include="Skyline.DataMiner.CICD.Assemblers.Automation" Version="1.0.16-alpha" PrivateAssets="all" />
43-
<PackageReference Include="Skyline.DataMiner.Core.AppPackageCreator" Version="2.0.0-alpha2" PrivateAssets="all" />
45+
<PackageReference Include="Skyline.DataMiner.Core.AppPackageCreator" Version="2.0.0-alpha3" PrivateAssets="all" />
4446
<PackageReference Include="System.Text.Json" Version="9.0.0" PrivateAssets="all" />
4547
</ItemGroup>
4648

Sdk/Sdk/Sdk.targets

+32-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" Condition=" '$(CommonTargetsPath)' == '' " />
44

55
<!-- Declare which tasks will be used later -->
6-
<UsingTask TaskName="DmappCreation" AssemblyFile="$(MSBuildThisFileDirectory)\netstandard2.0\Skyline.DataMiner.Sdk.dll"/>
6+
<UsingTask TaskName="Skyline.DataMiner.Sdk.Tasks.PublishToCatalog" AssemblyFile="$(MSBuildThisFileDirectory)\netstandard2.0\Skyline.DataMiner.Sdk.dll"/>
7+
<UsingTask TaskName="Skyline.DataMiner.Sdk.Tasks.CatalogInformation" AssemblyFile="$(MSBuildThisFileDirectory)\netstandard2.0\Skyline.DataMiner.Sdk.dll"/>
8+
<UsingTask TaskName="Skyline.DataMiner.Sdk.Tasks.DmappCreation" AssemblyFile="$(MSBuildThisFileDirectory)\netstandard2.0\Skyline.DataMiner.Sdk.dll"/>
79

810
<!-- Custom target that will run after the build -->
911
<Target Name="DmappCreation" AfterTargets="Build" Condition="'$(GenerateDataMinerPackage)' == 'true'">
@@ -19,4 +21,33 @@
1921
MinimumRequiredDmVersion="$(MinimumRequiredDmVersion)"
2022
/>
2123
</Target>
24+
25+
<Target Name="CatalogInformation" AfterTargets="Build" Condition="'$(GenerateDataMinerPackage)' == 'true'">
26+
<CatalogInformation
27+
ProjectDirectory="$(MSBuildProjectDirectory)"
28+
BaseOutputPath="$(BaseOutputPath)"
29+
Configuration="$(Configuration)"
30+
PackageId="$(PackageId)"
31+
PackageVersion="$(PackageVersion)"
32+
/>
33+
</Target>
34+
35+
<Target Name="Publish" Condition="'$(GenerateDataMinerPackage)' == 'true'" DependsOnTargets="$(ConditionalBuildTarget)">
36+
<!-- Replaces the default publish steps -->
37+
<Message Text="Publishing to catalog.dataminer.services...." Importance="high" />
38+
<PublishToCatalog
39+
ProjectDirectory="$(MSBuildProjectDirectory)"
40+
BaseOutputPath="$(BaseOutputPath)"
41+
Configuration="$(Configuration)"
42+
PackageId="$(PackageId)"
43+
PackageVersion="$(PackageVersion)"
44+
VersionComment="$(VersionComment)"
45+
CatalogPublishKeyName="$(CatalogPublishKeyName)"
46+
UserSecretsId="$(UserSecretsId)"
47+
/>
48+
</Target>
49+
50+
<PropertyGroup>
51+
<ConditionalBuildTarget Condition="'$(NoBuild)' != 'true'">Build</ConditionalBuildTarget>
52+
</PropertyGroup>
2253
</Project>

0 commit comments

Comments
 (0)