Skip to content

Commit

Permalink
Add buildProperties to project service configuration (dotnet#383)
Browse files Browse the repository at this point in the history
* 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<BuildProperty>) 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
  • Loading branch information
alexfdezsauco authored and kishanAnem committed May 15, 2020
1 parent 368603a commit bce8817
Show file tree
Hide file tree
Showing 17 changed files with 216 additions and 21 deletions.
26 changes: 9 additions & 17 deletions src/Microsoft.Tye.Core/ApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,17 @@ public static async Task<ApplicationBuilder> 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();
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -211,6 +202,7 @@ service is ProjectServiceBuilder project2 &&
}
}


foreach (var configIngress in config.Ingress)
{
var ingress = new IngressBuilder(configIngress.Name!);
Expand Down
17 changes: 17 additions & 0 deletions src/Microsoft.Tye.Core/ConfigModel/BuildProperty.cs
Original file line number Diff line number Diff line change
@@ -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!;
}
}
1 change: 1 addition & 0 deletions src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ public class ConfigService
public List<ConfigVolume> Volumes { get; set; } = new List<ConfigVolume>();
[YamlMember(Alias = "env")]
public List<ConfigConfigurationSource> Configuration { get; set; } = new List<ConfigConfigurationSource>();
public List<BuildProperty> BuildProperties { get; set; } = new List<BuildProperty>();
}
}
2 changes: 2 additions & 0 deletions src/Microsoft.Tye.Core/ProjectReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace Microsoft.Tye
public static class ProjectReader
{
private static object @lock = new object();

private static bool registered;

public static IEnumerable<FileInfo> EnumerateProjects(FileInfo solutionFile)
Expand Down Expand Up @@ -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}'.");
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.Tye.Core/ProjectServiceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@ public ProjectServiceBuilder(string name, FileInfo projectFile)

// Used when running in a container locally.
public List<VolumeBuilder> Volumes { get; } = new List<VolumeBuilder>();

public Dictionary<string, string> BuildProperties { get; } = new Dictionary<string, string>();
}
}
38 changes: 38 additions & 0 deletions src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down Expand Up @@ -191,6 +198,17 @@ private static void HandleServiceVolumeNameMapping(YamlMappingNode yamlMappingNo
}
}

private static void HandleBuildProperties(YamlSequenceNode yamlSequenceNode, List<BuildProperty> 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<ConfigConfigurationSource> configuration)
{
foreach (var child in yamlSequenceNode.Children)
Expand Down Expand Up @@ -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));
}
}
}
}
}
3 changes: 3 additions & 0 deletions src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,6 +26,8 @@ public ProjectRunInfo(ProjectServiceBuilder project)
PublishOutputPath = project.PublishDir;
}

public Dictionary<string, string> BuildProperties { get; } = new Dictionary<string, string>();

public string? Args { get; }
public bool Build { get; }
public FileInfo ProjectFile { get; }
Expand Down
7 changes: 5 additions & 2 deletions src/Microsoft.Tye.Hosting/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);

Expand Down
6 changes: 5 additions & 1 deletion src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

namespace Microsoft.Tye.Hosting
{
using System.Linq;

public class TransformProjectsIntoContainers : IApplicationProcessor
{
private readonly ILogger _logger;
Expand Down Expand Up @@ -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}");

Expand Down
3 changes: 2 additions & 1 deletion src/tye/tye.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -7,6 +7,7 @@
<AssemblyName>tye</AssemblyName>
<PackageId>Microsoft.Tye</PackageId>
<ToolCommandName>tye</ToolCommandName>
<PackageVersion>0.2.0-dev-1</PackageVersion>
<PackAsTool>true</PackAsTool>
</PropertyGroup>

Expand Down
2 changes: 2 additions & 0 deletions test/E2ETest/E2ETest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
<None Remove="testassets\generate\single-project.yaml" />
<None Remove="testassets\init\frontend-backend.yaml" />
<None Remove="testassets\init\multi-project.yaml" />
<None Remove="testassets\projects\frontend-backend\tye-debug-configuration.yaml" />
<None Remove="testassets\projects\frontend-backend\tye-release-configuration.yaml" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions test/E2ETest/Infrastructure/ConditionalTheoryAttribute.cs
Original file line number Diff line number Diff line change
@@ -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
{
}
}
41 changes: 41 additions & 0 deletions test/E2ETest/Infrastructure/ConditionalTheoryDiscoverer.cs
Original file line number Diff line number Diff line change
@@ -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<IXunitTestCase> 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);
}
}
}
42 changes: 42 additions & 0 deletions test/E2ETest/TyeRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit bce8817

Please sign in to comment.