Extend use of validated string types (#2357)

In the Python code there were more validated string types that haven't been properly ported for use in the C# code (such as Region). Add that type back in and improve some others:

- Use `Region` type to represent regions (implicitly convertible to/from the `AzureLocation` SDK type)
- Improve validation of `Container` type to match Azure specs and use it in more places
- Restore/fix validation of `PoolName` type which was previously removed (#2080) due to being too strict: now allows 1-64 ASCII alphanumeric/hyphen/dash
  - We want to restrict pool names so that we can use them as disambiguating prefixes for scaleset names (see #2189). Note that underscore is not actually permitted in scaleset names so we will probably end up mapping it to hyphen.

Note that once C#7 lands we will be able to simplify the usage of `ValidatedString` a lot (using static abstract methods).

----

Open questions:

For deserializing from "known-good" places such as table storage or from Azure SDK APIs, should we have an `T.UnsafeAssumeValid(string input)` method which does no validation, to protect us from breakage?
This commit is contained in:
George Pollard
2022-09-12 10:06:51 +12:00
committed by GitHub
parent 140638f684
commit 2a2c07ed35
47 changed files with 324 additions and 264 deletions

View File

@ -42,7 +42,7 @@ public class ContainersFunction {
new Error( new Error(
Code: ErrorCode.INVALID_REQUEST, Code: ErrorCode.INVALID_REQUEST,
Errors: new[] { "invalid container" }), Errors: new[] { "invalid container" }),
context: get.Name.ContainerName); context: get.Name.String);
} }
var metadata = (await container.GetPropertiesAsync()).Value.Metadata; var metadata = (await container.GetPropertiesAsync()).Value.Metadata;
@ -63,7 +63,7 @@ public class ContainersFunction {
// otherwise list all containers // otherwise list all containers
var containers = await _context.Containers.GetContainers(StorageType.Corpus); var containers = await _context.Containers.GetContainers(StorageType.Corpus);
var result = containers.Select(c => new ContainerInfoBase(new Container(c.Key), c.Value)); var result = containers.Select(c => new ContainerInfoBase(c.Key, c.Value));
return await RequestHandling.Ok(req, result); return await RequestHandling.Ok(req, result);
} }
@ -104,7 +104,7 @@ public class ContainersFunction {
new Error( new Error(
Code: ErrorCode.INVALID_REQUEST, Code: ErrorCode.INVALID_REQUEST,
Errors: new[] { "invalid container" }), Errors: new[] { "invalid container" }),
context: post.Name.ContainerName); context: post.Name.String);
} }
return await RequestHandling.Ok( return await RequestHandling.Ok(

View File

@ -21,13 +21,13 @@ public class Download {
private async Async.Task<HttpResponseData> Get(HttpRequestData req) { private async Async.Task<HttpResponseData> Get(HttpRequestData req) {
var query = HttpUtility.ParseQueryString(req.Url.Query); var query = HttpUtility.ParseQueryString(req.Url.Query);
var container = query["container"]; var queryContainer = query["container"];
if (container is null) { if (queryContainer is null || !Container.TryParse(queryContainer, out var container)) {
return await _context.RequestHandling.NotOk( return await _context.RequestHandling.NotOk(
req, req,
new Error( new Error(
ErrorCode.INVALID_REQUEST, ErrorCode.INVALID_REQUEST,
new string[] { "'container' query parameter must be provided" }), new string[] { "'container' query parameter must be provided and valid" }),
"download"); "download");
} }
@ -42,7 +42,7 @@ public class Download {
} }
var sasUri = await _context.Containers.GetFileSasUrl( var sasUri = await _context.Containers.GetFileSasUrl(
new Container(container), container,
filename, filename,
StorageType.Corpus, StorageType.Corpus,
BlobSasPermissions.Read, BlobSasPermissions.Read,

View File

@ -52,7 +52,7 @@ public class Jobs {
var metadata = new Dictionary<string, string>{ var metadata = new Dictionary<string, string>{
{ "container_type", "logs" }, // TODO: use ContainerType.Logs enum somehow; needs snake case name { "container_type", "logs" }, // TODO: use ContainerType.Logs enum somehow; needs snake case name
}; };
var containerName = new Container($"logs-{job.JobId}"); var containerName = Container.Parse($"logs-{job.JobId}");
var containerSas = await _context.Containers.CreateContainer(containerName, StorageType.Corpus, metadata); var containerSas = await _context.Containers.CreateContainer(containerName, StorageType.Corpus, metadata);
if (containerSas is null) { if (containerSas is null) {
return await _context.RequestHandling.NotOk( return await _context.RequestHandling.NotOk(

View File

@ -22,8 +22,9 @@ public class Notifications {
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search"); return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search");
} }
var entries = request.OkV switch { { Container: null } => _context.NotificationOperations.SearchAll(), { Container: var c } => _context.NotificationOperations.SearchByRowKeys(c.Select(x => x.ContainerName)) var entries = request.OkV switch { { Container: null } => _context.NotificationOperations.SearchAll(), { Container: var c } => _context.NotificationOperations.SearchByRowKeys(c.Select(x => x.String))
}; };
var response = req.CreateResponse(HttpStatusCode.OK); var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(entries); await response.WriteAsJsonAsync(entries);
return response; return response;

View File

@ -56,6 +56,6 @@ public class QueueFileChanges {
var path = string.Join('/', parts.Skip(1)); var path = string.Join('/', parts.Skip(1));
log.Info($"file added container: {container} - path: {path}"); log.Info($"file added container: {container} - path: {path}");
await _notificationOperations.NewFiles(new Container(container), path, failTaskOnTransientError); await _notificationOperations.NewFiles(Container.Parse(container), path, failTaskOnTransientError);
} }
} }

View File

@ -75,7 +75,7 @@ public class Scaleset {
context: "ScalesetCreate"); context: "ScalesetCreate");
} }
string region; Region region;
if (create.Region is null) { if (create.Region is null) {
region = await _context.Creds.GetBaseRegion(); region = await _context.Creds.GetBaseRegion();
} else { } else {

View File

@ -62,15 +62,16 @@ public class TimerProxy {
// nsg enabled OneFuzz this will overwrite existing NSG // nsg enabled OneFuzz this will overwrite existing NSG
// assignment though. This behavior is acceptable at this point // assignment though. This behavior is acceptable at this point
// since we do not support bring your own NSG // since we do not support bring your own NSG
var nsgName = Nsg.NameFromRegion(region);
if (await nsgOpertions.GetNsg(region) != null) { if (await nsgOpertions.GetNsg(nsgName) != null) {
var network = await Network.Init(region, _context); var network = await Network.Init(region, _context);
var subnet = await network.GetSubnet(); var subnet = await network.GetSubnet();
if (subnet != null) { if (subnet != null) {
var vnet = await network.GetVnet(); var vnet = await network.GetVnet();
if (vnet != null) { if (vnet != null) {
var result = await nsgOpertions.AssociateSubnet(region, vnet, subnet); var result = await nsgOpertions.AssociateSubnet(nsgName, vnet, subnet);
if (!result.OkV) { if (!result.OkV) {
_logger.Error($"Failed to associate NSG and subnet due to {result.ErrorV} in region {region}"); _logger.Error($"Failed to associate NSG and subnet due to {result.ErrorV} in region {region}");
} }

View File

@ -2,7 +2,6 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
using Region = System.String;
namespace Microsoft.OneFuzz.Service; namespace Microsoft.OneFuzz.Service;

View File

@ -5,7 +5,6 @@ using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
using Endpoint = System.String; using Endpoint = System.String;
using GroupId = System.Guid; using GroupId = System.Guid;
using PrincipalId = System.Guid; using PrincipalId = System.Guid;
using Region = System.String;
namespace Microsoft.OneFuzz.Service; namespace Microsoft.OneFuzz.Service;
@ -406,25 +405,6 @@ public record Scaleset(
// 'Nodes' removed when porting from Python: only used in search response // 'Nodes' removed when porting from Python: only used in search response
) : StatefulEntityBase<ScalesetState>(State); ) : StatefulEntityBase<ScalesetState>(State);
[JsonConverter(typeof(ContainerConverter))]
public record Container(string ContainerName) {
public string ContainerName { get; } = ContainerName.All(c => char.IsLetterOrDigit(c) || c == '-') ? ContainerName : throw new ArgumentException("Container name must have only numbers, letters or dashes");
public override string ToString() {
return ContainerName;
}
}
public class ContainerConverter : JsonConverter<Container> {
public override Container? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
var containerName = reader.GetString();
return containerName == null ? null : new Container(containerName);
}
public override void Write(Utf8JsonWriter writer, Container value, JsonSerializerOptions options) {
writer.WriteStringValue(value.ContainerName);
}
}
public record Notification( public record Notification(
[PartitionKey] Guid NotificationId, [PartitionKey] Guid NotificationId,
[RowKey] Container Container, [RowKey] Container Container,
@ -732,7 +712,14 @@ public record Job(
public UserInfo? UserInfo { get; set; } public UserInfo? UserInfo { get; set; }
} }
public record Nsg(string Name, Region Region); public record Nsg(string Name, Region Region) {
public static Nsg ForRegion(Region region)
=> new(NameFromRegion(region), region);
// Currently, the name of a NSG is the same as the region it is in.
public static string NameFromRegion(Region region)
=> region.String;
};
public record WorkUnit( public record WorkUnit(
Guid JobId, Guid JobId,

View File

@ -191,7 +191,7 @@ public record ScalesetCreate(
[property: Required] PoolName PoolName, [property: Required] PoolName PoolName,
[property: Required] string VmSku, [property: Required] string VmSku,
[property: Required] string Image, [property: Required] string Image,
string? Region, Region? Region,
[property: Range(1, long.MaxValue), Required] long Size, [property: Range(1, long.MaxValue), Required] long Size,
[property: Required] bool SpotInstances, [property: Required] bool SpotInstances,
[property: Required] Dictionary<string, string> Tags, [property: Required] Dictionary<string, string> Tags,

View File

@ -56,7 +56,7 @@ public record BoolResult(
public record InfoResponse( public record InfoResponse(
string ResourceGroup, string ResourceGroup,
string Region, Region Region,
string Subscription, string Subscription,
IReadOnlyDictionary<string, InfoVersion> Versions, IReadOnlyDictionary<string, InfoVersion> Versions,
Guid? InstanceId, Guid? InstanceId,
@ -127,7 +127,7 @@ public record ScalesetResponse(
Authentication? Auth, Authentication? Auth,
string VmSku, string VmSku,
string Image, string Image,
string Region, Region Region,
long Size, long Size,
bool? SpotInstances, bool? SpotInstances,
bool EmphemeralOsDisks, bool EmphemeralOsDisks,
@ -175,7 +175,7 @@ public record ProxyGetResult(
); );
public record ProxyInfo( public record ProxyInfo(
string Region, Region Region,
Guid ProxyId, Guid ProxyId,
VmState State VmState State
); );

View File

@ -1,7 +1,9 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Azure.Core;
namespace Microsoft.OneFuzz.Service; namespace Microsoft.OneFuzz.Service;
@ -11,6 +13,10 @@ static class Check {
private static readonly Regex _isAlnumDash = new(@"\A[a-zA-Z0-9\-]+\z", RegexOptions.Compiled); private static readonly Regex _isAlnumDash = new(@"\A[a-zA-Z0-9\-]+\z", RegexOptions.Compiled);
public static bool IsAlnumDash(string input) => _isAlnumDash.IsMatch(input); public static bool IsAlnumDash(string input) => _isAlnumDash.IsMatch(input);
// Permits 1-64 characters: alphanumeric, underscore, or dash.
private static readonly Regex _isNameLike = new(@"\A[_a-zA-Z0-9\-]{1,64}\z", RegexOptions.Compiled);
public static bool IsNameLike(string input) => _isNameLike.IsMatch(input);
} }
// Base class for types that are wrappers around a validated string. // Base class for types that are wrappers around a validated string.
@ -47,9 +53,11 @@ public abstract class ValidatedStringConverter<T> : JsonConverter<T> where T : V
} }
[JsonConverter(typeof(Converter))] [JsonConverter(typeof(Converter))]
public record PoolName : ValidatedString { public sealed record PoolName : ValidatedString {
public PoolName(string value) : base(value) { private static bool IsValid(string input) => Check.IsNameLike(input);
// Debug.Assert(Check.IsAlnumDash(value));
private PoolName(string value) : base(value) {
Debug.Assert(IsValid(value));
} }
public static PoolName Parse(string input) { public static PoolName Parse(string input) {
@ -61,14 +69,10 @@ public record PoolName : ValidatedString {
} }
public static bool TryParse(string input, [NotNullWhen(returnValue: true)] out PoolName? result) { public static bool TryParse(string input, [NotNullWhen(returnValue: true)] out PoolName? result) {
if (!IsValid(input)) {
// bypassing the validation because this code has a stricter validation than the python equivalent result = default;
// see (issue #2080) return false;
}
// if (!Check.IsAlnumDash(input)) {
// result = default;
// return false;
// }
result = new PoolName(input); result = new PoolName(input);
return true; return true;
@ -80,12 +84,12 @@ public record PoolName : ValidatedString {
} }
} }
/* TODO: to be enabled in a separate PR
[JsonConverter(typeof(Converter))] [JsonConverter(typeof(Converter))]
public record Region : ValidatedString { public record Region : ValidatedString {
private Region(string value) : base(value) { private static bool IsValid(string input) => Check.IsAlnum(input);
Debug.Assert(Check.IsAlnum(value));
private Region(string value) : base(value.ToLowerInvariant()) {
Debug.Assert(IsValid(value));
} }
public static Region Parse(string input) { public static Region Parse(string input) {
@ -93,11 +97,11 @@ public record Region : ValidatedString {
return result; return result;
} }
throw new ArgumentException("Region name must have only numbers, letters or dashes"); throw new ArgumentException("Region name must have only numbers or letters");
} }
public static bool TryParse(string input, [NotNullWhen(returnValue: true)] out Region? result) { public static bool TryParse(string input, [NotNullWhen(returnValue: true)] out Region? result) {
if (!Check.IsAlnum(input)) { if (!IsValid(input)) {
result = default; result = default;
return false; return false;
} }
@ -106,6 +110,9 @@ public record Region : ValidatedString {
return true; return true;
} }
public static implicit operator AzureLocation(Region me) => new(me.String);
public static implicit operator Region(AzureLocation it) => new(it.Name);
public sealed class Converter : ValidatedStringConverter<Region> { public sealed class Converter : ValidatedStringConverter<Region> {
protected override bool TryParse(string input, out Region? output) protected override bool TryParse(string input, out Region? output)
=> Region.TryParse(input, out output); => Region.TryParse(input, out output);
@ -114,8 +121,16 @@ public record Region : ValidatedString {
[JsonConverter(typeof(Converter))] [JsonConverter(typeof(Converter))]
public record Container : ValidatedString { public record Container : ValidatedString {
// See: https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftstorage
// - 3-63
// - Lowercase letters, numbers, and hyphens.
// - Start with lowercase letter or number. Can't use consecutive hyphens.
private static readonly Regex _containerRegex = new(@"\A(?!-)(?!.*--)[a-z0-9\-]{3,63}\z", RegexOptions.Compiled);
private static bool IsValid(string input) => _containerRegex.IsMatch(input);
private Container(string value) : base(value) { private Container(string value) : base(value) {
Debug.Assert(Check.IsAlnumDash(value)); Debug.Assert(IsValid(value));
} }
public static Container Parse(string input) { public static Container Parse(string input) {
@ -127,7 +142,7 @@ public record Container : ValidatedString {
} }
public static bool TryParse(string input, [NotNullWhen(returnValue: true)] out Container? result) { public static bool TryParse(string input, [NotNullWhen(returnValue: true)] out Container? result) {
if (!Check.IsAlnumDash(input)) { if (!IsValid(input)) {
result = default; result = default;
return false; return false;
} }
@ -141,4 +156,3 @@ public record Container : ValidatedString {
=> Container.TryParse(input, out output); => Container.TryParse(input, out output);
} }
} }
*/

View File

@ -49,7 +49,7 @@ namespace ApiService.TestHooks {
_log.Info("Get base region"); _log.Info("Get base region");
var resp = req.CreateResponse(HttpStatusCode.OK); var resp = req.CreateResponse(HttpStatusCode.OK);
var region = await _creds.GetBaseRegion(); var region = await _creds.GetBaseRegion();
await resp.WriteStringAsync(region); await resp.WriteStringAsync(region.String);
return resp; return resp;
} }

View File

@ -30,7 +30,7 @@ namespace ApiService.TestHooks {
var fileName = query["fileName"]; var fileName = query["fileName"];
var failTaskOnTransientError = UriExtension.GetBool("failTaskOnTransientError", query, true); var failTaskOnTransientError = UriExtension.GetBool("failTaskOnTransientError", query, true);
await _notificationOps.NewFiles(new Container(container), fileName, failTaskOnTransientError); await _notificationOps.NewFiles(Container.Parse(container), fileName, failTaskOnTransientError);
var resp = req.CreateResponse(HttpStatusCode.OK); var resp = req.CreateResponse(HttpStatusCode.OK);
return resp; return resp;
} }
@ -43,7 +43,7 @@ namespace ApiService.TestHooks {
var query = UriExtension.GetQueryComponents(req.Url); var query = UriExtension.GetQueryComponents(req.Url);
var container = query["container"]; var container = query["container"];
var notifications = _notificationOps.GetNotifications(new Container(container)); var notifications = _notificationOps.GetNotifications(Container.Parse(container));
var json = JsonSerializer.Serialize(await notifications.ToListAsync(), EntityConverter.GetJsonSerializerOptions()); var json = JsonSerializer.Serialize(await notifications.ToListAsync(), EntityConverter.GetJsonSerializerOptions());
var resp = req.CreateResponse(HttpStatusCode.OK); var resp = req.CreateResponse(HttpStatusCode.OK);

View File

@ -28,7 +28,7 @@ namespace ApiService.TestHooks {
var poolRes = _proxyForward.SearchForward( var poolRes = _proxyForward.SearchForward(
UriExtension.GetGuid("scaleSetId", query), UriExtension.GetGuid("scaleSetId", query),
UriExtension.GetString("region", query), UriExtension.GetString("region", query) is string region ? Region.Parse(region) : null,
UriExtension.GetGuid("machineId", query), UriExtension.GetGuid("machineId", query),
UriExtension.GetGuid("proxyId", query), UriExtension.GetGuid("proxyId", query),
UriExtension.GetInt("dstPort", query)); UriExtension.GetInt("dstPort", query));

View File

@ -105,7 +105,7 @@ public class AutoScaleOperations : Orm<AutoScale>, IAutoScaleOperations {
return OneFuzzResultVoid.Ok; return OneFuzzResultVoid.Ok;
} }
private async Async.Task<OneFuzzResult<AutoscaleSettingResource>> CreateAutoScaleResourceFor(Guid resourceId, string location, AutoscaleProfile profile) { private async Async.Task<OneFuzzResult<AutoscaleSettingResource>> CreateAutoScaleResourceFor(Guid resourceId, Region location, AutoscaleProfile profile) {
_logTracer.Info($"Creating auto-scale resource for: {resourceId}"); _logTracer.Info($"Creating auto-scale resource for: {resourceId}");
var resourceGroup = _context.Creds.GetBaseResourceGroup(); var resourceGroup = _context.Creds.GetBaseResourceGroup();

View File

@ -458,21 +458,24 @@ public class Config : IConfig {
return ResultVoid<TaskConfigError>.Ok(); return ResultVoid<TaskConfigError>.Ok();
} }
var exist = new HashSet<string>(); var exist = new HashSet<Container>();
var containers = new Dictionary<ContainerType, List<Container>>(); var containers = new Dictionary<ContainerType, List<Container>>();
foreach (var container in config.Containers) { foreach (var container in config.Containers) {
if (exist.Contains(container.Name.ContainerName)) { if (exist.Contains(container.Name)) {
continue; continue;
} }
if (await _containers.FindContainer(container.Name, StorageType.Corpus) == null) { if (await _containers.FindContainer(container.Name, StorageType.Corpus) == null) {
return ResultVoid<TaskConfigError>.Error(new TaskConfigError($"missing container: {container.Name}")); return ResultVoid<TaskConfigError>.Error(new TaskConfigError($"missing container: {container.Name}"));
} }
exist.Add(container.Name.ContainerName);
exist.Add(container.Name);
if (!containers.ContainsKey(container.Type)) { if (!containers.ContainsKey(container.Type)) {
containers.Add(container.Type, new List<Container>()); containers.Add(container.Type, new List<Container>());
} }
containers[container.Type].Add(container.Name); containers[container.Type].Add(container.Name);
} }

View File

@ -28,7 +28,7 @@ public interface IContainers {
public Async.Task<bool> BlobExists(Container container, string name, StorageType storageType); public Async.Task<bool> BlobExists(Container container, string name, StorageType storageType);
public Async.Task<Uri> AddContainerSasUrl(Uri uri, TimeSpan? duration = null); public Async.Task<Uri> AddContainerSasUrl(Uri uri, TimeSpan? duration = null);
public Async.Task<Dictionary<string, IDictionary<string, string>>> GetContainers(StorageType corpus); public Async.Task<Dictionary<Container, IDictionary<string, string>>> GetContainers(StorageType corpus);
} }
public class Containers : IContainers { public class Containers : IContainers {
@ -36,7 +36,7 @@ public class Containers : IContainers {
private readonly IStorage _storage; private readonly IStorage _storage;
private readonly IServiceConfig _config; private readonly IServiceConfig _config;
static TimeSpan CONTAINER_SAS_DEFAULT_DURATION = TimeSpan.FromDays(30); static readonly TimeSpan CONTAINER_SAS_DEFAULT_DURATION = TimeSpan.FromDays(30);
public Containers(ILogTracer log, IStorage storage, IServiceConfig config) { public Containers(ILogTracer log, IStorage storage, IServiceConfig config) {
_log = log; _log = log;
@ -44,7 +44,7 @@ public class Containers : IContainers {
_config = config; _config = config;
_getInstanceId = new Lazy<Async.Task<Guid>>(async () => { _getInstanceId = new Lazy<Async.Task<Guid>>(async () => {
var blob = await GetBlob(new Container("base-config"), "instance_id", StorageType.Config); var blob = await GetBlob(WellKnownContainers.BaseConfig, "instance_id", StorageType.Config);
if (blob == null) { if (blob == null) {
throw new Exception("Blob Not Found"); throw new Exception("Blob Not Found");
} }
@ -99,14 +99,14 @@ public class Containers : IContainers {
var account = _storage.ChooseAccount(storageType); var account = _storage.ChooseAccount(storageType);
var client = await _storage.GetBlobServiceClientForAccount(account); var client = await _storage.GetBlobServiceClientForAccount(account);
var containerName = _config.OneFuzzStoragePrefix + container.ContainerName; var containerName = _config.OneFuzzStoragePrefix + container;
var cc = client.GetBlobContainerClient(containerName); var cc = client.GetBlobContainerClient(containerName);
try { try {
await cc.CreateAsync(metadata: metadata); await cc.CreateAsync(metadata: metadata);
} catch (RequestFailedException ex) when (ex.ErrorCode == "ContainerAlreadyExists") { } catch (RequestFailedException ex) when (ex.ErrorCode == "ContainerAlreadyExists") {
// note: resource exists error happens during creation if the container // note: resource exists error happens during creation if the container
// is being deleted // is being deleted
_log.Error($"unable to create container. account: {account} container: {container.ContainerName} metadata: {metadata} - {ex.Message}"); _log.Error($"unable to create container. account: {account} container: {container} metadata: {metadata} - {ex.Message}");
return null; return null;
} }
@ -123,7 +123,7 @@ public class Containers : IContainers {
// # Secondary accounts, if they exist, are preferred for containers and have // # Secondary accounts, if they exist, are preferred for containers and have
// # increased IOP rates, this should be a slight optimization // # increased IOP rates, this should be a slight optimization
var containerName = _config.OneFuzzStoragePrefix + container.ContainerName; var containerName = _config.OneFuzzStoragePrefix + container;
foreach (var account in _storage.GetAccounts(storageType).Reverse()) { foreach (var account in _storage.GetAccounts(storageType).Reverse()) {
var accountClient = await _storage.GetBlobServiceClientForAccount(account); var accountClient = await _storage.GetBlobServiceClientForAccount(account);
@ -137,7 +137,7 @@ public class Containers : IContainers {
} }
public async Async.Task<Uri> GetFileSasUrl(Container container, string name, StorageType storageType, BlobSasPermissions permissions, TimeSpan? duration = null) { public async Async.Task<Uri> GetFileSasUrl(Container container, string name, StorageType storageType, BlobSasPermissions permissions, TimeSpan? duration = null) {
var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container.ContainerName} - {storageType}"); var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container} - {storageType}");
var blobClient = client.GetBlobClient(name); var blobClient = client.GetBlobClient(name);
var timeWindow = SasTimeWindow(duration ?? TimeSpan.FromDays(30)); var timeWindow = SasTimeWindow(duration ?? TimeSpan.FromDays(30));
return _storage.GenerateBlobSasUri(permissions, blobClient, timeWindow); return _storage.GenerateBlobSasUri(permissions, blobClient, timeWindow);
@ -160,7 +160,7 @@ public class Containers : IContainers {
} }
public async Async.Task SaveBlob(Container container, string name, string data, StorageType storageType) { public async Async.Task SaveBlob(Container container, string name, string data, StorageType storageType) {
var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container.ContainerName} - {storageType}"); var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container} - {storageType}");
await client.GetBlobClient(name).UploadAsync(new BinaryData(data), overwrite: true); await client.GetBlobClient(name).UploadAsync(new BinaryData(data), overwrite: true);
} }
@ -192,23 +192,25 @@ public class Containers : IContainers {
} }
public async Task<Uri> GetContainerSasUrl(Container container, StorageType storageType, BlobContainerSasPermissions permissions, TimeSpan? duration = null) { public async Task<Uri> GetContainerSasUrl(Container container, StorageType storageType, BlobContainerSasPermissions permissions, TimeSpan? duration = null) {
var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container.ContainerName} - {storageType}"); var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container} - {storageType}");
var timeWindow = SasTimeWindow(duration ?? CONTAINER_SAS_DEFAULT_DURATION); var timeWindow = SasTimeWindow(duration ?? CONTAINER_SAS_DEFAULT_DURATION);
return _storage.GenerateBlobContainerSasUri(permissions, client, timeWindow); return _storage.GenerateBlobContainerSasUri(permissions, client, timeWindow);
} }
public async Async.Task<bool> BlobExists(Container container, string name, StorageType storageType) { public async Async.Task<bool> BlobExists(Container container, string name, StorageType storageType) {
var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container.ContainerName} - {storageType}"); var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container} - {storageType}");
return await client.GetBlobClient(name).ExistsAsync(); return await client.GetBlobClient(name).ExistsAsync();
} }
public async Task<Dictionary<string, IDictionary<string, string>>> GetContainers(StorageType corpus) { public async Task<Dictionary<Container, IDictionary<string, string>>> GetContainers(StorageType corpus) {
var accounts = _storage.GetAccounts(corpus); var accounts = _storage.GetAccounts(corpus);
IEnumerable<IEnumerable<KeyValuePair<string, IDictionary<string, string>>>> data = IEnumerable<IEnumerable<KeyValuePair<Container, IDictionary<string, string>>>> data =
await Async.Task.WhenAll(accounts.Select(async acc => { await Async.Task.WhenAll(accounts.Select(async acc => {
var service = await _storage.GetBlobServiceClientForAccount(acc); var service = await _storage.GetBlobServiceClientForAccount(acc);
return await service.GetBlobContainersAsync(BlobContainerTraits.Metadata).Select(container => return await service
KeyValuePair.Create(container.Name, container.Properties.Metadata)).ToListAsync(); .GetBlobContainersAsync(BlobContainerTraits.Metadata)
.Select(container => KeyValuePair.Create(Container.Parse(container.Name), container.Properties.Metadata))
.ToListAsync();
})); }));
return new(data.SelectMany(x => x)); return new(data.SelectMany(x => x));

View File

@ -25,14 +25,14 @@ public interface ICreds {
public SubscriptionResource GetSubscriptionResource(); public SubscriptionResource GetSubscriptionResource();
public Async.Task<string> GetBaseRegion(); public Async.Task<Region> GetBaseRegion();
public Async.Task<IReadOnlyList<Region>> GetRegions();
public Uri GetInstanceUrl(); public Uri GetInstanceUrl();
public Async.Task<Guid> GetScalesetPrincipalId(); public Async.Task<Guid> GetScalesetPrincipalId();
public GenericResource ParseResourceId(string resourceId); public GenericResource ParseResourceId(string resourceId);
public GenericResource ParseResourceId(ResourceIdentifier resourceId); public GenericResource ParseResourceId(ResourceIdentifier resourceId);
public Async.Task<GenericResource> GetData(GenericResource resource); public Async.Task<GenericResource> GetData(GenericResource resource);
Async.Task<IReadOnlyList<string>> GetRegions();
public ResourceIdentifier GetScalesetIdentityResourcePath(); public ResourceIdentifier GetScalesetIdentityResourcePath();
} }
@ -95,13 +95,13 @@ public sealed class Creds : ICreds {
return ArmClient.GetSubscriptionResource(id); return ArmClient.GetSubscriptionResource(id);
} }
public Async.Task<string> GetBaseRegion() { public Async.Task<Region> GetBaseRegion() {
return _cache.GetOrCreateAsync(nameof(GetBaseRegion), async _ => { return _cache.GetOrCreateAsync(nameof(GetBaseRegion), async _ => {
var rg = await ArmClient.GetResourceGroupResource(GetResourceGroupResourceIdentifier()).GetAsync(); var rg = await ArmClient.GetResourceGroupResource(GetResourceGroupResourceIdentifier()).GetAsync();
if (rg.GetRawResponse().IsError) { if (rg.GetRawResponse().IsError) {
throw new Exception($"Failed to get base region due to [{rg.GetRawResponse().Status}] {rg.GetRawResponse().ReasonPhrase}"); throw new Exception($"Failed to get base region due to [{rg.GetRawResponse().Status}] {rg.GetRawResponse().ReasonPhrase}");
} }
return rg.Value.Data.Location.Name; return Region.Parse(rg.Value.Data.Location.Name);
}); });
} }
@ -144,8 +144,8 @@ public sealed class Creds : ICreds {
return resource; return resource;
} }
public Task<IReadOnlyList<string>> GetRegions() public Task<IReadOnlyList<Region>> GetRegions()
=> _cache.GetOrCreateAsync<IReadOnlyList<string>>( => _cache.GetOrCreateAsync<IReadOnlyList<Region>>(
nameof(Creds) + "." + nameof(GetRegions), nameof(Creds) + "." + nameof(GetRegions),
async entry => { async entry => {
// cache for one day // cache for one day
@ -153,7 +153,7 @@ public sealed class Creds : ICreds {
var subscriptionId = SubscriptionResource.CreateResourceIdentifier(GetSubscription()); var subscriptionId = SubscriptionResource.CreateResourceIdentifier(GetSubscription());
return await ArmClient.GetSubscriptionResource(subscriptionId) return await ArmClient.GetSubscriptionResource(subscriptionId)
.GetLocationsAsync() .GetLocationsAsync()
.Select(x => x.Name) .Select(x => Region.Parse(x.Name))
.ToListAsync(); .ToListAsync();
}); });

View File

@ -12,15 +12,13 @@ public interface IExtensions {
Async.Task<IList<VirtualMachineScaleSetExtensionData>> FuzzExtensions(Pool pool, Scaleset scaleset); Async.Task<IList<VirtualMachineScaleSetExtensionData>> FuzzExtensions(Pool pool, Scaleset scaleset);
Async.Task<Dictionary<string, VirtualMachineExtensionData>> ReproExtensions(AzureLocation region, Os reproOs, Guid reproId, ReproConfig reproConfig, Container? setupContainer); Async.Task<Dictionary<string, VirtualMachineExtensionData>> ReproExtensions(AzureLocation region, Os reproOs, Guid reproId, ReproConfig reproConfig, Container? setupContainer);
Task<IList<VMExtensionWrapper>> ProxyManagerExtensions(string region, Guid proxyId); Task<IList<VMExtensionWrapper>> ProxyManagerExtensions(Region region, Guid proxyId);
} }
public class Extensions : IExtensions { public class Extensions : IExtensions {
IOnefuzzContext _context; private readonly IOnefuzzContext _context;
private static readonly JsonSerializerOptions _extensionSerializerOptions = new JsonSerializerOptions { private static readonly JsonSerializerOptions _extensionSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public Extensions(IOnefuzzContext context) { public Extensions(IOnefuzzContext context) {
_context = context; _context = context;
@ -227,11 +225,12 @@ public class Extensions : IExtensions {
var fileName = $"{pool.Name}/config.json"; var fileName = $"{pool.Name}/config.json";
var configJson = JsonSerializer.Serialize(config, EntityConverter.GetJsonSerializerOptions()); var configJson = JsonSerializer.Serialize(config, EntityConverter.GetJsonSerializerOptions());
await _context.Containers.SaveBlob(new Container("vm-scripts"), fileName, configJson, StorageType.Config); await _context.Containers.SaveBlob(WellKnownContainers.VmScripts, fileName, configJson, StorageType.Config);
return await ConfigUrl(new Container("vm-scripts"), fileName, false); return await ConfigUrl(WellKnownContainers.VmScripts, fileName, false);
} }
public async Async.Task<Uri?> BuildScaleSetScript(Pool pool, Scaleset scaleSet) { public async Async.Task<Uri?> BuildScaleSetScript(Pool pool, Scaleset scaleSet) {
List<string> commands = new(); List<string> commands = new();
var extension = pool.Os == Os.Windows ? "ps1" : "sh"; var extension = pool.Os == Os.Windows ? "ps1" : "sh";
@ -244,21 +243,23 @@ public class Extensions : IExtensions {
commands.Add($"Set-Content -Path {sshPath} -Value \"{sshKey}\""); commands.Add($"Set-Content -Path {sshPath} -Value \"{sshKey}\"");
} }
await _context.Containers.SaveBlob(new Container("vm-scripts"), fileName, string.Join(sep, commands) + sep, StorageType.Config); await _context.Containers.SaveBlob(WellKnownContainers.VmScripts, fileName, string.Join(sep, commands) + sep, StorageType.Config);
return await _context.Containers.GetFileUrl(new Container("vm-scripts"), fileName, StorageType.Config); return await _context.Containers.GetFileUrl(WellKnownContainers.VmScripts, fileName, StorageType.Config);
} }
public async Async.Task UpdateManagedScripts() { public async Async.Task UpdateManagedScripts() {
var instanceSpecificSetupSas = await _context.Containers.GetContainerSasUrl(new Container("instance-specific-setup"), StorageType.Config, BlobContainerSasPermissions.List | BlobContainerSasPermissions.Read); var listAndRead = BlobContainerSasPermissions.List | BlobContainerSasPermissions.Read;
var toolsSas = await _context.Containers.GetContainerSasUrl(new Container("tools"), StorageType.Config, BlobContainerSasPermissions.List | BlobContainerSasPermissions.Read); var instanceSpecificSetupSas = await _context.Containers.GetContainerSasUrl(WellKnownContainers.InstanceSpecificSetup, StorageType.Config, listAndRead);
var toolsSas = await _context.Containers.GetContainerSasUrl(WellKnownContainers.Tools, StorageType.Config, listAndRead);
string[] commands = { string[] commands = {
$"azcopy sync '{instanceSpecificSetupSas}' instance-specific-setup", $"azcopy sync '{instanceSpecificSetupSas}' instance-specific-setup",
$"azcopy sync '{toolsSas}' tools" $"azcopy sync '{toolsSas}' tools"
}; };
await _context.Containers.SaveBlob(new Container("vm-scripts"), "managed.ps1", string.Join("\r\n", commands) + "\r\n", StorageType.Config); await _context.Containers.SaveBlob(WellKnownContainers.VmScripts, "managed.ps1", string.Join("\r\n", commands) + "\r\n", StorageType.Config);
await _context.Containers.SaveBlob(new Container("vm-scripts"), "managed.sh", string.Join("\n", commands) + "\n", StorageType.Config); await _context.Containers.SaveBlob(WellKnownContainers.VmScripts, "managed.sh", string.Join("\n", commands) + "\n", StorageType.Config);
} }
public async Async.Task<VMExtensionWrapper> AgentConfig(AzureLocation region, Os vmOs, AgentMode mode, List<Uri>? urls = null, bool withSas = false) { public async Async.Task<VMExtensionWrapper> AgentConfig(AzureLocation region, Os vmOs, AgentMode mode, List<Uri>? urls = null, bool withSas = false) {
@ -267,10 +268,10 @@ public class Extensions : IExtensions {
var managedIdentity = JsonSerializer.Serialize(new { ManagedIdentity = new Dictionary<string, string>() }, _extensionSerializerOptions); var managedIdentity = JsonSerializer.Serialize(new { ManagedIdentity = new Dictionary<string, string>() }, _extensionSerializerOptions);
if (vmOs == Os.Windows) { if (vmOs == Os.Windows) {
var vmScripts = await ConfigUrl(new Container("vm-scripts"), "managed.ps1", withSas) ?? throw new Exception("failed to get VmScripts config url"); var vmScripts = await ConfigUrl(WellKnownContainers.VmScripts, "managed.ps1", withSas) ?? throw new Exception("failed to get VmScripts config url");
var toolsAzCopy = await ConfigUrl(new Container("tools"), "win64/azcopy.exe", withSas) ?? throw new Exception("failed to get toolsAzCopy config url"); var toolsAzCopy = await ConfigUrl(WellKnownContainers.Tools, "win64/azcopy.exe", withSas) ?? throw new Exception("failed to get toolsAzCopy config url");
var toolsSetup = await ConfigUrl(new Container("tools"), "win64/setup.ps1", withSas) ?? throw new Exception("failed to get toolsSetup config url"); var toolsSetup = await ConfigUrl(WellKnownContainers.Tools, "win64/setup.ps1", withSas) ?? throw new Exception("failed to get toolsSetup config url");
var toolsOneFuzz = await ConfigUrl(new Container("tools"), "win64/onefuzz.ps1", withSas) ?? throw new Exception("failed to get toolsOneFuzz config url"); var toolsOneFuzz = await ConfigUrl(WellKnownContainers.Tools, "win64/onefuzz.ps1", withSas) ?? throw new Exception("failed to get toolsOneFuzz config url");
urlsUpdated.Add(vmScripts); urlsUpdated.Add(vmScripts);
urlsUpdated.Add(toolsAzCopy); urlsUpdated.Add(toolsAzCopy);
@ -293,9 +294,9 @@ public class Extensions : IExtensions {
return extension; return extension;
} else if (vmOs == Os.Linux) { } else if (vmOs == Os.Linux) {
var vmScripts = await ConfigUrl(new Container("vm-scripts"), "managed.sh", withSas) ?? throw new Exception("failed to get VmScripts config url"); var vmScripts = await ConfigUrl(WellKnownContainers.VmScripts, "managed.sh", withSas) ?? throw new Exception("failed to get VmScripts config url");
var toolsAzCopy = await ConfigUrl(new Container("tools"), "linux/azcopy", withSas) ?? throw new Exception("failed to get toolsAzCopy config url"); var toolsAzCopy = await ConfigUrl(WellKnownContainers.Tools, "linux/azcopy", withSas) ?? throw new Exception("failed to get toolsAzCopy config url");
var toolsSetup = await ConfigUrl(new Container("tools"), "linux/setup.sh", withSas) ?? throw new Exception("failed to get toolsSetup config url"); var toolsSetup = await ConfigUrl(WellKnownContainers.Tools, "linux/setup.sh", withSas) ?? throw new Exception("failed to get toolsSetup config url");
urlsUpdated.Add(vmScripts); urlsUpdated.Add(vmScripts);
urlsUpdated.Add(toolsAzCopy); urlsUpdated.Add(toolsAzCopy);
@ -423,7 +424,7 @@ public class Extensions : IExtensions {
} }
await _context.Containers.SaveBlob( await _context.Containers.SaveBlob(
new Container("task-configs"), WellKnownContainers.TaskConfigs,
$"{reproId}/{scriptName}", $"{reproId}/{scriptName}",
taskScript, taskScript,
StorageType.Config StorageType.Config
@ -433,13 +434,13 @@ public class Extensions : IExtensions {
urls.AddRange(new List<Uri>() urls.AddRange(new List<Uri>()
{ {
await _context.Containers.GetFileSasUrl( await _context.Containers.GetFileSasUrl(
new Container("repro-scripts"), WellKnownContainers.ReproScripts,
reproFile, reproFile,
StorageType.Config, StorageType.Config,
BlobSasPermissions.Read BlobSasPermissions.Read
), ),
await _context.Containers.GetFileSasUrl( await _context.Containers.GetFileSasUrl(
new Container("task-configs"), WellKnownContainers.TaskConfigs,
$"{reproId}/{scriptName}", $"{reproId}/{scriptName}",
StorageType.Config, StorageType.Config,
BlobSasPermissions.Read BlobSasPermissions.Read
@ -460,13 +461,18 @@ public class Extensions : IExtensions {
return extensionsDict; return extensionsDict;
} }
public async Task<IList<VMExtensionWrapper>> ProxyManagerExtensions(string region, Guid proxyId) { public async Task<IList<VMExtensionWrapper>> ProxyManagerExtensions(Region region, Guid proxyId) {
var config = await _context.Containers.GetFileSasUrl(new Container("proxy-configs"), var config = await _context.Containers.GetFileSasUrl(
$"{region}/{proxyId}/config.json", StorageType.Config, BlobSasPermissions.Read); WellKnownContainers.ProxyConfigs,
$"{region}/{proxyId}/config.json",
var proxyManager = await _context.Containers.GetFileSasUrl(new Container("tools"), StorageType.Config,
$"linux/onefuzz-proxy-manager", StorageType.Config, BlobSasPermissions.Read); BlobSasPermissions.Read);
var proxyManager = await _context.Containers.GetFileSasUrl(
WellKnownContainers.Tools,
$"linux/onefuzz-proxy-manager",
StorageType.Config,
BlobSasPermissions.Read);
var baseExtension = var baseExtension =
await AgentConfig(region, Os.Linux, AgentMode.Proxy, new List<Uri> { config, proxyManager }, true); await AgentConfig(region, Os.Linux, AgentMode.Proxy, new List<Uri> { config, proxyManager }, true);

View File

@ -7,7 +7,7 @@ namespace Microsoft.OneFuzz.Service;
public record ImageInfo(string Publisher, string Offer, string Sku, string Version); public record ImageInfo(string Publisher, string Offer, string Sku, string Version);
public interface IImageOperations { public interface IImageOperations {
public Async.Task<OneFuzzResult<Os>> GetOs(string region, string image); public Async.Task<OneFuzzResult<Os>> GetOs(Region region, string image);
public static ImageInfo GetImageInfo(string image) { public static ImageInfo GetImageInfo(string image) {
var imageParts = image.Split(":"); var imageParts = image.Split(":");
@ -32,7 +32,7 @@ public class ImageOperations : IImageOperations {
_context = context; _context = context;
} }
public async Task<OneFuzzResult<Os>> GetOs(string region, string image) { public async Task<OneFuzzResult<Os>> GetOs(Region region, string image) {
string? name = null; string? name = null;
try { try {
var parsed = _context.Creds.ParseResourceId(image); var parsed = _context.Creds.ParseResourceId(image);
@ -86,7 +86,7 @@ public class ImageOperations : IImageOperations {
if (string.Equals(imageInfo.Version, "latest", StringComparison.Ordinal)) { if (string.Equals(imageInfo.Version, "latest", StringComparison.Ordinal)) {
version = version =
(await subscription.GetVirtualMachineImagesAsync( (await subscription.GetVirtualMachineImagesAsync(
region, region.String,
imageInfo.Publisher, imageInfo.Publisher,
imageInfo.Offer, imageInfo.Offer,
imageInfo.Sku, imageInfo.Sku,
@ -97,7 +97,7 @@ public class ImageOperations : IImageOperations {
} }
name = (await subscription.GetVirtualMachineImageAsync( name = (await subscription.GetVirtualMachineImageAsync(
region, region.String,
imageInfo.Publisher, imageInfo.Publisher,
imageInfo.Offer, imageInfo.Offer,
imageInfo.Sku imageInfo.Sku

View File

@ -12,7 +12,7 @@ namespace Microsoft.OneFuzz.Service;
public interface IIpOperations { public interface IIpOperations {
public Async.Task<NetworkInterfaceResource?> GetPublicNic(string resourceGroup, string name); public Async.Task<NetworkInterfaceResource?> GetPublicNic(string resourceGroup, string name);
public Async.Task<OneFuzzResultVoid> CreatePublicNic(string resourceGroup, string name, string region, Nsg? nsg); public Async.Task<OneFuzzResultVoid> CreatePublicNic(string resourceGroup, string name, Region region, Nsg? nsg);
public Async.Task<string?> GetPublicIp(ResourceIdentifier resourceId); public Async.Task<string?> GetPublicIp(ResourceIdentifier resourceId);
@ -26,7 +26,7 @@ public interface IIpOperations {
public Async.Task<string?> GetScalesetInstanceIp(Guid scalesetId, Guid machineId); public Async.Task<string?> GetScalesetInstanceIp(Guid scalesetId, Guid machineId);
public Async.Task CreateIp(string resourceGroup, string name, string region); public Async.Task CreateIp(string resourceGroup, string name, Region region);
} }
@ -120,7 +120,7 @@ public class IpOperations : IIpOperations {
} }
} }
public async Task<OneFuzzResultVoid> CreatePublicNic(string resourceGroup, string name, string region, Nsg? nsg) { public async Task<OneFuzzResultVoid> CreatePublicNic(string resourceGroup, string name, Region region, Nsg? nsg) {
_logTracer.Info($"creating nic for {resourceGroup}:{name} in {region}"); _logTracer.Info($"creating nic for {resourceGroup}:{name} in {region}");
var network = await Network.Init(region, _context); var network = await Network.Init(region, _context);
@ -190,7 +190,7 @@ public class IpOperations : IIpOperations {
return OneFuzzResultVoid.Ok; return OneFuzzResultVoid.Ok;
} }
public async Async.Task CreateIp(string resourceGroup, string name, string region) { public async Async.Task CreateIp(string resourceGroup, string name, Region region) {
var ipParams = new PublicIPAddressData() { var ipParams = new PublicIPAddressData() {
Location = region, Location = region,
PublicIPAllocationMethod = NetworkIPAllocationMethod.Dynamic PublicIPAllocationMethod = NetworkIPAllocationMethod.Dynamic
@ -259,5 +259,3 @@ public class IpOperations : IIpOperations {
} }
} }
} }

View File

@ -6,7 +6,7 @@ namespace Microsoft.OneFuzz.Service;
public class Network { public class Network {
private readonly string _name; private readonly string _name;
private readonly string _group; private readonly string _group;
private readonly string _region; private readonly Region _region;
private readonly IOnefuzzContext _context; private readonly IOnefuzzContext _context;
private readonly NetworkConfig _networkConfig; private readonly NetworkConfig _networkConfig;
@ -14,7 +14,7 @@ public class Network {
// This was generated randomly and should be preserved moving forwards // This was generated randomly and should be preserved moving forwards
static Guid NETWORK_GUID_NAMESPACE = Guid.Parse("372977ad-b533-416a-b1b4-f770898e0b11"); static Guid NETWORK_GUID_NAMESPACE = Guid.Parse("372977ad-b533-416a-b1b4-f770898e0b11");
public Network(string region, string group, string name, IOnefuzzContext context, NetworkConfig networkConfig) { public Network(Region region, string group, string name, IOnefuzzContext context, NetworkConfig networkConfig) {
_region = region; _region = region;
_group = group; _group = group;
_name = name; _name = name;
@ -22,7 +22,7 @@ public class Network {
_networkConfig = networkConfig; _networkConfig = networkConfig;
} }
public static async Async.Task<Network> Init(string region, IOnefuzzContext context) { public static async Async.Task<Network> Init(Region region, IOnefuzzContext context) {
var group = context.Creds.GetBaseResourceGroup(); var group = context.Creds.GetBaseResourceGroup();
var instanceConfig = await context.ConfigOperations.Fetch(); var instanceConfig = await context.ConfigOperations.Fetch();
var networkConfig = instanceConfig.NetworkConfig; var networkConfig = instanceConfig.NetworkConfig;
@ -33,16 +33,14 @@ public class Network {
// configs. // configs.
string name; string name;
if (networkConfig.AddressSpace == NetworkConfig.Default.AddressSpace && networkConfig.Subnet == NetworkConfig.Default.Subnet) { if (networkConfig.AddressSpace == NetworkConfig.Default.AddressSpace && networkConfig.Subnet == NetworkConfig.Default.Subnet) {
name = region; name = region.String;
} else { } else {
//TODO: Remove dependency on "Faithlife" //TODO: Remove dependency on "Faithlife"
var networkId = Faithlife.Utility.GuidUtility.Create(NETWORK_GUID_NAMESPACE, string.Join("|", networkConfig.AddressSpace, networkConfig.Subnet), 5); var networkId = Faithlife.Utility.GuidUtility.Create(NETWORK_GUID_NAMESPACE, string.Join("|", networkConfig.AddressSpace, networkConfig.Subnet), 5);
name = $"{region}-{networkId}"; name = $"{region}-{networkId}";
} }
return new Network(region, group, name, context, networkConfig); return new Network(region, group, name, context, networkConfig);
} }

View File

@ -7,7 +7,7 @@ namespace Microsoft.OneFuzz.Service;
public interface INotificationOperations : IOrm<Notification> { public interface INotificationOperations : IOrm<Notification> {
Async.Task NewFiles(Container container, string filename, bool failTaskOnTransientError); Async.Task NewFiles(Container container, string filename, bool failTaskOnTransientError);
IAsyncEnumerable<Notification> GetNotifications(Container container); IAsyncEnumerable<Notification> GetNotifications(Container container);
IAsyncEnumerable<(Task, IEnumerable<string>)> GetQueueTasks(); IAsyncEnumerable<(Task, IEnumerable<Container>)> GetQueueTasks();
Async.Task<OneFuzzResult<Notification>> Create(Container container, NotificationTemplate config, bool replaceExisting); Async.Task<OneFuzzResult<Notification>> Create(Container container, NotificationTemplate config, bool replaceExisting);
} }
@ -52,8 +52,8 @@ public class NotificationOperations : Orm<Notification>, INotificationOperations
} }
await foreach (var (task, containers) in GetQueueTasks()) { await foreach (var (task, containers) in GetQueueTasks()) {
if (containers.Contains(container.ContainerName)) { if (containers.Contains(container)) {
_logTracer.Info($"queuing input {container.ContainerName} {filename} {task.TaskId}"); _logTracer.Info($"queuing input {container} {filename} {task.TaskId}");
var url = _context.Containers.GetFileSasUrl(container, filename, StorageType.Corpus, BlobSasPermissions.Read | BlobSasPermissions.Delete); var url = _context.Containers.GetFileSasUrl(container, filename, StorageType.Corpus, BlobSasPermissions.Read | BlobSasPermissions.Delete);
await _context.Queue.SendMessage(task.TaskId.ToString(), url?.ToString() ?? "", StorageType.Corpus); await _context.Queue.SendMessage(task.TaskId.ToString(), url?.ToString() ?? "", StorageType.Corpus);
} }
@ -77,10 +77,10 @@ public class NotificationOperations : Orm<Notification>, INotificationOperations
} }
public IAsyncEnumerable<Notification> GetNotifications(Container container) { public IAsyncEnumerable<Notification> GetNotifications(Container container) {
return QueryAsync(filter: $"container eq '{container.ContainerName}'"); return QueryAsync(filter: $"container eq '{container}'");
} }
public IAsyncEnumerable<(Task, IEnumerable<string>)> GetQueueTasks() { public IAsyncEnumerable<(Task, IEnumerable<Container>)> GetQueueTasks() {
// Nullability mismatch: We filter tuples where the containers are null // Nullability mismatch: We filter tuples where the containers are null
return _context.TaskOperations.SearchStates(states: TaskStateHelper.AvailableStates) return _context.TaskOperations.SearchStates(states: TaskStateHelper.AvailableStates)
.Select(task => (task, _context.TaskOperations.GetInputContainerQueues(task.Config))) .Select(task => (task, _context.TaskOperations.GetInputContainerQueues(task.Config)))
@ -93,7 +93,7 @@ public class NotificationOperations : Orm<Notification>, INotificationOperations
} }
if (replaceExisting) { if (replaceExisting) {
var existing = this.SearchByRowKeys(new[] { container.ContainerName }); var existing = this.SearchByRowKeys(new[] { container.String });
await foreach (var existingEntry in existing) { await foreach (var existingEntry in existing) {
_logTracer.Info($"replacing existing notification: {existingEntry.NotificationId} - {container}"); _logTracer.Info($"replacing existing notification: {existingEntry.NotificationId} - {container}");
await this.Delete(existingEntry); await this.Delete(existingEntry);

View File

@ -8,7 +8,7 @@ namespace Microsoft.OneFuzz.Service {
Async.Task<NetworkSecurityGroupResource?> GetNsg(string name); Async.Task<NetworkSecurityGroupResource?> GetNsg(string name);
public Async.Task<OneFuzzResult<bool>> AssociateSubnet(string name, VirtualNetworkResource vnet, SubnetResource subnet); public Async.Task<OneFuzzResult<bool>> AssociateSubnet(string name, VirtualNetworkResource vnet, SubnetResource subnet);
IAsyncEnumerable<NetworkSecurityGroupResource> ListNsgs(); IAsyncEnumerable<NetworkSecurityGroupResource> ListNsgs();
bool OkToDelete(HashSet<string> active_regions, string nsg_region, string nsg_name); bool OkToDelete(IReadOnlySet<Region> active_regions, Region nsg_region, string nsg_name);
Async.Task<bool> StartDeleteNsg(string name); Async.Task<bool> StartDeleteNsg(string name);
Async.Task<OneFuzzResultVoid> DissociateNic(Nsg nsg, NetworkInterfaceResource nic); Async.Task<OneFuzzResultVoid> DissociateNic(Nsg nsg, NetworkInterfaceResource nic);
@ -128,8 +128,8 @@ namespace Microsoft.OneFuzz.Service {
return _context.Creds.GetResourceGroupResource().GetNetworkSecurityGroups().GetAllAsync(); return _context.Creds.GetResourceGroupResource().GetNetworkSecurityGroups().GetAllAsync();
} }
public bool OkToDelete(HashSet<string> active_regions, string nsg_region, string nsg_name) { public bool OkToDelete(IReadOnlySet<Region> active_regions, Region nsg_region, string nsg_name) {
return !active_regions.Contains(nsg_region) && nsg_region == nsg_name; return !active_regions.Contains(nsg_region) && Nsg.NameFromRegion(nsg_region) == nsg_name;
} }
/// <summary> /// <summary>
@ -164,7 +164,7 @@ namespace Microsoft.OneFuzz.Service {
return await CreateNsg(nsg.Name, nsg.Region); return await CreateNsg(nsg.Name, nsg.Region);
} }
private async Task<OneFuzzResultVoid> CreateNsg(string name, string location) { private async Task<OneFuzzResultVoid> CreateNsg(string name, Region location) {
var resourceGroup = _context.Creds.GetBaseResourceGroup(); var resourceGroup = _context.Creds.GetBaseResourceGroup();
_logTracer.Info($"creating nsg {resourceGroup}:{location}:{name}"); _logTracer.Info($"creating nsg {resourceGroup}:{location}:{name}");

View File

@ -5,10 +5,10 @@ namespace Microsoft.OneFuzz.Service;
public interface IProxyForwardOperations : IOrm<ProxyForward> { public interface IProxyForwardOperations : IOrm<ProxyForward> {
IAsyncEnumerable<ProxyForward> SearchForward(Guid? scalesetId = null, string? region = null, Guid? machineId = null, Guid? proxyId = null, int? dstPort = null); IAsyncEnumerable<ProxyForward> SearchForward(Guid? scalesetId = null, Region? region = null, Guid? machineId = null, Guid? proxyId = null, int? dstPort = null);
Forward ToForward(ProxyForward proxyForward); Forward ToForward(ProxyForward proxyForward);
Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(string region, Guid scalesetId, Guid machineId, int dstPort, int duration); Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(Region region, Guid scalesetId, Guid machineId, int dstPort, int duration);
Task<HashSet<string>> RemoveForward(Guid scalesetId, Guid? machineId = null, int? dstPort = null, Guid? proxyId = null); Task<HashSet<Region>> RemoveForward(Guid scalesetId, Guid? machineId = null, int? dstPort = null, Guid? proxyId = null);
} }
@ -20,7 +20,7 @@ public class ProxyForwardOperations : Orm<ProxyForward>, IProxyForwardOperations
} }
public IAsyncEnumerable<ProxyForward> SearchForward(Guid? scalesetId = null, string? region = null, Guid? machineId = null, Guid? proxyId = null, int? dstPort = null) { public IAsyncEnumerable<ProxyForward> SearchForward(Guid? scalesetId = null, Region? region = null, Guid? machineId = null, Guid? proxyId = null, int? dstPort = null) {
var conditions = var conditions =
new[] { new[] {
@ -40,7 +40,7 @@ public class ProxyForwardOperations : Orm<ProxyForward>, IProxyForwardOperations
return new Forward(proxyForward.Port, proxyForward.DstPort, proxyForward.DstIp); return new Forward(proxyForward.Port, proxyForward.DstPort, proxyForward.DstIp);
} }
public async Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(string region, Guid scalesetId, Guid machineId, int dstPort, int duration) { public async Task<OneFuzzResult<ProxyForward>> UpdateOrCreate(Region region, Guid scalesetId, Guid machineId, int dstPort, int duration) {
var privateIp = await _context.IpOperations.GetScalesetInstanceIp(scalesetId, machineId); var privateIp = await _context.IpOperations.GetScalesetInstanceIp(scalesetId, machineId);
if (privateIp == null) { if (privateIp == null) {
@ -88,10 +88,10 @@ public class ProxyForwardOperations : Orm<ProxyForward>, IProxyForwardOperations
} }
public async Task<HashSet<string>> RemoveForward(Guid scalesetId, Guid? machineId, int? dstPort, Guid? proxyId) { public async Task<HashSet<Region>> RemoveForward(Guid scalesetId, Guid? machineId, int? dstPort, Guid? proxyId) {
var entries = await SearchForward(scalesetId: scalesetId, machineId: machineId, proxyId: proxyId, dstPort: dstPort).ToListAsync(); var entries = await SearchForward(scalesetId: scalesetId, machineId: machineId, proxyId: proxyId, dstPort: dstPort).ToListAsync();
var regions = new HashSet<string>(); var regions = new HashSet<Region>();
foreach (var entry in entries) { foreach (var entry in entries) {
regions.Add(entry.Region); regions.Add(entry.Region);
await Delete(entry); await Delete(entry);

View File

@ -14,7 +14,7 @@ public interface IProxyOperations : IStatefulOrm<Proxy, VmState> {
bool IsAlive(Proxy proxy); bool IsAlive(Proxy proxy);
Async.Task SaveProxyConfig(Proxy proxy); Async.Task SaveProxyConfig(Proxy proxy);
bool IsOutdated(Proxy proxy); bool IsOutdated(Proxy proxy);
Async.Task<Proxy?> GetOrCreate(string region); Async.Task<Proxy?> GetOrCreate(Region region);
Task<bool> IsUsed(Proxy proxy); Task<bool> IsUsed(Proxy proxy);
// state transitions: // state transitions:
@ -27,13 +27,10 @@ public interface IProxyOperations : IStatefulOrm<Proxy, VmState> {
Async.Task<Proxy> Stopped(Proxy proxy); Async.Task<Proxy> Stopped(Proxy proxy);
} }
public class ProxyOperations : StatefulOrm<Proxy, VmState, ProxyOperations>, IProxyOperations { public class ProxyOperations : StatefulOrm<Proxy, VmState, ProxyOperations>, IProxyOperations {
static readonly TimeSpan PROXY_LIFESPAN = TimeSpan.FromDays(7);
static TimeSpan PROXY_LIFESPAN = TimeSpan.FromDays(7);
public ProxyOperations(ILogTracer log, IOnefuzzContext context) public ProxyOperations(ILogTracer log, IOnefuzzContext context)
: base(log.WithTag("Component", "scaleset-proxy"), context) { : base(log.WithTag("Component", "scaleset-proxy"), context) {
} }
@ -44,7 +41,7 @@ public class ProxyOperations : StatefulOrm<Proxy, VmState, ProxyOperations>, IPr
return await data.FirstOrDefaultAsync(); return await data.FirstOrDefaultAsync();
} }
public async Async.Task<Proxy?> GetOrCreate(string region) { public async Async.Task<Proxy?> GetOrCreate(Region region) {
var proxyList = QueryAsync(filter: $"region eq '{region}' and outdated eq false"); var proxyList = QueryAsync(filter: $"region eq '{region}' and outdated eq false");
await foreach (var proxy in proxyList) { await foreach (var proxy in proxyList) {
@ -117,7 +114,7 @@ public class ProxyOperations : StatefulOrm<Proxy, VmState, ProxyOperations>, IPr
public async Async.Task SaveProxyConfig(Proxy proxy) { public async Async.Task SaveProxyConfig(Proxy proxy) {
var forwards = await GetForwards(proxy); var forwards = await GetForwards(proxy);
var url = (await _context.Containers.GetFileSasUrl(new Container("proxy-configs"), $"{proxy.Region}/{proxy.ProxyId}/config.json", StorageType.Config, BlobSasPermissions.Read)).EnsureNotNull("Can't generate file sas"); var url = (await _context.Containers.GetFileSasUrl(WellKnownContainers.ProxyConfigs, $"{proxy.Region}/{proxy.ProxyId}/config.json", StorageType.Config, BlobSasPermissions.Read)).EnsureNotNull("Can't generate file sas");
var queueSas = await _context.Queue.GetQueueSas("proxy", StorageType.Config, QueueSasPermissions.Add).EnsureNotNull("can't generate queue sas") ?? throw new Exception("Queue sas is null"); var queueSas = await _context.Queue.GetQueueSas("proxy", StorageType.Config, QueueSasPermissions.Add).EnsureNotNull("can't generate queue sas") ?? throw new Exception("Queue sas is null");
var proxyConfig = new ProxyConfig( var proxyConfig = new ProxyConfig(
@ -130,7 +127,7 @@ public class ProxyOperations : StatefulOrm<Proxy, VmState, ProxyOperations>, IPr
MicrosoftTelemetryKey: _context.ServiceConfiguration.OneFuzzTelemetry.EnsureNotNull("missing Telemetry"), MicrosoftTelemetryKey: _context.ServiceConfiguration.OneFuzzTelemetry.EnsureNotNull("missing Telemetry"),
InstanceId: await _context.Containers.GetInstanceId()); InstanceId: await _context.Containers.GetInstanceId());
await _context.Containers.SaveBlob(new Container("proxy-configs"), $"{proxy.Region}/{proxy.ProxyId}/config.json", EntityConverter.ToJsonString(proxyConfig), StorageType.Config); await _context.Containers.SaveBlob(WellKnownContainers.ProxyConfigs, $"{proxy.Region}/{proxy.ProxyId}/config.json", EntityConverter.ToJsonString(proxyConfig), StorageType.Config);
} }
@ -175,7 +172,7 @@ public class ProxyOperations : StatefulOrm<Proxy, VmState, ProxyOperations>, IPr
return await SetState(proxy, VmState.ExtensionsLaunch); return await SetState(proxy, VmState.ExtensionsLaunch);
} }
} else { } else {
var nsg = new Nsg(proxy.Region, proxy.Region); var nsg = Nsg.ForRegion(proxy.Region);
var result = await _context.NsgOperations.Create(nsg); var result = await _context.NsgOperations.Create(nsg);
if (!result.IsOk) { if (!result.IsOk) {
return await SetFailed(proxy, result.ErrorV); return await SetFailed(proxy, result.ErrorV);

View File

@ -27,7 +27,7 @@ public class Reports : IReports {
} }
public async Async.Task<IReport?> GetReportOrRegression(Container container, string fileName, bool expectReports = false, params string[] args) { public async Async.Task<IReport?> GetReportOrRegression(Container container, string fileName, bool expectReports = false, params string[] args) {
var filePath = String.Join("/", new[] { container.ContainerName, fileName }); var filePath = string.Join("/", new[] { container.String, fileName });
if (!fileName.EndsWith(".json", StringComparison.Ordinal)) { if (!fileName.EndsWith(".json", StringComparison.Ordinal)) {
if (expectReports) { if (expectReports) {
_log.Error($"get_report invalid extension: {filePath}"); _log.Error($"get_report invalid extension: {filePath}");

View File

@ -131,7 +131,7 @@ public class ReproOperations : StatefulOrm<Repro, VmState, ReproOperations>, IRe
repro = repro with { State = VmState.ExtensionsLaunch }; repro = repro with { State = VmState.ExtensionsLaunch };
} }
} else { } else {
var nsg = new Nsg(vm.Region, vm.Region); var nsg = Nsg.ForRegion(vm.Region);
var result = await _context.NsgOperations.Create(nsg); var result = await _context.NsgOperations.Create(nsg);
if (!result.IsOk) { if (!result.IsOk) {
return await _context.ReproOperations.SetError(repro, result.ErrorV); return await _context.ReproOperations.SetError(repro, result.ErrorV);
@ -260,7 +260,7 @@ public class ReproOperations : StatefulOrm<Repro, VmState, ReproOperations>, IRe
foreach (var (fileName, fileContents) in files) { foreach (var (fileName, fileContents) in files) {
await _context.Containers.SaveBlob( await _context.Containers.SaveBlob(
new Container("repro-scripts"), WellKnownContainers.ReproScripts,
$"{repro.VmId}/{fileName}", $"{repro.VmId}/{fileName}",
fileContents, fileContents,
StorageType.Config StorageType.Config

View File

@ -184,7 +184,7 @@ public class Scheduler : IScheduler {
return (bucketConfig, workUnit); return (bucketConfig, workUnit);
} }
public record struct BucketId(Os os, Guid jobId, (string, string)? vm, PoolName? pool, string setupContainer, bool? reboot, Guid? unique); public record struct BucketId(Os os, Guid jobId, (string, string)? vm, PoolName? pool, Container setupContainer, bool? reboot, Guid? unique);
public static ILookup<BucketId, Task> BucketTasks(IEnumerable<Task> tasks) { public static ILookup<BucketId, Task> BucketTasks(IEnumerable<Task> tasks) {
@ -221,11 +221,11 @@ public class Scheduler : IScheduler {
}); });
} }
static string GetSetupContainer(TaskConfig config) { static Container GetSetupContainer(TaskConfig config) {
foreach (var container in config.Containers ?? throw new Exception("Missing containers")) { foreach (var container in config.Containers ?? throw new Exception("Missing containers")) {
if (container.Type == ContainerType.Setup) { if (container.Type == ContainerType.Setup) {
return container.Name.ContainerName; return container.Name;
} }
} }

View File

@ -11,7 +11,7 @@ public interface ISubnet {
Async.Task<SubnetResource?> GetSubnet(string vnetName, string subnetName); Async.Task<SubnetResource?> GetSubnet(string vnetName, string subnetName);
Async.Task<OneFuzzResultVoid> CreateVirtualNetwork(string resourceGroup, string name, string region, NetworkConfig networkConfig); Async.Task<OneFuzzResultVoid> CreateVirtualNetwork(string resourceGroup, string name, Region region, NetworkConfig networkConfig);
Async.Task<ResourceIdentifier?> GetSubnetId(string name, string subnetName); Async.Task<ResourceIdentifier?> GetSubnetId(string name, string subnetName);
} }
@ -29,7 +29,7 @@ public class Subnet : ISubnet {
_context = context; _context = context;
} }
public async Task<OneFuzzResultVoid> CreateVirtualNetwork(string resourceGroup, string name, string region, NetworkConfig networkConfig) { public async Task<OneFuzzResultVoid> CreateVirtualNetwork(string resourceGroup, string name, Region region, NetworkConfig networkConfig) {
_logTracer.Info($"creating subnet - resource group:{resourceGroup} name:{name} region: {region}"); _logTracer.Info($"creating subnet - resource group:{resourceGroup} name:{name} region: {region}");
var virtualNetParam = new VirtualNetworkData { var virtualNetParam = new VirtualNetworkData {

View File

@ -15,7 +15,7 @@ public interface ITaskOperations : IStatefulOrm<Task, TaskState> {
IAsyncEnumerable<Task> SearchStates(Guid? jobId = null, IEnumerable<TaskState>? states = null); IAsyncEnumerable<Task> SearchStates(Guid? jobId = null, IEnumerable<TaskState>? states = null);
IEnumerable<string>? GetInputContainerQueues(TaskConfig config); IEnumerable<Container>? GetInputContainerQueues(TaskConfig config);
IAsyncEnumerable<Task> SearchExpired(); IAsyncEnumerable<Task> SearchExpired();
Async.Task MarkStopping(Task task); Async.Task MarkStopping(Task task);
@ -75,7 +75,7 @@ public class TaskOperations : StatefulOrm<Task, TaskState, TaskOperations>, ITas
return QueryAsync(filter: queryString); return QueryAsync(filter: queryString);
} }
public IEnumerable<string>? GetInputContainerQueues(TaskConfig config) { public IEnumerable<Container>? GetInputContainerQueues(TaskConfig config) {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -226,7 +226,7 @@ public class VmOperations : IVmOperations {
async Task<OneFuzzResultVoid> CreateVm( async Task<OneFuzzResultVoid> CreateVm(
string name, string name,
string location, Region location,
string vmSku, string vmSku,
string image, string image,
string password, string password,

View File

@ -16,7 +16,7 @@ public interface IVmssOperations {
Async.Task<OneFuzzResultVoid> UpdateExtensions(Guid name, IList<VirtualMachineScaleSetExtensionData> extensions); Async.Task<OneFuzzResultVoid> UpdateExtensions(Guid name, IList<VirtualMachineScaleSetExtensionData> extensions);
Async.Task<VirtualMachineScaleSetData?> GetVmss(Guid name); Async.Task<VirtualMachineScaleSetData?> GetVmss(Guid name);
Async.Task<IReadOnlyList<string>> ListAvailableSkus(string region); Async.Task<IReadOnlyList<string>> ListAvailableSkus(Region region);
Async.Task<bool> DeleteVmss(Guid name, bool? forceDeletion = null); Async.Task<bool> DeleteVmss(Guid name, bool? forceDeletion = null);
@ -27,7 +27,7 @@ public interface IVmssOperations {
Async.Task<OneFuzzResultVoid> ResizeVmss(Guid name, long capacity); Async.Task<OneFuzzResultVoid> ResizeVmss(Guid name, long capacity);
Async.Task<OneFuzzResultVoid> CreateVmss( Async.Task<OneFuzzResultVoid> CreateVmss(
string location, Region location,
Guid name, Guid name,
string vmSku, string vmSku,
long vmCount, long vmCount,
@ -236,7 +236,7 @@ public class VmssOperations : IVmssOperations {
} }
public async Async.Task<OneFuzzResultVoid> CreateVmss( public async Async.Task<OneFuzzResultVoid> CreateVmss(
string location, Region location,
Guid name, Guid name,
string vmSku, string vmSku,
long vmCount, long vmCount,
@ -394,7 +394,7 @@ public class VmssOperations : IVmssOperations {
return null; return null;
} }
public Async.Task<IReadOnlyList<string>> ListAvailableSkus(string region) public Async.Task<IReadOnlyList<string>> ListAvailableSkus(Region region)
=> _cache.GetOrCreateAsync<IReadOnlyList<string>>($"compute-skus-{region}", async entry => { => _cache.GetOrCreateAsync<IReadOnlyList<string>>($"compute-skus-{region}", async entry => {
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(10)); entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(10));
@ -407,7 +407,7 @@ public class VmssOperations : IVmssOperations {
if (sku.Restrictions is not null) { if (sku.Restrictions is not null) {
foreach (var restriction in sku.Restrictions) { foreach (var restriction in sku.Restrictions) {
if (restriction.RestrictionsType == ResourceSkuRestrictionsType.Location && if (restriction.RestrictionsType == ResourceSkuRestrictionsType.Location &&
restriction.Values.Contains(region, StringComparer.OrdinalIgnoreCase)) { restriction.Values.Contains(region.String, StringComparer.OrdinalIgnoreCase)) {
available = false; available = false;
break; break;
} }

View File

@ -0,0 +1,12 @@

namespace Microsoft.OneFuzz.Service;
public static class WellKnownContainers {
public static readonly Container BaseConfig = Container.Parse("base-config");
public static readonly Container VmScripts = Container.Parse("vm-scripts");
public static readonly Container InstanceSpecificSetup = Container.Parse("instance-specific-setup");
public static readonly Container Tools = Container.Parse("tools");
public static readonly Container ReproScripts = Container.Parse("repro-scripts");
public static readonly Container TaskConfigs = Container.Parse("task-configs");
public static readonly Container ProxyConfigs = Container.Parse("proxy-configs");
}

View File

@ -225,17 +225,23 @@ public class EntityConverter {
private object? GetFieldValue(EntityInfo info, string name, TableEntity entity) { private object? GetFieldValue(EntityInfo info, string name, TableEntity entity) {
var ef = info.properties[name].First(); var ef = info.properties[name].First();
if (ef.kind == EntityPropertyKind.PartitionKey || ef.kind == EntityPropertyKind.RowKey) { if (ef.kind == EntityPropertyKind.PartitionKey || ef.kind == EntityPropertyKind.RowKey) {
if (ef.type == typeof(string)) // partition & row keys must always be strings
return entity.GetString(ef.kind.ToString()); var stringValue = entity.GetString(ef.kind.ToString());
else if (ef.type == typeof(Guid)) if (ef.type == typeof(string)) {
return Guid.Parse(entity.GetString(ef.kind.ToString())); return stringValue;
else if (ef.type == typeof(int)) } else if (ef.type == typeof(Guid)) {
return int.Parse(entity.GetString(ef.kind.ToString())); return Guid.Parse(stringValue);
else if (ef.type == typeof(long)) } else if (ef.type == typeof(int)) {
return long.Parse(entity.GetString(ef.kind.ToString())); return int.Parse(stringValue);
else if (ef.type.IsClass) } else if (ef.type == typeof(long)) {
return ef.type.GetConstructor(new[] { typeof(string) })!.Invoke(new[] { entity.GetString(ef.kind.ToString()) }); return long.Parse(stringValue);
else { } else if (ef.type.IsClass) {
if (ef.type.IsAssignableTo(typeof(ValidatedString))) {
return ef.type.GetMethod("Parse")!.Invoke(null, new[] { stringValue });
}
return Activator.CreateInstance(ef.type, new[] { stringValue });
} else {
throw new Exception($"invalid partition or row key type of {info.type} property {name}: {ef.type}"); throw new Exception($"invalid partition or row key type of {info.type} property {name}: {ef.type}");
} }
} }

View File

@ -48,11 +48,11 @@ public abstract class ContainersTestBase : FunctionTestBase {
[Fact] [Fact]
public async Async.Task CanDelete() { public async Async.Task CanDelete() {
var containerName = "test"; var containerName = Container.Parse("test");
var client = GetContainerClient(containerName); var client = GetContainerClient(containerName);
await client.CreateIfNotExistsAsync(); await client.CreateIfNotExistsAsync();
var msg = TestHttpRequestData.FromJson("DELETE", new ContainerDelete(new Container(containerName))); var msg = TestHttpRequestData.FromJson("DELETE", new ContainerDelete(containerName));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context); var func = new ContainersFunction(Logger, auth, Context);
@ -67,8 +67,8 @@ public abstract class ContainersTestBase : FunctionTestBase {
[Fact] [Fact]
public async Async.Task CanPost_New() { public async Async.Task CanPost_New() {
var meta = new Dictionary<string, string> { { "some", "value" } }; var meta = new Dictionary<string, string> { { "some", "value" } };
var containerName = "test"; var containerName = Container.Parse("test");
var msg = TestHttpRequestData.FromJson("POST", new ContainerCreate(new Container(containerName), meta)); var msg = TestHttpRequestData.FromJson("POST", new ContainerCreate(containerName, meta));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context); var func = new ContainersFunction(Logger, auth, Context);
@ -87,12 +87,12 @@ public abstract class ContainersTestBase : FunctionTestBase {
[Fact] [Fact]
public async Async.Task CanPost_Existing() { public async Async.Task CanPost_Existing() {
var containerName = "test"; var containerName = Container.Parse("test");
var client = GetContainerClient(containerName); var client = GetContainerClient(containerName);
await client.CreateIfNotExistsAsync(); await client.CreateIfNotExistsAsync();
var metadata = new Dictionary<string, string> { { "some", "value" } }; var metadata = new Dictionary<string, string> { { "some", "value" } };
var msg = TestHttpRequestData.FromJson("POST", new ContainerCreate(new Container(containerName), metadata)); var msg = TestHttpRequestData.FromJson("POST", new ContainerCreate(containerName, metadata));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context); var func = new ContainersFunction(Logger, auth, Context);
@ -110,13 +110,13 @@ public abstract class ContainersTestBase : FunctionTestBase {
[Fact] [Fact]
public async Async.Task Get_Existing() { public async Async.Task Get_Existing() {
var containerName = "test"; var containerName = Container.Parse("test");
{ {
var client = GetContainerClient(containerName); var client = GetContainerClient(containerName);
await client.CreateIfNotExistsAsync(); await client.CreateIfNotExistsAsync();
} }
var msg = TestHttpRequestData.FromJson("GET", new ContainerGet(new Container(containerName))); var msg = TestHttpRequestData.FromJson("GET", new ContainerGet(containerName));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context); var func = new ContainersFunction(Logger, auth, Context);
@ -130,7 +130,8 @@ public abstract class ContainersTestBase : FunctionTestBase {
[Fact] [Fact]
public async Async.Task Get_Missing_Fails() { public async Async.Task Get_Missing_Fails() {
var msg = TestHttpRequestData.FromJson("GET", new ContainerGet(new Container("container"))); var container = Container.Parse("container");
var msg = TestHttpRequestData.FromJson("GET", new ContainerGet(container));
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ContainersFunction(Logger, auth, Context); var func = new ContainersFunction(Logger, auth, Context);
@ -142,8 +143,8 @@ public abstract class ContainersTestBase : FunctionTestBase {
public async Async.Task List_Existing() { public async Async.Task List_Existing() {
var meta1 = new Dictionary<string, string> { { "key1", "value1" } }; var meta1 = new Dictionary<string, string> { { "key1", "value1" } };
var meta2 = new Dictionary<string, string> { { "key2", "value2" } }; var meta2 = new Dictionary<string, string> { { "key2", "value2" } };
await GetContainerClient("one").CreateIfNotExistsAsync(metadata: meta1); await GetContainerClient(Container.Parse("one")).CreateIfNotExistsAsync(metadata: meta1);
await GetContainerClient("two").CreateIfNotExistsAsync(metadata: meta2); await GetContainerClient(Container.Parse("two")).CreateIfNotExistsAsync(metadata: meta2);
var msg = TestHttpRequestData.Empty("GET"); // this means list all var msg = TestHttpRequestData.Empty("GET"); // this means list all
@ -154,7 +155,7 @@ public abstract class ContainersTestBase : FunctionTestBase {
var list = BodyAs<ContainerInfoBase[]>(result); var list = BodyAs<ContainerInfoBase[]>(result);
// other tests can run in parallel, so filter to just our containers: // other tests can run in parallel, so filter to just our containers:
var cs = list.Where(ci => ci.Name.ContainerName.StartsWith(Context.ServiceConfiguration.OneFuzzStoragePrefix)).ToList(); var cs = list.Where(ci => ci.Name.String.StartsWith(Context.ServiceConfiguration.OneFuzzStoragePrefix)).ToList();
Assert.Equal(2, cs.Count); Assert.Equal(2, cs.Count);
// ensure correct metadata was returned. // ensure correct metadata was returned.

View File

@ -72,7 +72,8 @@ public abstract class DownloadTestBase : FunctionTestBase {
[Fact] [Fact]
public async Async.Task Download_RedirectsToResult_WithLocationHeader() { public async Async.Task Download_RedirectsToResult_WithLocationHeader() {
// set up a file to download // set up a file to download
var container = GetContainerClient("xxx"); var containerName = Container.Parse("xxx");
var container = GetContainerClient(containerName);
await container.CreateAsync(); await container.CreateAsync();
await container.UploadBlobAsync("yyy", new BinaryData("content")); await container.UploadBlobAsync("yyy", new BinaryData("content"));

View File

@ -15,9 +15,9 @@ class TestCreds : ICreds {
private readonly Guid _subscriptionId; private readonly Guid _subscriptionId;
private readonly string _resourceGroup; private readonly string _resourceGroup;
private readonly string _region; private readonly Region _region;
public TestCreds(Guid subscriptionId, string resourceGroup, string region) { public TestCreds(Guid subscriptionId, string resourceGroup, Region region) {
_subscriptionId = subscriptionId; _subscriptionId = subscriptionId;
_resourceGroup = resourceGroup; _resourceGroup = resourceGroup;
_region = region; _region = region;
@ -26,8 +26,8 @@ class TestCreds : ICreds {
public ArmClient ArmClient => null!; public ArmClient ArmClient => null!;
// we have to return something in some test cases, even if it isnt used // we have to return something in some test cases, even if it isnt used
public Task<string> GetBaseRegion() => Task.FromResult(_region); public Task<Region> GetBaseRegion() => Task.FromResult(_region);
public Task<IReadOnlyList<string>> GetRegions() => Task.FromResult<IReadOnlyList<string>>(new[] { _region }); public Task<IReadOnlyList<Region>> GetRegions() => Task.FromResult<IReadOnlyList<Region>>(new[] { _region });
public string GetBaseResourceGroup() => _resourceGroup; public string GetBaseResourceGroup() => _resourceGroup;

View File

@ -48,7 +48,8 @@ public abstract class InfoTestBase : FunctionTestBase {
// store the instance ID in the expected location: // store the instance ID in the expected location:
// for production this is done by the deploy script // for production this is done by the deploy script
var instanceId = Guid.NewGuid().ToString(); var instanceId = Guid.NewGuid().ToString();
var containerClient = GetContainerClient("base-config"); var baseConfigContainer = WellKnownContainers.BaseConfig;
var containerClient = GetContainerClient(baseConfigContainer);
await containerClient.CreateAsync(); await containerClient.CreateAsync();
await containerClient.GetBlobClient("instance_id").UploadAsync(new BinaryData(instanceId)); await containerClient.GetBlobClient("instance_id").UploadAsync(new BinaryData(instanceId));

View File

@ -169,7 +169,7 @@ public abstract class JobsTestBase : FunctionTestBase {
Assert.NotNull(job.Config.Logs); Assert.NotNull(job.Config.Logs);
Assert.Empty(new Uri(job.Config.Logs!).Query); Assert.Empty(new Uri(job.Config.Logs!).Query);
var container = Assert.Single(await Context.Containers.GetContainers(StorageType.Corpus), c => c.Key.Contains(job.JobId.ToString())); var container = Assert.Single(await Context.Containers.GetContainers(StorageType.Corpus), c => c.Key.String.Contains(job.JobId.ToString()));
var metadata = Assert.Single(container.Value); var metadata = Assert.Single(container.Value);
Assert.Equal(new KeyValuePair<string, string>("container_type", "logs"), metadata); Assert.Equal(new KeyValuePair<string, string>("container_type", "logs"), metadata);
} }

View File

@ -35,15 +35,15 @@ public abstract class FunctionTestBase : IAsyncLifetime {
private readonly Guid _subscriptionId = Guid.NewGuid(); private readonly Guid _subscriptionId = Guid.NewGuid();
private readonly string _resourceGroup = "FakeResourceGroup"; private readonly string _resourceGroup = "FakeResourceGroup";
private readonly string _region = "fakeregion"; private readonly Region _region = Region.Parse("fakeregion");
protected ILogTracer Logger { get; } protected ILogTracer Logger { get; }
protected TestContext Context { get; } protected TestContext Context { get; }
private readonly BlobServiceClient _blobClient; private readonly BlobServiceClient _blobClient;
protected BlobContainerClient GetContainerClient(string container) protected BlobContainerClient GetContainerClient(Container container)
=> _blobClient.GetBlobContainerClient(_storagePrefix + container); => _blobClient.GetBlobContainerClient(_storagePrefix + container.String);
public FunctionTestBase(ITestOutputHelper output, IStorage storage) { public FunctionTestBase(ITestOutputHelper output, IStorage storage) {
Logger = new TestLogTracer(output); Logger = new TestLogTracer(output);

View File

@ -91,6 +91,11 @@ namespace Tests {
where PoolName.TryParse(name.Get, out _) where PoolName.TryParse(name.Get, out _)
select PoolName.Parse(name.Get); select PoolName.Parse(name.Get);
public static Gen<Region> RegionGen { get; }
= from name in Arb.Generate<NonEmptyString>()
where Region.TryParse(name.Get, out _)
select Region.Parse(name.Get);
public static Gen<Node> Node { get; } public static Gen<Node> Node { get; }
= from arg in Arb.Generate<Tuple<Tuple<DateTimeOffset?, Guid?, Guid, NodeState>, Tuple<Guid?, DateTimeOffset, string, bool, bool, bool>>>() = from arg in Arb.Generate<Tuple<Tuple<DateTimeOffset?, Guid?, Guid, NodeState>, Tuple<Guid?, DateTimeOffset, string, bool, bool, bool>>>()
from poolName in PoolNameGen from poolName in PoolNameGen
@ -107,39 +112,47 @@ namespace Tests {
DeleteRequested: arg.Item2.Item5, DeleteRequested: arg.Item2.Item5,
DebugKeepNode: arg.Item2.Item6); DebugKeepNode: arg.Item2.Item6);
public static Gen<ProxyForward> ProxyForward() { public static Gen<ProxyForward> ProxyForward { get; } =
return Arb.Generate<Tuple<Tuple<string, long, Guid, Guid, Guid?, long>, Tuple<IPv4Address, DateTimeOffset>>>().Select( from region in RegionGen
arg => from port in Gen.Choose(0, ushort.MaxValue)
new ProxyForward( from scalesetId in Arb.Generate<Guid>()
Region: arg.Item1.Item1, from machineId in Arb.Generate<Guid>()
Port: arg.Item1.Item2, from proxyId in Arb.Generate<Guid?>()
ScalesetId: arg.Item1.Item3, from dstPort in Gen.Choose(0, ushort.MaxValue)
MachineId: arg.Item1.Item4, from dstIp in Arb.Generate<IPv4Address>()
ProxyId: arg.Item1.Item5, from endTime in Arb.Generate<DateTimeOffset>()
DstPort: arg.Item1.Item6, select new ProxyForward(
DstIp: arg.Item2.Item1.ToString(), Region: region,
EndTime: arg.Item2.Item2 Port: port,
) ScalesetId: scalesetId,
); MachineId: machineId,
} ProxyId: proxyId,
DstPort: dstPort,
DstIp: dstIp.ToString(),
EndTime: endTime);
public static Gen<Proxy> Proxy() { public static Gen<Proxy> Proxy { get; } =
return Arb.Generate<Tuple<Tuple<string, Guid, DateTimeOffset?, VmState, Authentication, string?, Error?>, Tuple<string, ProxyHeartbeat?, bool>>>().Select( from region in RegionGen
arg => from proxyId in Arb.Generate<Guid>()
new Proxy( from createdTimestamp in Arb.Generate<DateTimeOffset?>()
Region: arg.Item1.Item1, from state in Arb.Generate<VmState>()
ProxyId: arg.Item1.Item2, from auth in Arb.Generate<Authentication>()
CreatedTimestamp: arg.Item1.Item3, from ip in Arb.Generate<string>()
State: arg.Item1.Item4, from error in Arb.Generate<Error?>()
Auth: arg.Item1.Item5, from version in Arb.Generate<string>()
Ip: arg.Item1.Item6, from heartbeat in Arb.Generate<ProxyHeartbeat?>()
Error: arg.Item1.Item7, from outdated in Arb.Generate<bool>()
Version: arg.Item2.Item1, select new Proxy(
Heartbeat: arg.Item2.Item2, Region: region,
Outdated: arg.Item2.Item3 ProxyId: proxyId,
) CreatedTimestamp: createdTimestamp,
); State: state,
} Auth: auth,
Ip: ip,
Error: error,
Version: version,
Heartbeat: heartbeat,
Outdated: outdated);
public static Gen<EventMessage> EventMessage() { public static Gen<EventMessage> EventMessage() {
return Arb.Generate<Tuple<Guid, BaseEvent, Guid, string>>().Select( return Arb.Generate<Tuple<Guid, BaseEvent, Guid, string>>().Select(
@ -219,10 +232,11 @@ namespace Tests {
} }
public static Gen<Scaleset> Scaleset { get; } public static Gen<Scaleset> Scaleset { get; }
= from arg in Arb.Generate<Tuple< = from arg in Arb.Generate<Tuple<
Tuple<Guid, ScalesetState, Authentication?, string, string, string>, Tuple<Guid, ScalesetState, Authentication?, string, string>,
Tuple<int, bool, bool, bool, Error?, Guid?>, Tuple<int, bool, bool, bool, Error?, Guid?>,
Tuple<Guid?, Dictionary<string, string>>>>() Tuple<Guid?, Dictionary<string, string>>>>()
from poolName in PoolNameGen from poolName in PoolNameGen
from region in RegionGen
select new Scaleset( select new Scaleset(
PoolName: poolName, PoolName: poolName,
ScalesetId: arg.Item1.Item1, ScalesetId: arg.Item1.Item1,
@ -230,7 +244,7 @@ namespace Tests {
Auth: arg.Item1.Item3, Auth: arg.Item1.Item3,
VmSku: arg.Item1.Item4, VmSku: arg.Item1.Item4,
Image: arg.Item1.Item5, Image: arg.Item1.Item5,
Region: arg.Item1.Item6, Region: region,
Size: arg.Item2.Item1, Size: arg.Item2.Item1,
SpotInstances: arg.Item2.Item2, SpotInstances: arg.Item2.Item2,
@ -346,11 +360,12 @@ namespace Tests {
); );
} }
public static Gen<Container> Container() { public static Gen<Container> ContainerGen { get; } =
return Arb.Generate<Tuple<NonNull<string>>>().Select( from len in Gen.Choose(3, 63)
arg => new Container(string.Join("", arg.Item1.Get.Where(c => char.IsLetterOrDigit(c) || c == '-'))!) from name in Gen.ArrayOf(len, Gen.Elements<char>("abcdefghijklmnopqrstuvwxyz0123456789-"))
); let nameString = new string(name)
} where Container.TryParse(nameString, out var _)
select Container.Parse(nameString);
public static Gen<NotificationTemplate> NotificationTemplate() { public static Gen<NotificationTemplate> NotificationTemplate() {
return Gen.OneOf(new[] { return Gen.OneOf(new[] {
@ -411,11 +426,11 @@ namespace Tests {
} }
public static Arbitrary<ProxyForward> ProxyForward() { public static Arbitrary<ProxyForward> ProxyForward() {
return Arb.From(OrmGenerators.ProxyForward()); return Arb.From(OrmGenerators.ProxyForward);
} }
public static Arbitrary<Proxy> Proxy() { public static Arbitrary<Proxy> Proxy() {
return Arb.From(OrmGenerators.Proxy()); return Arb.From(OrmGenerators.Proxy);
} }
public static Arbitrary<EventMessage> EventMessage() { public static Arbitrary<EventMessage> EventMessage() {
@ -458,9 +473,12 @@ namespace Tests {
} }
public static Arbitrary<Container> Container() { public static Arbitrary<Container> Container() {
return Arb.From(OrmGenerators.Container()); return Arb.From(OrmGenerators.ContainerGen);
} }
public static Arbitrary<Region> Region() {
return Arb.From(OrmGenerators.RegionGen);
}
public static Arbitrary<NotificationTemplate> NotificationTemplate() { public static Arbitrary<NotificationTemplate> NotificationTemplate() {
return Arb.From(OrmGenerators.NotificationTemplate()); return Arb.From(OrmGenerators.NotificationTemplate());

View File

@ -265,15 +265,15 @@ namespace Tests {
[Fact] [Fact]
public void TestContainerSerialization() { public void TestContainerSerialization() {
var container = new Container("abc-123"); var container = Container.Parse("abc-123");
var expected = new Entity3(123, "abc", container); var expected = new Entity3(123, "abc", container);
var converter = new EntityConverter(); var converter = new EntityConverter();
var tableEntity = converter.ToTableEntity(expected); var tableEntity = converter.ToTableEntity(expected);
var actual = converter.ToRecord<Entity3>(tableEntity); var actual = converter.ToRecord<Entity3>(tableEntity);
Assert.Equal(expected.Container.ContainerName, actual.Container.ContainerName); Assert.Equal(expected.Container, actual.Container);
Assert.Equal(expected.Container.ContainerName, tableEntity.GetString("container")); Assert.Equal(expected.Container.String, tableEntity.GetString("container"));
} }
[Fact] [Fact]
@ -288,7 +288,7 @@ namespace Tests {
Assert.Equal(123, entity?.Id); Assert.Equal(123, entity?.Id);
Assert.Equal("abc", entity?.TheName); Assert.Equal("abc", entity?.TheName);
Assert.Equal("abc-123", entity?.Container.ContainerName); Assert.Equal("abc-123", entity?.Container.String);
} }
@ -300,7 +300,7 @@ namespace Tests {
[Fact] [Fact]
public void TestPartitionKeyIsRowKey() { public void TestPartitionKeyIsRowKey() {
var container = new Container("abc-123"); var container = Container.Parse("abc-123");
var expected = new Entity4(123, "abc", container); var expected = new Entity4(123, "abc", container);
var converter = new EntityConverter(); var converter = new EntityConverter();
@ -310,8 +310,8 @@ namespace Tests {
var actual = converter.ToRecord<Entity4>(tableEntity); var actual = converter.ToRecord<Entity4>(tableEntity);
Assert.Equal(expected.Container.ContainerName, actual.Container.ContainerName); Assert.Equal(expected.Container, actual.Container);
Assert.Equal(expected.Container.ContainerName, tableEntity.GetString("container")); Assert.Equal(expected.Container.String, tableEntity.GetString("container"));
} }

View File

@ -25,7 +25,9 @@ public class SchedulerTests {
TargetEnv: new Dictionary<string, string>(), TargetEnv: new Dictionary<string, string>(),
TargetOptions: new List<string>()), TargetOptions: new List<string>()),
Pool: new TaskPool(1, PoolName.Parse("pool")), Pool: new TaskPool(1, PoolName.Parse("pool")),
Containers: new List<TaskContainers> { new TaskContainers(ContainerType.Setup, new Container("setup")) }, Containers: new List<TaskContainers> {
new TaskContainers(ContainerType.Setup, Container.Parse("setup"))
},
Colocate: true Colocate: true
), ),
@ -105,7 +107,7 @@ public class SchedulerTests {
var tasks = BuildTasks(100).Select((task, i) => { var tasks = BuildTasks(100).Select((task, i) => {
var containers = new List<TaskContainers>(task.Config.Containers!); var containers = new List<TaskContainers>(task.Config.Containers!);
if (i % 4 == 0) { if (i % 4 == 0) {
containers[0] = containers[0] with { Name = new Container("setup2") }; containers[0] = containers[0] with { Name = Container.Parse("setup2") };
} }
return task with { return task with {
JobId = i % 2 == 0 ? jobId : task.JobId, JobId = i % 2 == 0 ? jobId : task.JobId,

View File

@ -101,10 +101,9 @@ public class TimerReproTests {
Guid.NewGuid(), Guid.NewGuid(),
Guid.Empty, Guid.Empty,
new ReproConfig( new ReproConfig(
new Container(String.Empty), Container.Parse("container"),
String.Empty, "",
0 0),
),
null, null,
Os.Linux, Os.Linux,
VmState.Init, VmState.Init,

View File

@ -19,4 +19,18 @@ public class ValidatedStringTests {
var result = JsonSerializer.Serialize(new ThingContainingPoolName(PoolName.Parse("is-a-pool"))); var result = JsonSerializer.Serialize(new ThingContainingPoolName(PoolName.Parse("is-a-pool")));
Assert.Equal("{\"PoolName\":\"is-a-pool\"}", result); Assert.Equal("{\"PoolName\":\"is-a-pool\"}", result);
} }
[Theory]
[InlineData("x", false)] // too short
[InlineData("xy", false)] // too short
[InlineData("xyz", true)]
[InlineData("-container", false)] // can't start with hyphen
[InlineData("container-", true)] // can end with hyphen
[InlineData("container-name", true)] // can have middle hyphen
[InlineData("container--name", false)] // can't have two consecutive hyphens
[InlineData("container-Name", false)] // can't have capitals
[InlineData("container-name-09", true)] // can have numbers
public void ContainerNames(string name, bool valid) {
Assert.Equal(valid, Container.TryParse(name, out var _));
}
} }