diff --git a/docs/reference/schema.md b/docs/reference/schema.md
index 6b700697..c089da69 100644
--- a/docs/reference/schema.md
+++ b/docs/reference/schema.md
@@ -184,10 +184,14 @@ Including `external: true` marks the service as *external*:
External services are useful to provide bindings without any run or deployment behavior.
-#### `env` (`EnvironmentVariable[]`)
+#### `env` (`EnvironmentVariable[] | string[]`)
A list of environment variable mappings for the service. Does not apply when the service is external.
+#### `env_file` (`string[]`)
+
+A list of files from which environment variables are taken. Does not apply when the service is external.
+
#### `args` (string)
Command-line arguments to use when launching the service. Does not apply when the service is external.
@@ -256,6 +260,55 @@ The name of the environment variable.
The value of the environment variable.
+Environment variables can also be provided using a compact syntax (similar to that of [docker-compose](https://docs.docker.com/compose/environment-variables/)).
+
+### Environment Variable Compact Syntax Example
+
+```yaml
+name: myapplication
+services:
+- name: backend
+ project: backend/backend.csproj
+
+ # environment variables appear here
+ env:
+ - SOME_KEY=SOME_VALUE
+ - SOME_KEY2="SOME VALUE"
+ - SOME_KEY3
+```
+
+Using the compact syntax, you provide environment variable name and value via a single string, separated by a `=` sign.
+
+In the absence of an `=` sign, the value of the environment variable will be taken from the operating system/shell.
+
+## Environment Variables Files
+
+`string` elements appear in a list inside the `env_file` property of a `Service`.
+
+These strings reference [`.env` files](https://docs.docker.com/compose/env-file/) from which the environment variables will be injected.
+
+### Environment Variables File Example
+
+```yaml
+name: myapplication
+services:
+- name: backend
+ project: backend/backend.csproj
+
+ # environment variables files appear here
+ env_file:
+ - ./envfile_a.env
+ - ./envfile_b.env
+```
+
+### .env File Example
+
+```
+SOME_KEY=SOME_VALUE
+# This line is ignored because it start with '#'
+SOME_KEY2="SOME VALUE"
+```
+
## Build Properties
Configuration that can be specified when building a project. These will be passed in as MSBuild properties when building a project. It appears in the list `buildProperties` of a `Service`.
diff --git a/src/Microsoft.Tye.Core/CoreStrings.resx b/src/Microsoft.Tye.Core/CoreStrings.resx
index 4d85da23..350964f0 100644
--- a/src/Microsoft.Tye.Core/CoreStrings.resx
+++ b/src/Microsoft.Tye.Core/CoreStrings.resx
@@ -141,12 +141,12 @@
Cannot have multiple {0} bindings with the same port.
-
- A prober must be configured for the {0} probe.
-
-
- "successThreshold" for {0} probe must be set to "1".
-
+
+ A prober must be configured for the {0} probe.
+
+
+ "successThreshold" for {0} probe must be set to "1".
+
"{value}" must be a boolean value (true/false).
@@ -156,9 +156,9 @@
"{value}" value cannot be negative.
-
- "{value}" value must be greater than zero.
-
+
+ "{value}" value must be greater than zero.
+
Cannot have both "{0}" and "{1}" set for a service. Only one of project, image, and executable can be set for a given service.
@@ -171,4 +171,13 @@
Unexpected key "{key}" in tye.yaml.
-
+
+ Unexpected node type in tye.yaml. Expected one of ({expected}) but got "{actual}".
+
+
+ Path "{path}" was not found.
+
+
+ Expected a value for environment variable "{key}".
+
+
\ No newline at end of file
diff --git a/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs b/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs
index 4988d2e1..c9a1038c 100644
--- a/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs
+++ b/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs
@@ -35,7 +35,7 @@ namespace Tye.Serialization
break;
case "services":
YamlParser.ThrowIfNotYamlSequence(key, child.Value);
- ConfigServiceParser.HandleServiceMapping((child.Value as YamlSequenceNode)!, app.Services);
+ ConfigServiceParser.HandleServiceMapping((child.Value as YamlSequenceNode)!, app.Services, app);
break;
case "extensions":
YamlParser.ThrowIfNotYamlSequence(key, child.Value);
diff --git a/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs b/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs
index e719bb9f..014bce75 100644
--- a/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs
+++ b/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs
@@ -2,7 +2,9 @@
// 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 System.Linq;
using Microsoft.Tye.ConfigModel;
using YamlDotNet.RepresentationModel;
@@ -11,18 +13,18 @@ namespace Tye.Serialization
{
public static class ConfigServiceParser
{
- public static void HandleServiceMapping(YamlSequenceNode yamlSequenceNode, List services)
+ public static void HandleServiceMapping(YamlSequenceNode yamlSequenceNode, List services, ConfigApplication application)
{
foreach (var child in yamlSequenceNode.Children)
{
YamlParser.ThrowIfNotYamlMapping(child);
var service = new ConfigService();
- HandleServiceNameMapping((YamlMappingNode)child, service);
+ HandleServiceNameMapping((YamlMappingNode)child, service, application);
services.Add(service);
}
}
- private static void HandleServiceNameMapping(YamlMappingNode yamlMappingNode, ConfigService service)
+ private static void HandleServiceNameMapping(YamlMappingNode yamlMappingNode, ConfigService service, ConfigApplication application)
{
foreach (var child in yamlMappingNode!.Children)
{
@@ -130,6 +132,14 @@ namespace Tye.Serialization
HandleServiceConfiguration((child.Value as YamlSequenceNode)!, service.Configuration);
break;
+ case "env_file":
+ if (child.Value.NodeType != YamlNodeType.Sequence)
+ {
+ throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
+ }
+
+ HandleServiceEnvFiles((child.Value as YamlSequenceNode)!, service.Configuration, application);
+ break;
case "liveness":
service.Liveness = new ConfigProbe();
HandleServiceProbe((YamlMappingNode)child.Value, service.Liveness!);
@@ -430,9 +440,25 @@ namespace Tye.Serialization
{
foreach (var child in yamlSequenceNode.Children)
{
- YamlParser.ThrowIfNotYamlMapping(child);
var config = new ConfigConfigurationSource();
- HandleServiceConfigurationNameMapping((YamlMappingNode)child, config);
+ switch (child)
+ {
+ case YamlMappingNode childMappingNode:
+ HandleServiceConfigurationNameMapping(childMappingNode, config);
+ break;
+ case YamlScalarNode childScalarNode:
+ HandleServiceConfigurationCompact(childScalarNode, config);
+ break;
+ default:
+ throw new TyeYamlException(child.Start, CoreStrings.FormatUnexpectedTypes($"\"{YamlNodeType.Mapping.ToString()}\", \"{YamlNodeType.Scalar.ToString()}\"", child.NodeType.ToString()));
+ }
+
+ // if no value is given, we take the value from the system/shell environment variables
+ if (config.Value == null)
+ {
+ config.Value = Environment.GetEnvironmentVariable(config.Name) ?? string.Empty;
+ }
+
configuration.Add(config);
}
}
@@ -457,6 +483,66 @@ namespace Tye.Serialization
}
}
+ private static void HandleServiceConfigurationCompact(YamlScalarNode yamlScalarNode, ConfigConfigurationSource config)
+ {
+ var nodeValue = YamlParser.GetScalarValue(yamlScalarNode);
+ var keyValueSeparator = nodeValue.IndexOf('=');
+
+ if (keyValueSeparator != -1)
+ {
+ var key = nodeValue.Substring(0, keyValueSeparator).Trim();
+ var value = nodeValue.Substring(keyValueSeparator + 1)?.Trim();
+
+ config.Name = key;
+ config.Value = value?.Trim(new[] { ' ', '"' }) ?? string.Empty;
+ }
+ else
+ {
+ config.Name = nodeValue.Trim();
+ }
+ }
+
+ private static void HandleServiceEnvFiles(YamlSequenceNode yamlSequenceNode, List configuration, ConfigApplication application)
+ {
+ foreach (var child in yamlSequenceNode.Children)
+ {
+ switch (child)
+ {
+ case YamlScalarNode childScalarNode:
+ var envFile = new FileInfo(Path.Combine(application.Source?.DirectoryName ?? Directory.GetCurrentDirectory(), YamlParser.GetScalarValue(childScalarNode)));
+ if (!envFile.Exists)
+ throw new TyeYamlException(child.Start, CoreStrings.FormatPathNotFound(envFile.FullName));
+ HandleServiceEnvFile(childScalarNode, File.ReadAllLines(envFile.FullName), configuration);
+ break;
+ default:
+ throw new TyeYamlException(child.Start, CoreStrings.FormatUnexpectedType(YamlNodeType.Scalar.ToString(), child.NodeType.ToString()));
+ }
+ }
+ }
+
+ private static void HandleServiceEnvFile(YamlScalarNode yamlScalarNode, string[] envLines, List configuration)
+ {
+ foreach (var line in envLines)
+ {
+ var lineTrim = line?.Trim();
+ if (string.IsNullOrEmpty(lineTrim) || lineTrim[0] == '#')
+ {
+ continue;
+ }
+
+ var keyValueSeparator = lineTrim.IndexOf('=');
+
+ if (keyValueSeparator == -1)
+ throw new TyeYamlException(yamlScalarNode.Start, CoreStrings.FormatExpectedEnvironmentVariableValue(lineTrim));
+
+ configuration.Add(new ConfigConfigurationSource
+ {
+ Name = lineTrim.Substring(0, keyValueSeparator).Trim(),
+ Value = lineTrim.Substring(keyValueSeparator + 1)?.Trim(new[] { ' ', '"' }) ?? string.Empty
+ });
+ }
+ }
+
private static void HandleServiceBuildPropertyNameMapping(YamlMappingNode yamlMappingNode, BuildProperty buildProperty)
{
foreach (var child in yamlMappingNode!.Children)
diff --git a/src/Microsoft.Tye.Core/Serialization/YamlParser.cs b/src/Microsoft.Tye.Core/Serialization/YamlParser.cs
index e5754b74..92f38e6d 100644
--- a/src/Microsoft.Tye.Core/Serialization/YamlParser.cs
+++ b/src/Microsoft.Tye.Core/Serialization/YamlParser.cs
@@ -17,21 +17,21 @@ namespace Tye.Serialization
private FileInfo? _fileInfo;
private TextReader _reader;
- public YamlParser(string yamlContent)
- : this(new StringReader(yamlContent))
+ public YamlParser(string yamlContent, FileInfo? fileInfo = null)
+ : this(new StringReader(yamlContent), fileInfo)
{
}
public YamlParser(FileInfo fileInfo)
- : this(fileInfo.OpenText())
+ : this(fileInfo.OpenText(), fileInfo)
{
- _fileInfo = fileInfo;
}
- internal YamlParser(TextReader reader)
+ internal YamlParser(TextReader reader, FileInfo? fileInfo = null)
{
_reader = reader;
_yamlStream = new YamlStream();
+ _fileInfo = fileInfo;
}
public ConfigApplication ParseConfigApplication()
@@ -51,9 +51,11 @@ namespace Tye.Serialization
var document = _yamlStream.Documents[0];
var node = document.RootNode;
ThrowIfNotYamlMapping(node);
- ConfigApplicationParser.HandleConfigApplication((YamlMappingNode)node, app);
app.Source = _fileInfo!;
+
+ ConfigApplicationParser.HandleConfigApplication((YamlMappingNode)node, app);
+
app.Name ??= NameInferer.InferApplicationName(_fileInfo!);
// TODO confirm if these are ever null.
diff --git a/test/UnitTests/Microsoft.Tye.UnitTests.csproj b/test/UnitTests/Microsoft.Tye.UnitTests.csproj
index cda06b56..c526b119 100644
--- a/test/UnitTests/Microsoft.Tye.UnitTests.csproj
+++ b/test/UnitTests/Microsoft.Tye.UnitTests.csproj
@@ -24,4 +24,13 @@
+
+
+
+ Always
+
+
+ Always
+
+
diff --git a/test/UnitTests/TyeDeserializationTests.cs b/test/UnitTests/TyeDeserializationTests.cs
index 82c9c346..c03c1713 100644
--- a/test/UnitTests/TyeDeserializationTests.cs
+++ b/test/UnitTests/TyeDeserializationTests.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System;
+using System.IO;
using System.Linq;
using Microsoft.Tye.ConfigModel;
using Test.Infrastructure;
@@ -228,6 +229,121 @@ services:
var app = parser.ParseConfigApplication();
}
+ [Theory]
+ [InlineData("env")]
+ [InlineData("configuration")]
+ public void EnvSimpleSyntaxTest(string rootKeyName)
+ {
+ using var parser = new YamlParser(
+@$"
+services:
+ - {rootKeyName}:
+ - name: env1
+ value: value1
+ - name: env2
+ value: value2
+ - name: env3
+ value: ""long string""
+ - name: env4
+ value:
+");
+
+ var app = parser.ParseConfigApplication();
+ var serviceConfig = app.Services.First().Configuration;
+
+ Assert.Equal(4, serviceConfig.Count);
+ Assert.Equal("value1", serviceConfig.Where(env => env.Name == "env1").First().Value);
+ Assert.Equal("value2", serviceConfig.Where(env => env.Name == "env2").First().Value);
+ Assert.Equal("long string", serviceConfig.Where(env => env.Name == "env3").First().Value);
+ Assert.Equal(string.Empty, serviceConfig.Where(env => env.Name == "env4").First().Value);
+ }
+
+ [Fact]
+ public void EnvCompactSyntaxTest()
+ {
+ using var parser = new YamlParser(
+@"
+services:
+ - env:
+ - env1=value1
+ - env2=value2
+ - env3 = value3
+ - env4 = ""long string""
+ - name: env5
+ value: value5
+ - env6 =
+");
+
+ var app = parser.ParseConfigApplication();
+ var serviceConfig = app.Services.First().Configuration;
+
+ Assert.Equal(6, serviceConfig.Count);
+ Assert.Equal("value1", serviceConfig.Where(env => env.Name == "env1").First().Value);
+ Assert.Equal("value2", serviceConfig.Where(env => env.Name == "env2").First().Value);
+ Assert.Equal("value3", serviceConfig.Where(env => env.Name == "env3").First().Value);
+ Assert.Equal("long string", serviceConfig.Where(env => env.Name == "env4").First().Value);
+ Assert.Equal("value5", serviceConfig.Where(env => env.Name == "env5").First().Value);
+ Assert.Equal(string.Empty, serviceConfig.Where(env => env.Name == "env6").First().Value);
+ }
+
+ [Fact]
+ public void EnvTakeValueFromEnvironmentTest()
+ {
+ using var parser = new YamlParser(
+@"
+services:
+ - env:
+ - env1
+ - name: env2
+ - env3
+");
+
+ Environment.SetEnvironmentVariable("env1", "value1");
+ Environment.SetEnvironmentVariable("env2", "value2");
+
+ var app = parser.ParseConfigApplication();
+ var serviceConfig = app.Services.First().Configuration;
+
+ Assert.Equal(3, serviceConfig.Count);
+ Assert.Equal("value1", serviceConfig.Where(env => env.Name == "env1").First().Value);
+ Assert.Equal("value2", serviceConfig.Where(env => env.Name == "env2").First().Value);
+ Assert.Equal(string.Empty, serviceConfig.Where(env => env.Name == "env3").First().Value);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void EnvFromFile(bool setWorkingDirectory)
+ {
+ var subDirectory = setWorkingDirectory ? string.Empty : "testassets/";
+ using var parser = new YamlParser(
+@$"
+services:
+ - env_file:
+ - ./{subDirectory}envfile_a.env
+ - ./{subDirectory}envfile_b.env
+", setWorkingDirectory ? new FileInfo(Path.Join(Directory.GetCurrentDirectory(), "testassets", "tye.yaml")) : null);
+
+
+ var app = parser.ParseConfigApplication();
+ var serviceConfig = app.Services.First().Configuration;
+
+ Assert.Equal(3, serviceConfig.Count);
+ }
+
+ [Fact]
+ public void PathNotFound_ThrowException()
+ {
+ using var parser = new YamlParser(
+@"
+services:
+ - env_file:
+ - ./envfile_c.env
+", new FileInfo(Path.Join(Directory.GetCurrentDirectory(), "testassets", "tye.yaml")));
+
+ var exception = Assert.Throws(() => parser.ParseConfigApplication());
+ Assert.Contains(CoreStrings.FormatPathNotFound(Path.Join(Directory.GetCurrentDirectory(), "testassets", "envfile_c.env")), exception.Message);
+ }
[Fact]
public void UnrecognizedConfigApplicationField_ThrowException()
diff --git a/test/UnitTests/testassets/envfile_a.env b/test/UnitTests/testassets/envfile_a.env
new file mode 100644
index 00000000..38549f51
--- /dev/null
+++ b/test/UnitTests/testassets/envfile_a.env
@@ -0,0 +1,3 @@
+env1=value1
+# Ignore comment
+env2 = value2
\ No newline at end of file
diff --git a/test/UnitTests/testassets/envfile_b.env b/test/UnitTests/testassets/envfile_b.env
new file mode 100644
index 00000000..599cef5f
--- /dev/null
+++ b/test/UnitTests/testassets/envfile_b.env
@@ -0,0 +1 @@
+env3 = "long string"
\ No newline at end of file