Skip to content

Commit

Permalink
Use the full container uri when path is not set (#198)
Browse files Browse the repository at this point in the history
* Use full container path for path uri

* Update docs and test

* Update changelog
  • Loading branch information
emgarten authored Apr 30, 2024
1 parent fa3cfbe commit 11922d4
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 18 deletions.
3 changes: 3 additions & 0 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Release Notes

## 6.0.2
* Fixed a bug where Azure feeds failed when path was not set to the full container URI [Issue](https://github.com/emgarten/Sleet/issues/197)

## 6.0.0
* Moved from Microsoft.Azure.Storage.Blob to Azure.Storage.Blobs [PR](https://github.com/emgarten/Sleet/pull/191)
* Added support for Managed Identity and DefaultAzureCredential with Azure storage accounts [PR](https://github.com/emgarten/Sleet/pull/195)
Expand Down
36 changes: 36 additions & 0 deletions doc/feed-type-azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,42 @@ For `.netconfig`, just create or edit the file directly in the [desired location
connectionString = "DefaultEndpointsProtocol=https;AccountName=;AccountKey=;BlobEndpoint="
```

## Using Microsoft Entra ID

Alternatively you can use Entra ID to provide a service principal or managed identity to access the storage account.

For the list of environment variables that can be used see:
https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential?view=azure-dotnet

`path` must be set to the full uri of the feed including the container name. This gives sleet context on which account and contanier to use the Entra ID with.

Sleet will pick up the environment variables using the Microsoft Identity package and use to authenticate with the storage account.

### sleet.json

```json
{
"sources": [
{
"name": "feed",
"type": "azure",
"container": "feed",
"path": "https://<your feed>.blob.core.windows.net/feed/"
}
]
}
```

### .netconfg

```gitconfig
[sleet "feed"]
type = azure
container = feed
path = "https://<your feed>.blob.core.windows.net/feed/"
```


## Adding packages

Add packages to the feed with the push command, this can be used with either a path to a single nupkg or a folder of nupkgs.
Expand Down
2 changes: 1 addition & 1 deletion src/SleetLib/FileSystem/AzureFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public AzureFileSystem(LocalCache cache, Uri root, Uri baseUri, BlobServiceClien
// Verify that the provided path is sane.
if (!expectedPath.AbsoluteUri.StartsWith(expectedPath.AbsoluteUri, StringComparison.Ordinal))
{
throw new ArgumentException($"Invalid feed path. Azure container {container} resolved to {containerUri.AbsoluteUri} which does not match the provided URI of {expectedPath} Update path in sleet.json or remove the path property to auto resolve the value.");
throw new ArgumentException($"Invalid feed path. Azure container {container} resolved to {containerUri.AbsoluteUri} which does not match the provided URI of {expectedPath} Update path in sleet.json or remove the path property to auto resolve the value if using a connection string.");
}

// Compute sub path, ignore the given sub path
Expand Down
40 changes: 24 additions & 16 deletions src/SleetLib/FileSystem/FileSystemFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,14 @@ public static async Task<ISleetFileSystem> CreateFileSystemAsync(LocalSettings s
var connectionString = JsonUtility.GetValueCaseInsensitive(sourceEntry, "connectionString");
var container = JsonUtility.GetValueCaseInsensitive(sourceEntry, "container");

var blobServiceClient = await GetBlobServiceClient(log, connectionString, pathUri);
var blobServiceClient = await GetBlobServiceClient(log, connectionString, pathUri, container);

if (pathUri == null)
{
// Get the default url from the container
pathUri = AzureUtility.GetContainerPath(blobServiceClient, container);
}

pathUri ??= blobServiceClient.Uri;
baseUri ??= pathUri;

result = new AzureFileSystem(cache, pathUri, baseUri, blobServiceClient, container, feedSubPath);
Expand Down Expand Up @@ -232,33 +237,36 @@ public static async Task<ISleetFileSystem> CreateFileSystemAsync(LocalSettings s
private static async Task<BlobServiceClient> GetBlobServiceClient(
ILogger log,
string connectionString,
Uri pathUri)
Uri pathUri,
string container)
{
if (pathUri is not null && connectionString is not null)
{
throw new ArgumentException("path (recommended) and connectionString (discouraged) are mutually exclusive for azure account. Chose one or the other.");
}

if (pathUri is not null)
// Path can be used with a connection string.
// If the path is set and the connection string is not, use the default creds.
if (pathUri is not null && connectionString is null)
{
return new BlobServiceClient(new Uri(pathUri.GetLeftPart(UriPartial.Authority)), new DefaultAzureCredential());
}

if (connectionString is null)
// Continue with connection string
if (string.IsNullOrEmpty(connectionString))
{
throw new ArgumentException("Missing path (recommended) or connectionString (discouraged) for azure account.");
throw new ArgumentException("Missing connectionString for azure account.");
}

await log.LogAsync(LogLevel.Warning,
"connectionString (with access key) is not recommended for azure account. More information here: https://learn.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage?tabs=azure-portal#protect-your-access-keys" + Environment.NewLine +
"Use path instead.");


if (connectionString.Equals(AzureFileSystem.AzureEmptyConnectionString, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Invalid connectionString for azure account.");
}

if (string.IsNullOrEmpty(container))
{
throw new ArgumentException("Missing container for azure account.");
}

await log.LogAsync(LogLevel.Verbose,
"connectionString (with access key) is not recommended for azure account. More information here: https://learn.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage?tabs=azure-portal#protect-your-access-keys" + Environment.NewLine +
"Use path instead.");

return new BlobServiceClient(connectionString);
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/SleetLib/Utility/AzureUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Azure.Storage.Blobs;

namespace Sleet
{
public static class AzureUtility
{
public static Uri GetContainerPath(BlobServiceClient blobServiceClient, string container)
{
var blobContainerClient = blobServiceClient.GetBlobContainerClient(container);
return UriUtility.EnsureTrailingSlash(blobContainerClient.Uri);
}
}
}
41 changes: 41 additions & 0 deletions test/Sleet.Azure.Tests/AzureFileSystemTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FluentAssertions;
using Newtonsoft.Json.Linq;
using Sleet.Test.Common;

namespace Sleet.Azure.Tests
Expand Down Expand Up @@ -36,5 +37,45 @@ public async Task GivenAStorageAccountVerifyContainerOperations()
await testContext.CleanupAsync();
}
}

[EnvVarExistsFact(AzureTestContext.EnvVarName)]
public async Task GivenAStorageAccountConnStringVerifyFileSystemFactoryCreatesFS()
{
using (var testContext = new AzureTestContext())
{
testContext.CreateContainerOnInit = false;
await testContext.InitAsync();

var settings = LocalSettings.Load(new JObject(
new JProperty("sources",
new JArray(
new JObject(
new JProperty("name", "azure"),
new JProperty("type", "azure"),
new JProperty("container", testContext.ContainerName),
new JProperty("connectionString", AzureTestContext.GetConnectionString()))))));

var fs = await FileSystemFactory.CreateFileSystemAsync(settings, testContext.LocalCache, "azure", testContext.Logger);
fs.GetPath("test.txt").AbsolutePath.Should().Contain("/test.txt");

// Verify at the start
(await fs.HasBucket(testContext.Logger, CancellationToken.None)).Should().BeFalse();
(await fs.Validate(testContext.Logger, CancellationToken.None)).Should().BeFalse();

// Create
await fs.CreateBucket(testContext.Logger, CancellationToken.None);

(await fs.HasBucket(testContext.Logger, CancellationToken.None)).Should().BeTrue();
(await fs.Validate(testContext.Logger, CancellationToken.None)).Should().BeTrue();

// Delete
await fs.DeleteBucket(testContext.Logger, CancellationToken.None);

(await fs.HasBucket(testContext.Logger, CancellationToken.None)).Should().BeFalse();
(await fs.Validate(testContext.Logger, CancellationToken.None)).Should().BeFalse();

await testContext.CleanupAsync();
}
}
}
}
2 changes: 1 addition & 1 deletion test/Sleet.Azure.Tests/AzureTestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void Dispose()

public const string EnvVarName = "SLEET_TEST_ACCOUNT";

private static string GetConnectionString()
public static string GetConnectionString()
{
// Use a real azure storage account
var s = Environment.GetEnvironmentVariable(EnvVarName);
Expand Down

0 comments on commit 11922d4

Please sign in to comment.