Skip to content

Commit

Permalink
Implement custom deserialization and validation logic (dotnet#294)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkotalik authored Apr 4, 2020
1 parent b92ea38 commit feb85ce
Show file tree
Hide file tree
Showing 20 changed files with 1,680 additions and 118 deletions.
5 changes: 5 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<Project>
<Import Project="Sdk.targets" Sdk="Microsoft.DotNet.Arcade.Sdk" />

<PropertyGroup Label="Resx settings">
<GenerateResxSource Condition="$(GenerateResxSource) == ''">true</GenerateResxSource>
<GenerateResxSourceEmitFormatMethods Condition="$(GenerateResxSourceEmitFormatMethods) == ''">true</GenerateResxSourceEmitFormatMethods>
</PropertyGroup>

<Target Name="AddInternalsVisibleTo" BeforeTargets="CoreCompile">
<ItemGroup Condition="'@(InternalsVisibleTo->Count())' &gt; 0">
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
Expand Down
2 changes: 0 additions & 2 deletions samples/nginx-ingress/tye.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ services:
target: /etc/nginx/conf.d/default.conf
- name: appA
project: ApplicationA/ApplicationA.csproj
bindings:
replicas: 2
- name: appB
project: ApplicationB/ApplicationB.csproj
bindings:
replicas: 2
92 changes: 6 additions & 86 deletions src/Microsoft.Tye.Core/ApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
}

var config = ConfigFactory.FromFile(source);
ValidateConfigApplication(config);

config.Validate();
var builder = new ApplicationBuilder(source, config.Name ?? source.Directory.Name.ToLowerInvariant());
if (!string.IsNullOrEmpty(config.Registry))
{
Expand Down Expand Up @@ -55,7 +54,7 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
{
var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project);
var projectFile = new FileInfo(Path.Combine(builder.Source.DirectoryName, expandedProject));
var project = new ProjectServiceBuilder(configService.Name, projectFile);
var project = new ProjectServiceBuilder(configService.Name!, projectFile);
service = project;

project.Build = configService.Build ?? true;
Expand All @@ -76,7 +75,7 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
}
else if (!string.IsNullOrEmpty(configService.Image))
{
var container = new ContainerServiceBuilder(configService.Name, configService.Image)
var container = new ContainerServiceBuilder(configService.Name!, configService.Image)
{
Args = configService.Args,
Replicas = configService.Replicas ?? 1
Expand All @@ -95,7 +94,7 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
workingDirectory = Path.GetDirectoryName(expandedExecutable)!;
}

var executable = new ExecutableServiceBuilder(configService.Name, expandedExecutable)
var executable = new ExecutableServiceBuilder(configService.Name!, expandedExecutable)
{
Args = configService.Args,
WorkingDirectory = configService.WorkingDirectory != null ?
Expand All @@ -107,7 +106,7 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
}
else if (configService.External)
{
var external = new ExternalServiceBuilder(configService.Name);
var external = new ExternalServiceBuilder(configService.Name!);
service = external;
}
else
Expand Down Expand Up @@ -214,7 +213,7 @@ service is ProjectServiceBuilder project2 &&

foreach (var configIngress in config.Ingress)
{
var ingress = new IngressBuilder(configIngress.Name);
var ingress = new IngressBuilder(configIngress.Name!);
ingress.Replicas = configIngress.Replicas ?? 1;

builder.Ingress.Add(ingress);
Expand Down Expand Up @@ -244,84 +243,5 @@ service is ProjectServiceBuilder project2 &&

return builder;
}

private static void ValidateConfigApplication(ConfigApplication config)
{
var context = new ValidationContext(config);
var results = new List<ValidationResult>();
if (!Validator.TryValidateObject(config, context, results, validateAllProperties: true))
{
throw new CommandException(
"Configuration validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}

foreach (var extension in config.Extensions)
{
if (!extension.TryGetValue("name", out var name) || string.IsNullOrWhiteSpace(name as string))
{
throw new CommandException(
"Configuration validation failed." + Environment.NewLine +
"Extensions must provide a name.");
}
}

foreach (var service in config.Services)
{
context = new ValidationContext(service);
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}

foreach (var binding in service.Bindings)
{
context = new ValidationContext(binding);
if (!Validator.TryValidateObject(binding, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Binding '{binding.Name}' of service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}

foreach (var envVar in service.Configuration)
{
context = new ValidationContext(service);
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Environment variable '{envVar.Name}' of service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}

foreach (var volume in service.Volumes)
{
context = new ValidationContext(service);
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Volume '{volume.Source}' of service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}
}

foreach (var ingress in config.Ingress)
{
// We don't currently recurse into ingress rules or ingress bindings right now.
// There's nothing to validate there.
context = new ValidationContext(ingress);
if (!Validator.TryValidateObject(ingress, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Ingress '{ingress.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}
}
}
}
137 changes: 137 additions & 0 deletions src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using Tye;
using Tye.Serialization;
using YamlDotNet.Serialization;

namespace Microsoft.Tye.ConfigModel
Expand All @@ -28,5 +33,137 @@ public class ConfigApplication
public List<ConfigService> Services { get; set; } = new List<ConfigService>();

public List<ConfigIngress> Ingress { get; set; } = new List<ConfigIngress>();

public void Validate()
{
var config = this;

var context = new ValidationContext(config);
var results = new List<ValidationResult>();

if (!Validator.TryValidateObject(config, context, results, validateAllProperties: true))
{
throw new TyeYamlException(
"Configuration validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}

foreach (var extension in config.Extensions)
{
if (!extension.TryGetValue("name", out var name) || string.IsNullOrWhiteSpace(name as string))
{
throw new TyeYamlException(CoreStrings.ExtensionMustProvideAName);
}
}

foreach (var service in config.Services)
{
context = new ValidationContext(service);
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true))
{
throw new TyeYamlException(
$"Service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}

if (config.Services.Where(o => o.Name == service.Name).Count() > 1)
{
throw new TyeYamlException(CoreStrings.ServiceMustHaveUniqueNames);
}

foreach (var binding in service.Bindings)
{
context = new ValidationContext(binding);
if (!Validator.TryValidateObject(binding, context, results, validateAllProperties: true))
{
throw new TyeYamlException(
$"Binding '{binding.Name}' of service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}

if (string.IsNullOrEmpty(binding.Name) && service.Bindings.Count > 1)
{
throw new TyeYamlException(CoreStrings.MultipleServiceBindingsWithoutName);
}
if (service.Bindings.Where(o => o.Name == binding.Name).Count() > 1)
{
throw new TyeYamlException(CoreStrings.MultipleServiceBindingsWithSameName);
}
}

foreach (var envVar in service.Configuration)
{
context = new ValidationContext(service);
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true))
{
throw new TyeYamlException(
$"Environment variable '{envVar.Name}' of service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}

foreach (var volume in service.Volumes)
{
context = new ValidationContext(service);
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true))
{
throw new TyeYamlException(
$"Volume '{volume.Source}' of service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}
}

foreach (var ingress in config.Ingress)
{
context = new ValidationContext(ingress);
if (!Validator.TryValidateObject(ingress, context, results, validateAllProperties: true))
{
throw new TyeYamlException(
$"Ingress '{ingress.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}

foreach (var binding in ingress.Bindings)
{
context = new ValidationContext(binding);
if (!Validator.TryValidateObject(binding, context, results, validateAllProperties: true))
{
throw new TyeYamlException(
$"Binding '{binding.Name}' of ingress '{ingress.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
if (string.IsNullOrEmpty(binding.Name) && ingress.Bindings.Count > 1)
{
throw new TyeYamlException(CoreStrings.MultipleIngressBindingWithoutName);
}
if (ingress.Bindings.Where(o => o.Name == binding.Name).Count() > 1)
{
throw new TyeYamlException(CoreStrings.MultipleIngressBindingWithSameName);
}
if (binding.Protocol != "http" && binding.Protocol != "https" && binding.Protocol != null)
{
throw new TyeYamlException(CoreStrings.IngressBindingMustBeHttpOrHttps);
}
}

// Make sure all ingress rules have an associated service
foreach (var rule in ingress.Rules)
{
context = new ValidationContext(rule);
if (!Validator.TryValidateObject(rule, context, results, validateAllProperties: true))
{
throw new TyeYamlException(
$"Rule '{rule.Path}' of ingress '{ingress.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}

if (config.Services.Where(o => o.Name == rule.Service).Count() != 1)
{
throw new TyeYamlException(CoreStrings.IngressRuleMustReferenceService);
}
}
}
}
}
}
25 changes: 3 additions & 22 deletions src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.Tye.Serialization;
using Tye.Serialization;

namespace Microsoft.Tye.ConfigModel
{
Expand Down Expand Up @@ -74,28 +75,8 @@ private static ConfigApplication FromSolution(FileInfo file)

private static ConfigApplication FromYaml(FileInfo file)
{
var deserializer = YamlSerializer.CreateDeserializer();

using var reader = file.OpenText();
var application = deserializer.Deserialize<ConfigApplication>(reader);
application.Source = file;

// Deserialization makes all collection properties null so make sure they are non-null so
// other code doesn't need to react
foreach (var service in application.Services)
{
service.Bindings ??= new List<ConfigServiceBinding>();
service.Configuration ??= new List<ConfigConfigurationSource>();
service.Volumes ??= new List<ConfigVolume>();
}

foreach (var ingress in application.Ingress)
{
ingress.Bindings ??= new List<ConfigIngressBinding>();
ingress.Rules ??= new List<ConfigIngressRule>();
}

return application;
using var parser = new YamlParser(file);
return parser.ParseConfigApplication();
}
}
}
Loading

0 comments on commit feb85ce

Please sign in to comment.