Skip to content

Commit

Permalink
1935 as a developper i want to use amazon s3 to store and expose devi…
Browse files Browse the repository at this point in the history
…ces images (#2032)

* AWS S3 store & expose device images

* s3 storage image + some unit testing

* #1935 code + tests

* #1935 code + tests

* AWS image cache control

* removing usersecret
  • Loading branch information
ssgueye2 authored and kbeaugrand committed Jun 22, 2023
1 parent b07682c commit abd7c28
Show file tree
Hide file tree
Showing 13 changed files with 659 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/AzureIoTHub.Portal.Domain/ConfigHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,6 @@ public abstract class ConfigHandler
public abstract string AWSAccessSecret { get; }
public abstract string AWSRegion { get; }
public abstract string AWSS3StorageConnectionString { get; }
public abstract string AWSBucketName { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
<ItemGroup>
<PackageReference Include="AWSSDK.IoT" Version="3.7.105.22" />
<PackageReference Include="AWSSDK.IotData" Version="3.7.102.5" />
<PackageReference Include="AWSSDK.S3" Version="3.7.103.50" />
<PackageReference Include="AWSSDK.SecretsManager" Version="3.7.102.21" />
<PackageReference Include="Azure.Data.Tables" Version="12.8.0" />
<PackageReference Include="Azure.Messaging.EventHubs" Version="5.9.2" />
Expand All @@ -126,8 +127,9 @@
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.7">
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
2 changes: 2 additions & 0 deletions src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,7 @@ internal abstract class ConfigHandlerBase : ConfigHandler
internal const string AWSAccessSecretKey = "AWS:AccessSecret";
internal const string AWSRegionKey = "AWS:Region";
internal const string AWSS3StorageConnectionStringKey = "AWS:S3Storage:ConnectionString";
internal const string AWSBucketNameKey = "AWS:BucketName";

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,6 @@ internal DevelopmentConfigHandler(IConfiguration config)
public override string AWSAccessSecret => this.config[AWSAccessSecretKey]!;
public override string AWSRegion => this.config[AWSRegionKey]!;
public override string AWSS3StorageConnectionString => this.config[AWSS3StorageConnectionStringKey]!;
public override string AWSBucketName => this.config[AWSBucketNameKey]!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright (c) CGI France. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace AzureIoTHub.Portal.Infrastructure.Managers
{
using System;
using System.Threading.Tasks;
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using Azure;
using AzureIoTHub.Portal.Application.Managers;
using AzureIoTHub.Portal.Domain;
using AzureIoTHub.Portal.Domain.Exceptions;
using AzureIoTHub.Portal.Domain.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

public class AwsDeviceModelImageManager : IDeviceModelImageManager
{
private readonly ILogger<AwsDeviceModelImageManager> logger;
private readonly ConfigHandler configHandler;
private readonly IOptions<DeviceModelImageOptions> imageOptions;
private readonly IAmazonS3 s3Client;

public AwsDeviceModelImageManager(
ILogger<AwsDeviceModelImageManager> logger,
ConfigHandler configHandler,
IOptions<DeviceModelImageOptions> options,
IAmazonS3 s3Client)
{
this.logger = logger;
this.configHandler = configHandler;
this.imageOptions = options;
this.s3Client = s3Client;
}


public async Task<string> ChangeDeviceModelImageAsync(string deviceModelId, Stream stream)
{
this.logger.LogInformation($"Uploading Image to AWS S3 storage");

//Portal must be able to upload images to Amazon S3
var putObjectRequest = new PutObjectRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId,
InputStream = stream,
ContentType = "image/*",
Headers = {CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" }
};

var putObjectResponse = await this.s3Client.PutObjectAsync(putObjectRequest);

if (putObjectResponse.HttpStatusCode == System.Net.HttpStatusCode.OK)
{
//Images on S3 are publicly accessible and read-only
var putAclRequest = new PutACLRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId,
CannedACL = S3CannedACL.PublicRead // Set the object's ACL to public read
};
var putACLResponse = await this.s3Client.PutACLAsync(putAclRequest);

return putACLResponse.HttpStatusCode == System.Net.HttpStatusCode.OK
? ComputeImageUrl(deviceModelId)
: throw new InternalServerErrorException("Error by setting the image access to public and read-only");
}
else
{
throw new InternalServerErrorException("Error by uploading the image in S3 Storage");
}

}

public Uri ComputeImageUri(string deviceModelId)
{
throw new NotImplementedException();
}

private string ComputeImageUrl(string deviceModelId)
{
return $"https://{this.configHandler.AWSBucketName}.s3.{RegionEndpoint.GetBySystemName(this.configHandler.AWSRegion)}.amazonaws.com/{deviceModelId}";
}
public async Task DeleteDeviceModelImageAsync(string deviceModelId)
{

this.logger.LogInformation($"Deleting image from AWS S3 storage");

var deleteImageObject = new DeleteObjectRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId
};
try
{
_ = await this.s3Client.DeleteObjectAsync(deleteImageObject);

}
catch (RequestFailedException e)
{
throw new InternalServerErrorException("Unable to delete the image from S3 storage.", e);
}
}

public async Task<string> SetDefaultImageToModel(string deviceModelId)
{
this.logger.LogInformation($"Uploading Default Image to AWS S3 storage");

//Portal must be able to upload images to Amazon S3
var putObjectRequest = new PutObjectRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId,
FilePath = $"../Resources/{this.imageOptions.Value.DefaultImageName}",
ContentType = "image/*", // image content type
Headers = {CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" }

};

var putObjectResponse = await this.s3Client.PutObjectAsync(putObjectRequest);

if (putObjectResponse.HttpStatusCode == System.Net.HttpStatusCode.OK)
{
//Images on S3 are publicly accessible and read-only
var putAclRequest = new PutACLRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId,
CannedACL = S3CannedACL.PublicRead // Set the object's ACL to public read
};
var putACLResponse = await this.s3Client.PutACLAsync(putAclRequest);

return putACLResponse.HttpStatusCode == System.Net.HttpStatusCode.OK
? ComputeImageUrl(deviceModelId)
: throw new InternalServerErrorException("Error by setting the image access to public and read-only");
}
else
{
throw new InternalServerErrorException("Error by uploading the image in S3 Storage");
}

}

public async Task InitializeDefaultImageBlob()
{

this.logger.LogInformation($"Initializing default Image to AWS S3 storage");

var putObjectRequest = new PutObjectRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = this.imageOptions.Value.DefaultImageName,
FilePath = $"../Resources/{this.imageOptions.Value.DefaultImageName}",
ContentType = "image/*", // image content type
Headers = {CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" }

};

var putObjectResponse = await this.s3Client.PutObjectAsync(putObjectRequest);

if (putObjectResponse.HttpStatusCode == System.Net.HttpStatusCode.OK)
{
//Images on S3 are publicly accessible and read-only
var putAclRequest = new PutACLRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = this.imageOptions.Value.DefaultImageName,
CannedACL = S3CannedACL.PublicRead // Set the object's ACL to public read
};
var putACLResponse = await this.s3Client.PutACLAsync(putAclRequest);

if (putACLResponse.HttpStatusCode != System.Net.HttpStatusCode.OK)
{
throw new InternalServerErrorException("Error by setting the image access to public and read-only");
}
}
else
{
throw new InternalServerErrorException("Error by uploading the image in S3 Storage");
}

}

public Task SyncImagesCacheControl()
{
/* We don't need an implementation of
this mehod for AWS because new images will processed by the method SetDefaultImageToModel
*/
throw new NotImplementedException();

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,6 @@ internal ProductionAWSConfigHandler(IConfiguration config)
public override string AWSAccessSecret => this.config[AWSAccessSecretKey]!;
public override string AWSRegion => this.config[AWSRegionKey]!;
public override string AWSS3StorageConnectionString => this.config[AWSS3StorageConnectionStringKey]!;
public override string AWSBucketName => this.config[AWSBucketNameKey]!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,7 @@ internal ProductionAzureConfigHandler(IConfiguration config)

public override string AWSRegion => throw new NotImplementedException();
public override string AWSS3StorageConnectionString => throw new NotImplementedException();

public override string AWSBucketName => throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ namespace AzureIoTHub.Portal.Infrastructure.Startup
using Amazon;
using Amazon.IoT;
using Amazon.IotData;
using Amazon.S3;
using Amazon.SecretsManager;
using AzureIoTHub.Portal.Application.Managers;
using AzureIoTHub.Portal.Domain;
using AzureIoTHub.Portal.Infrastructure.Managers;
using Microsoft.Extensions.DependencyInjection;

public static class AWSServiceCollectionExtension
Expand All @@ -34,6 +37,10 @@ private static IServiceCollection ConfigureAWSClient(this IServiceCollection ser

_ = services.AddSingleton(() => new AmazonSecretsManagerClient(configuration.AWSAccess, configuration.AWSAccessSecret, RegionEndpoint.GetBySystemName(configuration.AWSRegion)));

_ = services.AddSingleton(() => new AmazonS3Client(configuration.AWSAccess, configuration.AWSAccessSecret, RegionEndpoint.GetBySystemName(configuration.AWSRegion)));

_ = services.AddTransient<IDeviceModelImageManager, AwsDeviceModelImageManager>();

return services;
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/AzureIoTHub.Portal.Server/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ public async void Configure(IApplicationBuilder app, IWebHostEnvironment env)
await ConfigureAzureAsync(app);
break;
case CloudProviders.AWS:
await ConfigureAwsAsync(app);
break;
default:
break;
Expand All @@ -463,6 +464,12 @@ private static async Task ConfigureAzureAsync(IApplicationBuilder app)

await EnsureDatabaseCreatedAndUpToDate(app)!;
}
private static async Task ConfigureAwsAsync(IApplicationBuilder app)
{
var deviceModelImageManager = app.ApplicationServices.GetService<IDeviceModelImageManager>();

await deviceModelImageManager?.InitializeDefaultImageBlob()!;
}

private static void UseApiExceptionMiddleware(IApplicationBuilder app)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private DevelopmentConfigHandler CreateDevelopmentConfigHandler()
[TestCase(ConfigHandlerBase.AWSRegionKey, nameof(ConfigHandlerBase.AWSRegion))]
[TestCase(ConfigHandlerBase.AWSS3StorageConnectionStringKey, nameof(ConfigHandlerBase.AWSS3StorageConnectionString))]
[TestCase(ConfigHandlerBase.CloudProviderKey, nameof(ConfigHandlerBase.CloudProvider))]
[TestCase(ConfigHandlerBase.AWSBucketNameKey, nameof(ConfigHandlerBase.AWSBucketName))]
public void SettingsShouldGetValueFromAppSettings(string configKey, string configPropertyName)
{
// Arrange
Expand Down
Loading

0 comments on commit abd7c28

Please sign in to comment.