From 76691810e65de23280de5c76b529ad2a130c92e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igr=20Alex=C3=A1nder=20Fern=C3=A1ndez=20Sa=C3=BAco?= Date: Thu, 23 Apr 2020 00:08:08 -0400 Subject: [PATCH] Add buildProperties to project service configuration (#383) * Add buildArgs to service configuration and capture build configuration as global property to setup msbuild project * Fix format * Fix - Error CS8600: Converting null literal or possible null value to non-nullable type. * Improve configuration format for build arguments * Fix whitespace format * Fix error CS8618: Non-nullable property 'Properties' is uninitialized. Consider declaring the property as nullable. * Fix error CS8601: Possible null reference assignment. * Translate non first class properties as /p:{Key}={Value} into the build command * Fix property translation * All properties are used to create the msbuild project * Change the name (to BuildProperties) and the type (to List) of ConfigService property to load build properties * Add support of build properties when tye run with --docker option * Fix ComprehensionalTest * Add tests to verify the output directory for the corresponding build configuration * Fix whitespace format * Override the correct CreateTestCasesForTheory to fix error CS0618 * Remove the usage of BuildPropertiesToOptionsMap and fix the code format --- src/Microsoft.Tye.Core/ApplicationFactory.cs | 26 ++++-------- .../ConfigModel/BuildProperty.cs | 17 ++++++++ .../ConfigModel/ConfigService.cs | 1 + src/Microsoft.Tye.Core/ProjectReader.cs | 2 + .../ProjectServiceBuilder.cs | 2 + .../Serialization/ConfigServiceParser.cs | 38 +++++++++++++++++ .../Model/ProjectRunInfo.cs | 3 ++ src/Microsoft.Tye.Hosting/ProcessRunner.cs | 7 +++- .../TransformProjectsIntoContainers.cs | 6 ++- src/tye/tye.csproj | 3 +- test/E2ETest/E2ETest.csproj | 2 + .../ConditionalTheoryAttribute.cs | 16 +++++++ .../ConditionalTheoryDiscoverer.cs | 41 ++++++++++++++++++ test/E2ETest/TyeRunTests.cs | 42 +++++++++++++++++++ .../tye-debug-configuration.yaml | 14 +++++++ .../tye-release-configuration.yaml | 14 +++++++ test/UnitTests/TyeDeserializationTests.cs | 3 ++ 17 files changed, 216 insertions(+), 21 deletions(-) create mode 100644 src/Microsoft.Tye.Core/ConfigModel/BuildProperty.cs create mode 100644 test/E2ETest/Infrastructure/ConditionalTheoryAttribute.cs create mode 100644 test/E2ETest/Infrastructure/ConditionalTheoryDiscoverer.cs create mode 100644 test/E2ETest/testassets/projects/frontend-backend/tye-debug-configuration.yaml create mode 100644 test/E2ETest/testassets/projects/frontend-backend/tye-release-configuration.yaml diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 7d77c857f..2a497e57e 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -59,16 +59,17 @@ public static async Task CreateAsync(OutputContext output, F project.Build = configService.Build ?? true; project.Args = configService.Args; + foreach (var buildProperty in configService.BuildProperties) + { + project.BuildProperties.Add(buildProperty.Name, buildProperty.Value); + } project.Replicas = configService.Replicas ?? 1; await ProjectReader.ReadProjectDetailsAsync(output, project); // We don't apply more container defaults here because we might need // to prompt for the registry name. - project.ContainerInfo = new ContainerInfo() - { - UseMultiphaseDockerfile = false, - }; + project.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, }; // Do k8s by default. project.ManifestInfo = new KubernetesManifestInfo(); @@ -122,16 +123,9 @@ service is ProjectServiceBuilder project2 && project2.IsAspNet) { // HTTP is the default binding - service.Bindings.Add(new BindingBuilder() - { - Protocol = "http" - }); + service.Bindings.Add(new BindingBuilder() { Protocol = "http" }); - service.Bindings.Add(new BindingBuilder() - { - Name = "https", - Protocol = "https" - }); + service.Bindings.Add(new BindingBuilder() { Name = "https", Protocol = "https" }); } else { @@ -159,10 +153,7 @@ service is ProjectServiceBuilder project2 && foreach (var configEnvVar in configService.Configuration) { - var envVar = new EnvironmentVariableBuilder(configEnvVar.Name) - { - Value = configEnvVar.Value, - }; + var envVar = new EnvironmentVariableBuilder(configEnvVar.Name) { Value = configEnvVar.Value, }; if (service is ProjectServiceBuilder project) { project.EnvironmentVariables.Add(envVar); @@ -211,6 +202,7 @@ service is ProjectServiceBuilder project2 && } } + foreach (var configIngress in config.Ingress) { var ingress = new IngressBuilder(configIngress.Name!); diff --git a/src/Microsoft.Tye.Core/ConfigModel/BuildProperty.cs b/src/Microsoft.Tye.Core/ConfigModel/BuildProperty.cs new file mode 100644 index 000000000..9ef9a03c4 --- /dev/null +++ b/src/Microsoft.Tye.Core/ConfigModel/BuildProperty.cs @@ -0,0 +1,17 @@ +// 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.ComponentModel.DataAnnotations; + +namespace Microsoft.Tye.ConfigModel +{ + public class BuildProperty + { + [Required] + public string Name { get; set; } = default!; + + [Required] + public string Value { get; set; } = default!; + } +} diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs index 0da6ad624..d74d9953e 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs @@ -24,5 +24,6 @@ public class ConfigService public List Volumes { get; set; } = new List(); [YamlMember(Alias = "env")] public List Configuration { get; set; } = new List(); + public List BuildProperties { get; set; } = new List(); } } diff --git a/src/Microsoft.Tye.Core/ProjectReader.cs b/src/Microsoft.Tye.Core/ProjectReader.cs index 538bf4486..49ccbbec9 100644 --- a/src/Microsoft.Tye.Core/ProjectReader.cs +++ b/src/Microsoft.Tye.Core/ProjectReader.cs @@ -26,6 +26,7 @@ namespace Microsoft.Tye public static class ProjectReader { private static object @lock = new object(); + private static bool registered; public static IEnumerable EnumerateProjects(FileInfo solutionFile) @@ -165,6 +166,7 @@ private static void EvaluateProject(OutputContext output, ProjectServiceBuilder msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(project.ProjectFile.FullName, new ProjectOptions() { ProjectCollection = projectCollection, + GlobalProperties = project.BuildProperties }); projectInstance = msbuildProject.CreateProjectInstance(); output.WriteDebugLine($"Loaded project '{project.ProjectFile.FullName}'."); diff --git a/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs b/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs index 5c75daebd..e38f2cafb 100644 --- a/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs +++ b/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs @@ -49,5 +49,7 @@ public ProjectServiceBuilder(string name, FileInfo projectFile) // Used when running in a container locally. public List Volumes { get; } = new List(); + + public Dictionary BuildProperties { get; } = new Dictionary(); } } diff --git a/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs b/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs index 6dd434918..6ffb4bbf2 100644 --- a/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs +++ b/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs @@ -45,6 +45,13 @@ private static void HandleServiceNameMapping(YamlMappingNode yamlMappingNode, Co case "project": service.Project = YamlParser.GetScalarValue(key, child.Value); break; + case "buildProperties": + if (child.Value.NodeType != YamlNodeType.Sequence) + { + throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key)); + } + HandleBuildProperties((child.Value as YamlSequenceNode)!, service.BuildProperties); + break; case "build": if (!bool.TryParse(YamlParser.GetScalarValue(key, child.Value), out var build)) { @@ -191,6 +198,17 @@ private static void HandleServiceVolumeNameMapping(YamlMappingNode yamlMappingNo } } + private static void HandleBuildProperties(YamlSequenceNode yamlSequenceNode, List buildProperties) + { + foreach (var child in yamlSequenceNode.Children) + { + YamlParser.ThrowIfNotYamlMapping(child); + var buildProperty = new BuildProperty(); + HandleServiceBuildPropertyNameMapping((YamlMappingNode)child, buildProperty); + buildProperties.Add(buildProperty); + } + } + private static void HandleServiceConfiguration(YamlSequenceNode yamlSequenceNode, List configuration) { foreach (var child in yamlSequenceNode.Children) @@ -221,5 +239,25 @@ private static void HandleServiceConfigurationNameMapping(YamlMappingNode yamlMa } } } + + private static void HandleServiceBuildPropertyNameMapping(YamlMappingNode yamlMappingNode, BuildProperty buildProperty) + { + foreach (var child in yamlMappingNode!.Children) + { + var key = YamlParser.GetScalarValue(child.Key); + + switch (key) + { + case "name": + buildProperty.Name = YamlParser.GetScalarValue(key, child.Value); + break; + case "value": + buildProperty.Value = YamlParser.GetScalarValue(key, child.Value); + break; + default: + throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key)); + } + } + } } } diff --git a/src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs b/src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs index 18b2b6a64..6a09eda09 100644 --- a/src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs +++ b/src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs @@ -14,6 +14,7 @@ public ProjectRunInfo(ProjectServiceBuilder project) ProjectFile = project.ProjectFile; Args = project.Args; Build = project.Build; + BuildProperties = project.BuildProperties; TargetFramework = project.TargetFramework; TargetFrameworkName = project.TargetFrameworkName; TargetFrameworkVersion = project.TargetFrameworkVersion; @@ -25,6 +26,8 @@ public ProjectRunInfo(ProjectServiceBuilder project) PublishOutputPath = project.PublishDir; } + public Dictionary BuildProperties { get; } = new Dictionary(); + public string? Args { get; } public bool Build { get; } public FileInfo ProjectFile { get; } diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 6c4e55df5..776f51dae 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -51,12 +51,15 @@ private async Task BuildAndRunProjects(Application application) string path; string args; + var buildArgs = string.Empty; string workingDirectory; if (serviceDescription.RunInfo is ProjectRunInfo project) { path = project.RunCommand; workingDirectory = project.ProjectFile.Directory.FullName; args = project.Args == null ? project.RunArguments : project.RunArguments + " " + project.Args; + buildArgs = project.BuildProperties.Aggregate(string.Empty, (current, property) => current + $" /p:{property.Key}={property.Value}").TrimStart(); + service.Status.ProjectFilePath = project.ProjectFile.FullName; } else if (serviceDescription.RunInfo is ExecutableRunInfo executable) @@ -90,9 +93,9 @@ service.Description.RunInfo is ProjectRunInfo project2 && // Sometimes building can fail because of file locking (like files being open in VS) _logger.LogInformation("Building project {ProjectFile}", service.Status.ProjectFilePath); - service.Logs.OnNext($"dotnet build \"{service.Status.ProjectFilePath}\" /nologo"); + service.Logs.OnNext($"dotnet build \"{service.Status.ProjectFilePath}\" {buildArgs} /nologo"); - var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError: false, workingDirectory: workingDirectory); + var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" {buildArgs} /nologo", throwOnError: false, workingDirectory: workingDirectory); service.Logs.OnNext(buildResult.StandardOutput); diff --git a/src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs b/src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs index fda8213b6..d62f87641 100644 --- a/src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs +++ b/src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs @@ -9,6 +9,8 @@ namespace Microsoft.Tye.Hosting { + using System.Linq; + public class TransformProjectsIntoContainers : IApplicationProcessor { private readonly ILogger _logger; @@ -44,7 +46,9 @@ private async Task TransformProjectToContainer(Service service, ProjectRunInfo p // Sometimes building can fail because of file locking (like files being open in VS) _logger.LogInformation("Publishing project {ProjectFile}", service.Status.ProjectFilePath); - var publishCommand = $"publish \"{service.Status.ProjectFilePath}\" --framework {targetFramework} /nologo"; + var buildArgs = project.BuildProperties.Aggregate(string.Empty, (current, property) => current + $" /p:{property.Key}={property.Value}").TrimStart(); + + var publishCommand = $"publish \"{service.Status.ProjectFilePath}\" --framework {targetFramework} {buildArgs} /nologo"; service.Logs.OnNext($"dotnet {publishCommand}"); diff --git a/src/tye/tye.csproj b/src/tye/tye.csproj index 47515e114..3d4e64227 100644 --- a/src/tye/tye.csproj +++ b/src/tye/tye.csproj @@ -1,4 +1,4 @@ - + Exe @@ -7,6 +7,7 @@ tye Microsoft.Tye tye + 0.2.0-dev-1 true diff --git a/test/E2ETest/E2ETest.csproj b/test/E2ETest/E2ETest.csproj index 4343c569d..b31bf9371 100644 --- a/test/E2ETest/E2ETest.csproj +++ b/test/E2ETest/E2ETest.csproj @@ -39,6 +39,8 @@ + + diff --git a/test/E2ETest/Infrastructure/ConditionalTheoryAttribute.cs b/test/E2ETest/Infrastructure/ConditionalTheoryAttribute.cs new file mode 100644 index 000000000..fba9054fb --- /dev/null +++ b/test/E2ETest/Infrastructure/ConditionalTheoryAttribute.cs @@ -0,0 +1,16 @@ +// 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 Xunit; +using Xunit.Sdk; + +namespace E2ETest +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + [XunitTestCaseDiscoverer("E2ETest." + nameof(ConditionalTheoryDiscoverer), "Microsoft.Tye.E2ETest")] + public class ConditionalTheoryAttribute : TheoryAttribute + { + } +} diff --git a/test/E2ETest/Infrastructure/ConditionalTheoryDiscoverer.cs b/test/E2ETest/Infrastructure/ConditionalTheoryDiscoverer.cs new file mode 100644 index 000000000..29ea0f182 --- /dev/null +++ b/test/E2ETest/Infrastructure/ConditionalTheoryDiscoverer.cs @@ -0,0 +1,41 @@ +// 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 Xunit.Abstractions; +using Xunit.Sdk; + +namespace E2ETest +{ + internal class ConditionalTheoryDiscoverer : TheoryDiscoverer + { + private readonly IMessageSink _diagnosticMessageSink; + + public ConditionalTheoryDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + _diagnosticMessageSink = diagnosticMessageSink; + } + + protected override IEnumerable CreateTestCasesForTheory( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo theoryAttribute) + { + var skipReason = testMethod.EvaluateSkipConditions(); + return skipReason != null + ? new IXunitTestCase[] + { + new SkippedTestCase( + skipReason, + _diagnosticMessageSink, + discoveryOptions.MethodDisplayOrDefault(), + TestMethodDisplayOptions.None, + testMethod) + } + : base.CreateTestCasesForTheory(discoveryOptions, testMethod, theoryAttribute); + } + } +} diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 9458f71a5..1dac22c47 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -108,6 +108,48 @@ await RunHostingApplication(application, new[] { "--docker" }, async (app, uri) }); } + [ConditionalTheory] + [SkipIfDockerNotRunning] + [InlineData("Debug")] + [InlineData("Release")] + public async Task FrontendBackendRunTestWithDockerAndBuildConfigurationAsProperty(string buildConfiguration) + { + using var projectDirectory = CopyTestProjectDirectory("frontend-backend"); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, $"tye-{buildConfiguration.ToLower()}-configuration.yaml")); + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + AllowAutoRedirect = false + }; + + var client = new HttpClient(new RetryHandler(handler)); + + await RunHostingApplication(application, new[] { "--docker" }, async (app, uri) => + { + // Make sure we're running containers + Assert.True(app.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo)); + + var frontendUri = await GetServiceUrl(client, uri, "frontend"); + var backendUri = await GetServiceUrl(client, uri, "backend"); + + var backendResponse = await client.GetAsync(backendUri); + var frontendResponse = await client.GetAsync(frontendUri); + + Assert.True(backendResponse.IsSuccessStatusCode); + Assert.True(frontendResponse.IsSuccessStatusCode); + + Assert.True(app.Services.All(s => s.Value.Description.RunInfo != null && ((DockerRunInfo)s.Value.Description.RunInfo).VolumeMappings.Count > 0)); + + var outputFileInfos = app.Services.Select(s => new FileInfo((s.Value?.Description?.RunInfo as DockerRunInfo)?.VolumeMappings[0].Source ?? throw new InvalidOperationException())).ToList(); + + Assert.True(outputFileInfos.All(f => f.Directory?.Parent?.Parent?.Name == buildConfiguration)); + }); + } + [ConditionalFact] [SkipIfDockerNotRunning] public async Task FrontendProjectBackendDocker() diff --git a/test/E2ETest/testassets/projects/frontend-backend/tye-debug-configuration.yaml b/test/E2ETest/testassets/projects/frontend-backend/tye-debug-configuration.yaml new file mode 100644 index 000000000..1c4cb7c8d --- /dev/null +++ b/test/E2ETest/testassets/projects/frontend-backend/tye-debug-configuration.yaml @@ -0,0 +1,14 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +name: frontend-backend +services: +- name: backend + project: backend/backend.csproj + buildProperties: + - name: Configuration + value: Debug +- name: frontend + project: frontend/frontend.csproj + buildProperties: + - name: Configuration + value: Debug diff --git a/test/E2ETest/testassets/projects/frontend-backend/tye-release-configuration.yaml b/test/E2ETest/testassets/projects/frontend-backend/tye-release-configuration.yaml new file mode 100644 index 000000000..daf920950 --- /dev/null +++ b/test/E2ETest/testassets/projects/frontend-backend/tye-release-configuration.yaml @@ -0,0 +1,14 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +name: frontend-backend +services: +- name: backend + project: backend/backend.csproj + buildProperties: + - name: Configuration + value: Release +- name: frontend + project: frontend/frontend.csproj + buildProperties: + - name: Configuration + value: Release diff --git a/test/UnitTests/TyeDeserializationTests.cs b/test/UnitTests/TyeDeserializationTests.cs index 996dda63b..e3666460d 100644 --- a/test/UnitTests/TyeDeserializationTests.cs +++ b/test/UnitTests/TyeDeserializationTests.cs @@ -49,6 +49,9 @@ public void ComprehensionalTest() services: - name: appA project: ApplicationA/ApplicationA.csproj + buildProperties: + - name: Configuration + - value: Debug replicas: 2 external: false image: abc