Implement custom deserialization and validation logic (#294)

This commit is contained in:
Justin Kotalik 2020-04-03 18:45:04 -07:00 коммит произвёл GitHub
Родитель b92ea3870c
Коммит feb85cef84
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
20 изменённых файлов: 1680 добавлений и 118 удалений

Просмотреть файл

@ -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">

Просмотреть файл

@ -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

Просмотреть файл

@ -22,8 +22,7 @@ namespace Microsoft.Tye
}
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))
{
@ -55,7 +54,7 @@ namespace Microsoft.Tye
{
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;
@ -76,7 +75,7 @@ namespace Microsoft.Tye
}
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
@ -95,7 +94,7 @@ namespace Microsoft.Tye
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 ?
@ -107,7 +106,7 @@ namespace Microsoft.Tye
}
else if (configService.External)
{
var external = new ExternalServiceBuilder(configService.Name);
var external = new ExternalServiceBuilder(configService.Name!);
service = external;
}
else
@ -214,7 +213,7 @@ namespace Microsoft.Tye
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);
@ -244,84 +243,5 @@ namespace Microsoft.Tye
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)));
}
}
}
}
}

Просмотреть файл

@ -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
@ -28,5 +33,137 @@ namespace Microsoft.Tye.ConfigModel
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);
}
}
}
}
}
}

Просмотреть файл

@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.Tye.Serialization;
using Tye.Serialization;
namespace Microsoft.Tye.ConfigModel
{
@ -74,28 +75,8 @@ namespace Microsoft.Tye.ConfigModel
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();
}
}
}

Просмотреть файл

@ -0,0 +1,165 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ExpectedYamlScalar" xml:space="preserve">
<value>Excpeted scalar value for key: "{key}".</value>
</data>
<data name="ExpectedYamlSequence" xml:space="preserve">
<value>Excpeted yaml sequence for key: "{key}".</value>
</data>
<data name="ExtensionMustProvideAName" xml:space="preserve">
<value>Configuration validation failed. Extensions must provide a name.</value>
</data>
<data name="IngressBindingMustBeHttpOrHttps" xml:space="preserve">
<value>Ingress bindings must be http or https.</value>
</data>
<data name="IngressRuleMustReferenceService" xml:space="preserve">
<value>Ingress rules references a service that does not exist.</value>
</data>
<data name="MultipleIngressBindingWithoutName" xml:space="preserve">
<value>Cannot have multiple ingress bindings without names. Please specify names for each ingress binding.</value>
</data>
<data name="MultipleIngressBindingWithSameName" xml:space="preserve">
<value>Cannot have multiple ingress bindings with the same name.</value>
</data>
<data name="MultipleServiceBindingsWithoutName" xml:space="preserve">
<value>Cannot have multiple service bindings without names. Please specify names for each service binding.</value>
</data>
<data name="MultipleServiceBindingsWithSameName" xml:space="preserve">
<value>Cannot have multiple service bindings with the same name.</value>
</data>
<data name="MustBeABoolean" xml:space="preserve">
<value>"{value}" must be a boolean value (true/false).</value>
</data>
<data name="MustBeAnInteger" xml:space="preserve">
<value>"{value}" value must be an integer.</value>
</data>
<data name="MustBePositive" xml:space="preserve">
<value>"{value}" value cannot be negative.</value>
</data>
<data name="ServiceMustHaveUniqueNames" xml:space="preserve">
<value>Services must have unique names.</value>
</data>
<data name="UnexpectedType" xml:space="preserve">
<value>Unexpected node type in tye.yaml. Expected "{expected}" but got "{actual}".</value>
</data>
<data name="UnrecognizedKey" xml:space="preserve">
<value>Unexpected key "{key}" in tye.yaml.</value>
</data>
</root>

Просмотреть файл

@ -29,6 +29,11 @@
<Content Include="Templates\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="CoreStrings.resx" EmitFormatMethods="true">
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Include="..\shared\KubectlDetector.cs" Link="KubectlDetector.cs" />
<Compile Include="..\shared\TempDirectory.cs" Link="TempDirectory.cs" />

Просмотреть файл

@ -0,0 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.Tye.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]

Просмотреть файл

@ -0,0 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Tye.ConfigModel;
using YamlDotNet.RepresentationModel;
namespace Tye.Serialization
{
public static class ConfigApplicationParser
{
public static void HandleConfigApplication(YamlMappingNode yamlMappingNode, ConfigApplication app)
{
foreach (var child in yamlMappingNode.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
switch (key)
{
case "name":
app.Name = YamlParser.GetScalarValue(key, child.Value);
break;
case "registry":
app.Registry = YamlParser.GetScalarValue(key, child.Value);
break;
case "ingress":
YamlParser.ThrowIfNotYamlSequence(key, child.Value);
ConfigIngressParser.HandleIngress((child.Value as YamlSequenceNode)!, app.Ingress);
break;
case "services":
YamlParser.ThrowIfNotYamlSequence(key, child.Value);
ConfigServiceParser.HandleServiceMapping((child.Value as YamlSequenceNode)!, app.Services);
break;
case "extensions":
YamlParser.ThrowIfNotYamlSequence(key, child.Value);
ConfigExtensionsParser.HandleExtensionsMapping((child.Value as YamlSequenceNode)!, app.Extensions);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.Collections.Generic;
using YamlDotNet.RepresentationModel;
namespace Tye.Serialization
{
public static class ConfigExtensionsParser
{
public static void HandleExtensionsMapping(YamlSequenceNode yamlSequenceNode, List<Dictionary<string, object>> extensions)
{
foreach (var child in yamlSequenceNode.Children)
{
switch (child.NodeType)
{
case YamlNodeType.Mapping:
var extensionDictionary = new Dictionary<string, object>();
foreach (var mapping in (YamlMappingNode)child)
{
var key = YamlParser.GetScalarValue(mapping.Key);
extensionDictionary[key] = YamlParser.GetScalarValue(key, mapping.Value)!;
}
extensions.Add(extensionDictionary);
break;
default:
throw new TyeYamlException(child.Start,
CoreStrings.FormatUnexpectedType(YamlNodeType.Mapping.ToString(), child.NodeType.ToString()));
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.Collections.Generic;
using Microsoft.Tye.ConfigModel;
using YamlDotNet.RepresentationModel;
namespace Tye.Serialization
{
public static class ConfigIngressParser
{
public static void HandleIngress(YamlSequenceNode yamlSequenceNode, List<ConfigIngress> ingress)
{
foreach (var child in yamlSequenceNode.Children)
{
YamlParser.ThrowIfNotYamlMapping(child);
var configIngress = new ConfigIngress();
HandleIngressMapping((YamlMappingNode)child, configIngress);
ingress.Add(configIngress);
}
}
private static void HandleIngressMapping(YamlMappingNode yamlMappingNode, ConfigIngress configIngress)
{
foreach (var child in yamlMappingNode!.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
switch (key)
{
case "name":
configIngress.Name = YamlParser.GetScalarValue(key, child.Value);
break;
case "replicas":
if (!int.TryParse(YamlParser.GetScalarValue(key, child.Value), out var replicas))
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBeAnInteger(key));
}
if (replicas < 0)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBePositive(key));
}
configIngress.Replicas = replicas;
break;
case "rules":
if (child.Value.NodeType != YamlNodeType.Sequence)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
}
HandleIngressRules((child.Value as YamlSequenceNode)!, configIngress.Rules);
break;
case "bindings":
if (child.Value.NodeType != YamlNodeType.Sequence)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
}
HandleIngressBindings((child.Value as YamlSequenceNode)!, configIngress.Bindings);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
}
}
private static void HandleIngressRules(YamlSequenceNode yamlSequenceNode, List<ConfigIngressRule> rules)
{
foreach (var child in yamlSequenceNode.Children)
{
YamlParser.ThrowIfNotYamlMapping(child);
var rule = new ConfigIngressRule();
HandleIngressRuleMapping((YamlMappingNode)child, rule);
rules.Add(rule);
}
}
private static void HandleIngressRuleMapping(YamlMappingNode yamlMappingNode, ConfigIngressRule rule)
{
foreach (var child in yamlMappingNode!.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
switch (key)
{
case "host":
rule.Host = YamlParser.GetScalarValue(key, child.Value);
break;
case "path":
rule.Path = YamlParser.GetScalarValue(key, child.Value);
break;
case "service":
rule.Service = YamlParser.GetScalarValue(key, child.Value);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
}
}
private static void HandleIngressBindings(YamlSequenceNode yamlSequenceNode, List<ConfigIngressBinding> bindings)
{
foreach (var child in yamlSequenceNode.Children)
{
YamlParser.ThrowIfNotYamlMapping(child);
var binding = new ConfigIngressBinding();
HandleIngressBindingMapping((YamlMappingNode)child, binding);
bindings.Add(binding);
}
}
private static void HandleIngressBindingMapping(YamlMappingNode yamlMappingNode, ConfigIngressBinding binding)
{
foreach (var child in yamlMappingNode!.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
switch (key)
{
case "name":
binding.Name = YamlParser.GetScalarValue(key, child.Value);
break;
case "port":
if (!int.TryParse(YamlParser.GetScalarValue(key, child.Value), out var port))
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBeAnInteger(key));
}
binding.Port = port;
break;
case "protocol":
binding.Protocol = YamlParser.GetScalarValue(key, child.Value);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,225 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.Collections.Generic;
using Microsoft.Tye.ConfigModel;
using YamlDotNet.RepresentationModel;
namespace Tye.Serialization
{
public static class ConfigServiceParser
{
public static void HandleServiceMapping(YamlSequenceNode yamlSequenceNode, List<ConfigService> services)
{
foreach (var child in yamlSequenceNode.Children)
{
YamlParser.ThrowIfNotYamlMapping(child);
var service = new ConfigService();
HandleServiceNameMapping((YamlMappingNode)child, service);
services.Add(service);
}
}
private static void HandleServiceNameMapping(YamlMappingNode yamlMappingNode, ConfigService service)
{
foreach (var child in yamlMappingNode!.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
switch (key)
{
case "name":
service.Name = YamlParser.GetScalarValue(key, child.Value);
break;
case "external":
if (!bool.TryParse(YamlParser.GetScalarValue(key, child.Value), out var external))
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBeABoolean(key));
}
service.External = external;
break;
case "image":
service.Image = YamlParser.GetScalarValue(key, child.Value);
break;
case "project":
service.Project = YamlParser.GetScalarValue(key, child.Value);
break;
case "build":
if (!bool.TryParse(YamlParser.GetScalarValue(key, child.Value), out var build))
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBeABoolean(key));
}
service.Build = build;
break;
case "executable":
service.Executable = YamlParser.GetScalarValue(key, child.Value);
break;
case "workingDirectory":
service.WorkingDirectory = YamlParser.GetScalarValue(key, child.Value);
break;
case "args":
service.Args = YamlParser.GetScalarValue(key, child.Value);
break;
case "replicas":
if (!int.TryParse(YamlParser.GetScalarValue(key, child.Value), out var replicas))
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBeAnInteger(key));
}
if (replicas < 0)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBePositive(key));
}
service.Replicas = replicas;
break;
case "bindings":
if (child.Value.NodeType != YamlNodeType.Sequence)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
}
HandleServiceBindings((child.Value as YamlSequenceNode)!, service.Bindings);
break;
case "volumes":
if (child.Value.NodeType != YamlNodeType.Sequence)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
}
HandleServiceVolumes((child.Value as YamlSequenceNode)!, service.Volumes);
break;
case "env":
case "configuration":
if (child.Value.NodeType != YamlNodeType.Sequence)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
}
HandleServiceConfiguration((child.Value as YamlSequenceNode)!, service.Configuration);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
}
}
private static void HandleServiceBindings(YamlSequenceNode yamlSequenceNode, List<ConfigServiceBinding> bindings)
{
foreach (var child in yamlSequenceNode.Children)
{
YamlParser.ThrowIfNotYamlMapping(child);
var binding = new ConfigServiceBinding();
HandleServiceBindingNameMapping((YamlMappingNode)child, binding);
bindings.Add(binding);
}
}
private static void HandleServiceBindingNameMapping(YamlMappingNode yamlMappingNode, ConfigServiceBinding binding)
{
foreach (var child in yamlMappingNode.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
switch (key)
{
case "name":
binding.Name = YamlParser.GetScalarValue(key, child.Value);
break;
case "connectionString":
binding.ConnectionString = YamlParser.GetScalarValue(key, child.Value);
break;
case "port":
if (!int.TryParse(YamlParser.GetScalarValue(key, child.Value), out var port))
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBeAnInteger(key));
}
binding.Port = port;
break;
case "containerPort":
if (!int.TryParse(YamlParser.GetScalarValue(key, child.Value), out var containerPort))
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBeAnInteger(key));
}
binding.ContainerPort = containerPort;
break;
case "host":
binding.Host = YamlParser.GetScalarValue(key, child.Value);
break;
case "protocol":
binding.Protocol = YamlParser.GetScalarValue(key, child.Value);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
}
}
private static void HandleServiceVolumes(YamlSequenceNode yamlSequenceNode, List<ConfigVolume> volumes)
{
foreach (var child in yamlSequenceNode.Children)
{
YamlParser.ThrowIfNotYamlMapping(child);
var volume = new ConfigVolume();
HandleServiceVolumeNameMapping((YamlMappingNode)child, volume);
volumes.Add(volume);
}
}
private static void HandleServiceVolumeNameMapping(YamlMappingNode yamlMappingNode, ConfigVolume volume)
{
foreach (var child in yamlMappingNode!.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
switch (key)
{
case "name":
volume.Name = YamlParser.GetScalarValue(key, child.Value);
break;
case "source":
volume.Source = YamlParser.GetScalarValue(key, child.Value);
break;
case "target":
volume.Target = YamlParser.GetScalarValue(key, child.Value);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
}
}
private static void HandleServiceConfiguration(YamlSequenceNode yamlSequenceNode, List<ConfigConfigurationSource> configuration)
{
foreach (var child in yamlSequenceNode.Children)
{
YamlParser.ThrowIfNotYamlMapping(child);
var config = new ConfigConfigurationSource();
HandleServiceConfigurationNameMapping((YamlMappingNode)child, config);
configuration.Add(config);
}
}
private static void HandleServiceConfigurationNameMapping(YamlMappingNode yamlMappingNode, ConfigConfigurationSource config)
{
foreach (var child in yamlMappingNode!.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
switch (key)
{
case "name":
config.Name = YamlParser.GetScalarValue(key, child.Value);
break;
case "value":
config.Value = YamlParser.GetScalarValue(key, child.Value);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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 YamlDotNet.Core;
namespace Tye.Serialization
{
public class TyeYamlException : Exception
{
public TyeYamlException(string message)
: base(message)
{
}
public TyeYamlException(Mark start, string message)
: this(start, message, null)
{
}
public TyeYamlException(Mark start, string message, Exception? innerException)
: base($"Error parsing tye.yaml: ({start.Line}, {start.Column}): {message}", innerException)
{
}
public TyeYamlException(string message, Exception? inner)
: base(message, inner)
{
}
}
}

Просмотреть файл

@ -0,0 +1,118 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.IO;
using Microsoft.Tye.ConfigModel;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace Tye.Serialization
{
public class YamlParser : IDisposable
{
private YamlStream _yamlStream;
private FileInfo? _fileInfo;
private TextReader _reader;
public YamlParser(string yamlContent)
: this(new StringReader(yamlContent))
{
}
public YamlParser(FileInfo fileInfo)
: this(fileInfo.OpenText())
{
_fileInfo = fileInfo;
}
internal YamlParser(TextReader reader)
{
_reader = reader;
_yamlStream = new YamlStream();
}
public ConfigApplication ParseConfigApplication()
{
try
{
_yamlStream.Load(_reader);
}
catch (YamlException ex)
{
throw new TyeYamlException(ex.Start, "Unable to parse tye.yaml. See inner exception.", ex);
}
var app = new ConfigApplication();
// TODO assuming first document.
var document = _yamlStream.Documents[0];
var node = document.RootNode;
ThrowIfNotYamlMapping(node);
ConfigApplicationParser.HandleConfigApplication((YamlMappingNode)node, app);
app.Source = _fileInfo!;
// TODO confirm if these are ever null.
foreach (var service in app.Services)
{
service.Bindings ??= new List<ConfigServiceBinding>();
service.Configuration ??= new List<ConfigConfigurationSource>();
service.Volumes ??= new List<ConfigVolume>();
}
foreach (var ingress in app.Ingress)
{
ingress.Bindings ??= new List<ConfigIngressBinding>();
ingress.Rules ??= new List<ConfigIngressRule>();
}
return app;
}
public static string GetScalarValue(YamlNode node)
{
if (node.NodeType != YamlNodeType.Scalar)
{
throw new TyeYamlException(node.Start,
CoreStrings.FormatUnexpectedType(YamlNodeType.Scalar.ToString(), node.NodeType.ToString()));
}
return ((YamlScalarNode)node).Value!;
}
public static string GetScalarValue(string key, YamlNode node)
{
if (node.NodeType != YamlNodeType.Scalar)
{
throw new TyeYamlException(node.Start, CoreStrings.FormatExpectedYamlScalar(key));
}
return ((YamlScalarNode)node).Value!;
}
public static void ThrowIfNotYamlSequence(string key, YamlNode node)
{
if (node.NodeType != YamlNodeType.Sequence)
{
throw new TyeYamlException(node.Start, CoreStrings.FormatExpectedYamlSequence(key));
}
}
public static void ThrowIfNotYamlMapping(YamlNode node)
{
if (node.NodeType != YamlNodeType.Mapping)
{
throw new TyeYamlException(node.Start,
CoreStrings.FormatUnexpectedType(YamlNodeType.Mapping.ToString(), node.NodeType.ToString()));
}
}
public void Dispose()
{
_reader.Dispose();
}
}
}

Просмотреть файл

@ -17,12 +17,5 @@ namespace Microsoft.Tye.Serialization
.WithEmissionPhaseObjectGraphVisitor(args => new OmitDefaultAndEmptyArrayObjectGraphVisitor(args.InnerVisitor))
.Build();
}
public static IDeserializer CreateDeserializer()
{
return new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
}
}
}

Просмотреть файл

@ -1,4 +1,5 @@
@page "/"
@namespace Microsoft.Tye.Hosting.Dashboard.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
@ -17,7 +18,7 @@
</head>
<body>
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
<component type="typeof(Microsoft.Tye.Hosting.Dashboard.App)" render-mode="ServerPrerendered" />
</app>
<div id="blazor-error-ui">

Просмотреть файл

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Microsoft.Tye.UnitTests</AssemblyName>
<IsTestProject>true</IsTestProject>
<IsUnitTestProject>true</IsUnitTestProject>
<IsPackable>false</IsPackable>
<TestRunnerName>XUnit</TestRunnerName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="1.0.1" />
<PackageReference Include="coverlet.msbuild" Version="2.8.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Tye.Hosting\Microsoft.Tye.Hosting.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Tye.Core\Microsoft.Tye.Core.csproj" />
<ProjectReference Include="..\..\src\tye\tye.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,582 @@
using System.IO;
using System.Linq;
using Microsoft.Tye.ConfigModel;
using Tye;
using Tye.Serialization;
using Xunit;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace Microsoft.Tye.UnitTests
{
public class TyeDeserializationTests
{
private IDeserializer _deserializer;
public TyeDeserializationTests()
{
_deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
}
[Fact]
public void ComprehensionalTest()
{
var input = @"
name: apps-with-ingress
registry: myregistry
extensions:
- name: dapr
ingress:
- name: ingress
bindings:
- port: 8080
protocol: http
name: foo
rules:
- path: /A
service: appA
- path: /B
service: appB
- host: a.example.com
service: appA
- host: b.example.com
service: appB
replicas: 2
services:
- name: appA
project: ApplicationA/ApplicationA.csproj
replicas: 2
external: false
image: abc
build: false
executable: test.exe
workingDirectory: ApplicationA/
args: a b c
env:
- name: POSTGRES_PASSWORD
value: ""test""
- name: POSTGRES_PASSWORD2
value: ""test2""
volumes:
- name: volume
source: /data
target: /data
bindings:
- name: test
port: 4444
connectionString: asdf
containerPort: 80
host: localhost
protocol: http
- name: appB
project: ApplicationB/ApplicationB.csproj
replicas: 2";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
}
[Fact]
public void IngressIsSetCorrectly()
{
var input = @"
ingress:
- name: ingress
bindings:
- port: 8080
protocol: http
name: foo
rules:
- path: /A
service: appA
- path: /B
service: appB
- host: a.example.com
service: appA
- host: b.example.com
service: appB
replicas: 2";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
var expected = _deserializer.Deserialize<ConfigApplication>(new StringReader(input));
foreach (var ingress in app.Ingress)
{
var otherIngress = expected
.Ingress
.Where(o => o.Name == ingress.Name)
.Single();
Assert.NotNull(otherIngress);
Assert.Equal(otherIngress.Replicas, ingress.Replicas);
foreach (var rule in ingress.Rules)
{
var otherRule = otherIngress
.Rules
.Where(o => o.Path == rule.Path && o.Host == rule.Host && o.Service == rule.Service)
.Single();
Assert.NotNull(otherRule);
}
foreach (var binding in ingress.Bindings)
{
var otherBinding = otherIngress
.Bindings
.Where(o => o.Name == binding.Name && o.Port == binding.Port && o.Protocol == binding.Protocol)
.Single();
Assert.NotNull(otherBinding);
}
}
}
[Fact]
public void ServicesSetCorrectly()
{
var input = @"services:
- name: appA
project: ApplicationA/ApplicationA.csproj
replicas: 2
external: false
image: abc
build: false
executable: test.exe
workingDirectory: ApplicationA/
args: a b c
env:
- name: POSTGRES_PASSWORD
value: ""test""
- name: POSTGRES_PASSWORD2
value: ""test2""
volumes:
- name: volume
source: /data
target: /data
bindings:
- name: test
port: 4444
connectionString: asdf
containerPort: 80
host: localhost
protocol: http
- name: appB
project: ApplicationB/ApplicationB.csproj
replicas: 2";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
var expected = _deserializer.Deserialize<ConfigApplication>(new StringReader(input));
foreach (var service in app.Services)
{
var otherService = expected
.Services
.Where(o => o.Name == service.Name)
.Single();
Assert.NotNull(otherService);
Assert.Equal(otherService.Args, service.Args);
Assert.Equal(otherService.Build, service.Build);
Assert.Equal(otherService.Executable, service.Executable);
Assert.Equal(otherService.External, service.External);
Assert.Equal(otherService.Image, service.Image);
Assert.Equal(otherService.Project, service.Project);
Assert.Equal(otherService.Replicas, service.Replicas);
Assert.Equal(otherService.WorkingDirectory, service.WorkingDirectory);
foreach (var binding in service.Bindings)
{
var otherBinding = otherService.Bindings
.Where(o => o.Name == binding.Name
&& o.Port == binding.Port
&& o.Protocol == binding.Protocol
&& o.ConnectionString == binding.ConnectionString
&& o.ContainerPort == binding.ContainerPort
&& o.Host == binding.Host)
.Single();
Assert.NotNull(otherBinding);
}
foreach (var binding in service.Bindings)
{
var otherBinding = otherService.Bindings
.Where(o => o.Name == binding.Name
&& o.Port == binding.Port
&& o.Protocol == binding.Protocol
&& o.ConnectionString == binding.ConnectionString
&& o.ContainerPort == binding.ContainerPort
&& o.Host == binding.Host)
.Single();
Assert.NotNull(otherBinding);
}
foreach (var config in service.Configuration)
{
var otherConfig = otherService.Configuration
.Where(o => o.Name == config.Name
&& o.Value == config.Value)
.Single();
Assert.NotNull(otherConfig);
}
foreach (var volume in service.Volumes)
{
var otherVolume = otherService.Volumes
.Where(o => o.Name == volume.Name
&& o.Target == volume.Target
&& o.Source == volume.Source)
.Single();
Assert.NotNull(otherVolume);
}
}
}
[Fact]
public void ExtensionsTest()
{
var input = @"
extensions:
- name: dapr";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
Assert.Equal("dapr", app.Extensions.Single()["name"]);
var expected = _deserializer.Deserialize<ConfigApplication>(new StringReader(input));
Assert.Equal(expected.Extensions.Count, app.Extensions.Count);
}
[Fact]
public void VotingTest()
{
using var parser = new YamlParser(
@"name: VotingSample
registry: myregistry
services:
- name: vote
project: vote/vote.csproj
- name: redis
image: redis
bindings:
- port: 6379
- name: worker
project: worker/worker.csproj
- name: postgres
image: postgres
env:
- name: POSTGRES_PASSWORD
value: ""test""
bindings:
- port: 5432
- name: results
project: results/results.csproj");
var app = parser.ParseConfigApplication();
}
[Fact]
public void UnrecognizedConfigApplicationField_ThrowException()
{
using var parser = new YamlParser("asdf: 123");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnrecognizedKey("asdf"), exception.Message);
}
[Fact]
public void Replicas_MustBeInteger()
{
using var parser = new YamlParser(
@"services:
- name: app
replicas: asdf");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatMustBeAnInteger("replicas"), exception.Message);
}
[Fact]
public void Replicas_MustBePositive()
{
using var parser = new YamlParser(
@"services:
- name: app
replicas: -1");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatMustBePositive("replicas"), exception.Message);
}
[Fact]
public void Name_MustBeScalar()
{
using var parser = new YamlParser(
@"name:
- a: b");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlScalar("name"), exception.Message);
}
[Fact]
public void YamlIsCaseSensitive()
{
using var parser = new YamlParser(
@"Name: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnrecognizedKey("Name"), exception.Message);
}
[Fact]
public void Registry_MustBeScalar()
{
using var parser = new YamlParser(
@"registry:
- a: b");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlScalar("registry"), exception.Message);
}
[Fact]
public void Ingress_MustBeSequence()
{
using var parser = new YamlParser(
@"ingress: a");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("ingress"), exception.Message);
}
[Fact]
public void Services_MustBeSequence()
{
using var parser = new YamlParser(
@"services: a");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("services"), exception.Message);
}
[Fact]
public void ConfigApplication_MustBeMappings()
{
using var parser = new YamlParser(
@"- name: app
replicas: -1");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnexpectedType(YamlNodeType.Mapping.ToString(), YamlNodeType.Sequence.ToString()), exception.Message);
}
[Fact]
public void Services_MustBeMappings()
{
using var parser = new YamlParser(
@"services:
- name");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnexpectedType(YamlNodeType.Mapping.ToString(), YamlNodeType.Scalar.ToString()), exception.Message);
}
[Fact]
public void Ingress_MustBeMappings()
{
using var parser = new YamlParser(
@"ingress:
- name");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnexpectedType(YamlNodeType.Mapping.ToString(), YamlNodeType.Scalar.ToString()), exception.Message);
}
[Fact]
public void Ingress_Replicas_MustBeInteger()
{
using var parser = new YamlParser(
@"ingress:
- replicas: asdf");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatMustBeAnInteger("replicas"), exception.Message);
}
[Fact]
public void Ingress_Replicas_MustBePositive()
{
using var parser = new YamlParser(
@"ingress:
- replicas: -1");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatMustBePositive("replicas"), exception.Message);
}
[Fact]
public void Ingress_UnrecognizedKey()
{
using var parser = new YamlParser(
@"ingress:
- abc: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnrecognizedKey("abc"), exception.Message);
}
[Fact]
public void Ingress_Rules_MustSequence()
{
using var parser = new YamlParser(
@"ingress:
- rules: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("rules"), exception.Message);
}
[Fact]
public void Ingress_Rules_MustBeMappings()
{
using var parser = new YamlParser(
@"ingress:
- rules:
- abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnexpectedType(YamlNodeType.Mapping.ToString(), YamlNodeType.Scalar.ToString()), exception.Message);
}
[Fact]
public void Ingress_Bindings_MustBeMappings()
{
using var parser = new YamlParser(
@"ingress:
- bindings:
- abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnexpectedType(YamlNodeType.Mapping.ToString(), YamlNodeType.Scalar.ToString()), exception.Message);
}
[Fact]
public void Ingress_RulesMapping_UnrecognizedKey()
{
using var parser = new YamlParser(
@"ingress:
- rules:
- abc: 123");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnrecognizedKey("abc"), exception.Message);
}
[Fact]
public void Ingress_Bindings_MustSequence()
{
using var parser = new YamlParser(
@"ingress:
- bindings: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("bindings"), exception.Message);
}
[Fact]
public void Ingress_Bindings_Port_MustBeInteger()
{
using var parser = new YamlParser(
@"ingress:
- name: ingress
bindings:
- port: abc
protocol: http
name: foo");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatMustBeAnInteger("port"), exception.Message);
}
[Fact]
public void Ingress_Bindings_UnrecognizedKey()
{
using var parser = new YamlParser(
@"ingress:
- name: ingress
bindings:
- abc: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnrecognizedKey("abc"), exception.Message);
}
[Fact]
public void Services_External_MustBeBool()
{
using var parser = new YamlParser(
@"services:
- name: ingress
external: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatMustBeABoolean("external"), exception.Message);
}
[Fact]
public void Services_Build_MustBeBool()
{
using var parser = new YamlParser(
@"services:
- name: ingress
build: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatMustBeABoolean("build"), exception.Message);
}
[Fact]
public void Services_Bindings_MustBeSequence()
{
using var parser = new YamlParser(
@"services:
- name: ingress
bindings: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("bindings"), exception.Message);
}
[Fact]
public void Services_Volumes_MustBeSequence()
{
using var parser = new YamlParser(
@"services:
- name: ingress
volumes: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("volumes"), exception.Message);
}
[Fact]
public void Services_Env_MustBeSequence()
{
using var parser = new YamlParser(
@"services:
- name: ingress
env: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("env"), exception.Message);
}
[Fact]
public void Services_UnrecognizedKey()
{
using var parser = new YamlParser(
@"services:
- name: ingress
env: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("env"), exception.Message);
}
}
}

Просмотреть файл

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Text;
using Tye;
using Tye.Serialization;
using Xunit;
namespace Microsoft.Tye.UnitTests
{
public class TyeDeserializationValidationTests
{
[Fact]
public void MultipleIngressBindingsMustHaveNames()
{
var input = @"
ingress:
- name: ingress
bindings:
- port: 8080
protocol: http
- port: 8080
protocol: http";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
var exception = Assert.Throws<TyeYamlException>(() => app.Validate());
Assert.Contains(CoreStrings.MultipleIngressBindingWithoutName, exception.Message);
}
[Fact]
public void MultipleServicesBindingsMustHaveNames()
{
var input = @"
services:
- name: app
bindings:
- port: 8080
protocol: http
- port: 8080
protocol: http";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
var exception = Assert.Throws<TyeYamlException>(() => app.Validate());
Assert.Contains(CoreStrings.MultipleServiceBindingsWithoutName, exception.Message);
}
[Fact]
public void MultipleIngressBindingsMustUniqueNames()
{
var input = @"
ingress:
- name: ingress
bindings:
- port: 8080
protocol: http
name: a
- port: 8080
protocol: http
name: a";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
var exception = Assert.Throws<TyeYamlException>(() => app.Validate());
Assert.Contains(CoreStrings.MultipleIngressBindingWithSameName, exception.Message);
}
[Fact]
public void IngressProtocolsShouldBeHttpOrHttps()
{
var input = @"
ingress:
- name: ingress
bindings:
- port: 8080
protocol: tls
name: a";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
var exception = Assert.Throws<TyeYamlException>(() => app.Validate());
Assert.Contains(CoreStrings.IngressBindingMustBeHttpOrHttps, exception.Message);
}
[Fact]
public void MultipleServicesBindingsMustUniqueNames()
{
var input = @"
services:
- name: app
bindings:
- port: 8080
protocol: http
name: a
- port: 8080
protocol: http
name: a";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
var exception = Assert.Throws<TyeYamlException>(() => app.Validate());
Assert.Contains(CoreStrings.MultipleServiceBindingsWithSameName, exception.Message);
}
[Fact]
public void IngressMustReferenceService()
{
var input = @"
ingress:
- name: ingress
bindings:
- port: 8080
protocol: http
name: foo
rules:
- path: /A
service: appA
- path: /B
service: appB
- host: a.example.com
service: appA
- host: b.example.com
service: appB
replicas: 2";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
var exception = Assert.Throws<TyeYamlException>(() => app.Validate());
Assert.Contains(CoreStrings.IngressRuleMustReferenceService, exception.Message);
}
}
}

15
tye.sln
Просмотреть файл

@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Hosting.Runti
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Core", "src\Microsoft.Tye.Core\Microsoft.Tye.Core.csproj", "{D0359C69-6EA9-4B03-9455-90E8E04F1CB0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.UnitTests", "test\UnitTests\Microsoft.Tye.UnitTests.csproj", "{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Extensions", "src\Microsoft.Tye.Extensions\Microsoft.Tye.Extensions.csproj", "{AAF0CE0B-E53A-4E10-AA82-BF7200AB2B0C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Extensions.Configuration", "src\Microsoft.Tye.Extensions.Configuration\Microsoft.Tye.Extensions.Configuration.csproj", "{B07394E4-30A7-429A-BC5A-747B54D5A447}"
@ -143,6 +145,18 @@ Global
{7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Release|x64.Build.0 = Release|Any CPU
{7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Release|x86.ActiveCfg = Release|Any CPU
{7C9021B7-64BA-4DA9-88DA-5BC12A1C6233}.Release|x86.Build.0 = Release|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Debug|x64.ActiveCfg = Debug|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Debug|x64.Build.0 = Debug|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Debug|x86.ActiveCfg = Debug|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Debug|x86.Build.0 = Debug|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Release|Any CPU.Build.0 = Release|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Release|x64.ActiveCfg = Release|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Release|x64.Build.0 = Release|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Release|x86.ActiveCfg = Release|Any CPU
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -157,6 +171,7 @@ Global
{AAF0CE0B-E53A-4E10-AA82-BF7200AB2B0C} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601}
{B07394E4-30A7-429A-BC5A-747B54D5A447} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601}
{7C9021B7-64BA-4DA9-88DA-5BC12A1C6233} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601}
{2233F4A8-10F9-40A6-BFD3-8D0C37F8359A} = {F19B02EB-A372-417A-B2C2-EA0D5A3C76D5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D8002603-BB27-4500-BF86-274A8E72D302}