зеркало из https://github.com/dotnet/tye.git
Implement custom deserialization and validation logic (#294)
This commit is contained in:
Родитель
b92ea3870c
Коммит
feb85cef84
|
@ -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())' > 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
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}
|
||||
|
|
Загрузка…
Ссылка в новой задаче