Skip to content

Commit cbd0d5e

Browse files
authored
[Feature Flags] Add gallery integration (#6806)
Part of https://github.com/NuGet/Engineering/issues/2016
1 parent cc82088 commit cbd0d5e

23 files changed

Lines changed: 365 additions & 61 deletions

src/NuGetGallery.Core/CoreConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,7 @@ public static class Folders
4848
public const string UploadTracingKeyHeaderName = "upload-id";
4949

5050
public const string LicenseFileName = "license";
51+
52+
public const string FeatureFlagsFileName = "flags.json";
5153
}
5254
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using NuGet.Services.Entities;
6+
using NuGet.Services.FeatureFlags;
7+
8+
namespace NuGetGallery.Features
9+
{
10+
public static class FeatureFlagClientExtensions
11+
{
12+
public static bool IsEnabled(
13+
this IFeatureFlagClient client,
14+
string flight,
15+
User user,
16+
bool defaultValue)
17+
{
18+
if (user == null)
19+
{
20+
throw new ArgumentNullException(nameof(user));
21+
}
22+
23+
return client.IsEnabled(flight, new FlightUser(user), defaultValue);
24+
}
25+
26+
private class FlightUser : IFlightUser
27+
{
28+
public FlightUser(User user)
29+
{
30+
Username = user.Username;
31+
EmailAddress = user.EmailAddress;
32+
IsSiteAdmin = user.IsAdministrator;
33+
}
34+
35+
public string Username { get; }
36+
public string EmailAddress { get; }
37+
public bool IsSiteAdmin { get; }
38+
}
39+
}
40+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Threading.Tasks;
7+
using Newtonsoft.Json;
8+
using NuGet.Services.FeatureFlags;
9+
10+
namespace NuGetGallery.Features
11+
{
12+
public class FeatureFlagFileStorageService : IFeatureFlagStorageService
13+
{
14+
private readonly ICoreFileStorageService _storage;
15+
private readonly JsonSerializer _serializer;
16+
17+
public FeatureFlagFileStorageService(ICoreFileStorageService storage)
18+
{
19+
_storage = storage ?? throw new ArgumentNullException(nameof(storage));
20+
_serializer = new JsonSerializer();
21+
}
22+
23+
public async Task<FeatureFlags> GetAsync()
24+
{
25+
using (var stream = await _storage.GetFileAsync(CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName))
26+
using (var streamReader = new StreamReader(stream))
27+
using (var reader = new JsonTextReader(streamReader))
28+
{
29+
return _serializer.Deserialize<FeatureFlags>(reader);
30+
}
31+
}
32+
}
33+
}

src/NuGetGallery.Core/NuGetGallery.Core.csproj

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@
133133
<Compile Include="Extensions\StorageExceptionExtensions.cs" />
134134
<Compile Include="Extensions\UserExtensionsCore.cs" />
135135
<Compile Include="Extensions\ValidationIssueExtensions.cs" />
136+
<Compile Include="Features\FeatureFlagFileStorageService.cs" />
137+
<Compile Include="Features\FeatureFlagClientExtensions.cs" />
136138
<Compile Include="ICloudStorageStatusDependency.cs" />
137139
<Compile Include="Infrastructure\AzureEntityList.cs" />
138140
<Compile Include="Infrastructure\ElmahException.cs" />
@@ -214,14 +216,20 @@
214216
<EmbeddedResource Include="Infrastructure\MigrateUserToOrganization.sql" />
215217
</ItemGroup>
216218
<ItemGroup>
219+
<PackageReference Include="NuGet.Services.Entities">
220+
<Version>2.42.0</Version>
221+
</PackageReference>
222+
<PackageReference Include="NuGet.Services.FeatureFlags">
223+
<Version>2.42.0</Version>
224+
</PackageReference>
217225
<PackageReference Include="NuGet.Services.Messaging.Email">
218-
<Version>2.40.0</Version>
226+
<Version>2.42.0</Version>
219227
</PackageReference>
220228
<PackageReference Include="NuGet.Services.Validation">
221-
<Version>2.40.0</Version>
229+
<Version>2.42.0</Version>
222230
</PackageReference>
223231
<PackageReference Include="NuGet.Services.Validation.Issues">
224-
<Version>2.40.0</Version>
232+
<Version>2.42.0</Version>
225233
</PackageReference>
226234
<PackageReference Include="NuGet.StrongName.AnglicanGeek.MarkdownMailer">
227235
<Version>1.2.0</Version>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Features": {
3+
"NuGetGallery.Typosquatting": "Enabled"
4+
},
5+
6+
"Flights": {
7+
"NuGetGallery.TyposquattingFlight": {
8+
"All": true
9+
}
10+
}
11+
}

src/NuGetGallery/App_Start/DefaultDependenciesModule.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
using Microsoft.Extensions.Logging;
2222
using Microsoft.WindowsAzure.ServiceRuntime;
2323
using NuGet.Services.Entities;
24+
using NuGet.Services.FeatureFlags;
2425
using NuGet.Services.KeyVault;
2526
using NuGet.Services.Licenses;
2627
using NuGet.Services.Logging;
@@ -38,6 +39,7 @@
3839
using NuGetGallery.Configuration;
3940
using NuGetGallery.Cookies;
4041
using NuGetGallery.Diagnostics;
42+
using NuGetGallery.Features;
4143
using NuGetGallery.Infrastructure;
4244
using NuGetGallery.Infrastructure.Authentication;
4345
using NuGetGallery.Infrastructure.Lucene;
@@ -111,7 +113,11 @@ protected override void Load(ContainerBuilder builder)
111113
builder.Register(c => configuration.PackageDelete)
112114
.As<IPackageDeleteConfiguration>();
113115

114-
builder.RegisterType<TelemetryService>().As<ITelemetryService>().SingleInstance();
116+
builder.RegisterType<TelemetryService>()
117+
.As<ITelemetryService>()
118+
.As<IFeatureFlagTelemetryService>()
119+
.SingleInstance();
120+
115121
builder.RegisterType<CredentialBuilder>().As<ICredentialBuilder>().SingleInstance();
116122
builder.RegisterType<CredentialValidator>().As<ICredentialValidator>().SingleInstance();
117123

@@ -358,6 +364,7 @@ protected override void Load(ContainerBuilder builder)
358364
.As<ILicenseExpressionSegmentator>()
359365
.InstancePerLifetimeScope();
360366

367+
RegisterFeatureFlagsService(builder, configuration);
361368
RegisterMessagingService(builder, configuration);
362369

363370
builder.Register(c => HttpContext.Current.User)
@@ -406,6 +413,32 @@ protected override void Load(ContainerBuilder builder)
406413
ConfigureAutocomplete(builder, configuration);
407414
}
408415

416+
private static void RegisterFeatureFlagsService(ContainerBuilder builder, ConfigurationService configuration)
417+
{
418+
builder
419+
.Register(context => new FeatureFlagOptions
420+
{
421+
RefreshInterval = configuration.Current.FeatureFlagsRefreshInterval,
422+
})
423+
.AsSelf()
424+
.SingleInstance();
425+
426+
builder
427+
.RegisterType<FeatureFlagCacheService>()
428+
.As<IFeatureFlagCacheService>()
429+
.SingleInstance();
430+
431+
builder
432+
.RegisterType<FeatureFlagClient>()
433+
.As<IFeatureFlagClient>()
434+
.SingleInstance();
435+
436+
builder
437+
.RegisterType<FeatureFlagService>()
438+
.As<IFeatureFlagService>()
439+
.SingleInstance();
440+
}
441+
409442
private static void RegisterMessagingService(ContainerBuilder builder, ConfigurationService configuration)
410443
{
411444
if (configuration.Current.AsynchronousEmailServiceEnabled)

src/NuGetGallery/App_Start/OwinStartup.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Microsoft.Owin.Logging;
1818
using Microsoft.Owin.Security;
1919
using Microsoft.Owin.Security.Cookies;
20+
using NuGet.Services.FeatureFlags;
2021
using NuGet.Services.Logging;
2122
using NuGetGallery.Authentication;
2223
using NuGetGallery.Authentication.Providers;
@@ -73,7 +74,7 @@ public static void Configuration(IAppBuilder app)
7374
await Task.Delay(ContentObjectService.RefreshInterval, token);
7475
}
7576
});
76-
77+
7778
// Setup telemetry
7879
var instrumentationKey = config.Current.AppInsightsInstrumentationKey;
7980
if (!string.IsNullOrEmpty(instrumentationKey))
@@ -165,6 +166,14 @@ public static void Configuration(IAppBuilder app)
165166
auther.Startup(config, app).Wait();
166167
}
167168

169+
// Ensure feature flags are loaded once at startup, and then refresh them in the background.
170+
var featureFlags = DependencyResolver.Current.GetService<IFeatureFlagCacheService>();
171+
if (featureFlags != null)
172+
{
173+
featureFlags.RefreshAsync().Wait();
174+
HostingEnvironment.QueueBackgroundWorkItem(featureFlags.RunAsync);
175+
}
176+
168177
// Catch unobserved exceptions from threads before they cause IIS to crash:
169178
TaskScheduler.UnobservedTaskException += (sender, exArgs) =>
170179
{

src/NuGetGallery/App_Start/StorageDependent.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using NuGet.Services.FeatureFlags;
78
using NuGetGallery.Configuration;
9+
using NuGetGallery.Features;
810

911
namespace NuGetGallery
1012
{
@@ -91,6 +93,7 @@ public static IEnumerable<StorageDependent> GetAll(IAppConfiguration configurati
9193
Create<UploadFileService, IUploadFileService>(configuration.AzureStorage_Uploads_ConnectionString, isSingleInstance: false),
9294
Create<CoreLicenseFileService, ICoreLicenseFileService>(configuration.AzureStorage_Packages_ConnectionString, isSingleInstance: false),
9395
Create<RevalidationStateService, IRevalidationStateService>(configuration.AzureStorage_Revalidation_ConnectionString, isSingleInstance: false),
96+
Create<FeatureFlagFileStorageService, IFeatureFlagStorageService>(configuration.AzureStorage_Content_ConnectionString, isSingleInstance: true)
9497
};
9598

9699
var connectionStringToBindingKey = dependents

src/NuGetGallery/Configuration/AppConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public class AppConfiguration : IAppConfiguration
6767
/// </summary>
6868
public bool AzureStorageReadAccessGeoRedundant { get; set; }
6969

70+
public TimeSpan FeatureFlagsRefreshInterval { get; set; }
71+
7072
public bool AsynchronousPackageValidationEnabled { get; set; }
7173

7274
public bool BlockingAsynchronousPackageValidationEnabled { get; set; }

src/NuGetGallery/Configuration/IAppConfiguration.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ public interface IAppConfiguration : IMessageServiceConfiguration
8585
/// </summary>
8686
bool AzureStorageReadAccessGeoRedundant { get; set; }
8787

88+
/// <summary>
89+
/// How frequently the feature flags should be refreshed.
90+
/// </summary>
91+
TimeSpan FeatureFlagsRefreshInterval { get; set; }
92+
8893
/// <summary>
8994
/// Gets a boolean indicating whether asynchronous package validation is enabled.
9095
/// </summary>

0 commit comments

Comments
 (0)