Browse Source

Added tags to yaml definition to be used in CLI run filter (#528)

* fixed unit test: Services_UnrecognizedKey

* TyeAssert - duplicated check removed

* YAML service.tags added to parser and unit tests

* filter services by tags - run argument + ApplicationFactory filter

* filter ingress by tags

* Make tags work for all scenarios

Co-authored-by: Justin Kotalik <jukotali@microsoft.com>
pull/550/head
Krzysztof Koziarski 6 years ago
committed by GitHub
parent
commit
c9561a408f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      src/Microsoft.Tye.Core/ApplicationFactory.cs
  2. 32
      src/Microsoft.Tye.Core/ApplicationFactoryFilter.cs
  3. 1
      src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs
  4. 2
      src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs
  5. 17
      src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs
  6. 19
      src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs
  7. 2
      src/Microsoft.Tye.Core/Serialization/YamlParser.cs
  8. 16
      src/Microsoft.Tye.Core/StandardOptions.cs
  9. 5
      src/tye/BuildHost.cs
  10. 5
      src/tye/GenerateHost.cs
  11. 5
      src/tye/Program.BuildCommand.cs
  12. 11
      src/tye/Program.DeployCommand.cs
  13. 5
      src/tye/Program.GenerateCommand.cs
  14. 7
      src/tye/Program.PushCommand.cs
  15. 16
      src/tye/Program.RunCommand.cs
  16. 5
      src/tye/Program.UndeployCommand.cs
  17. 8
      src/tye/UndeployHost.cs
  18. 16
      test/Test.Infrastructure/TyeAssert.cs
  19. 97
      test/UnitTests/TyeDeserializationTests.cs

15
src/Microsoft.Tye.Core/ApplicationFactory.cs

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@ -15,7 +14,7 @@ namespace Microsoft.Tye
{
public static class ApplicationFactory
{
public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source)
public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source, ApplicationFactoryFilter? filter = null)
{
if (source is null)
{
@ -71,7 +70,11 @@ namespace Microsoft.Tye
root.Extensions.Add(extension);
}
foreach (var configService in config.Services)
var services = filter?.ServicesFilter != null ?
config.Services.Where(filter.ServicesFilter).ToList() :
config.Services;
foreach (var configService in services)
{
ServiceBuilder service;
if (root.Services.Any(s => s.Name == configService.Name))
@ -320,7 +323,11 @@ namespace Microsoft.Tye
}
}
foreach (var configIngress in config.Ingress)
var ingresses = filter?.IngressFilter != null ?
config.Ingress.Where(filter.IngressFilter).ToList() :
config.Ingress;
foreach (var configIngress in ingresses)
{
var ingress = new IngressBuilder(configIngress.Name!);
ingress.Replicas = configIngress.Replicas ?? 1;

32
src/Microsoft.Tye.Core/ApplicationFactoryFilter.cs

@ -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 System.Linq;
using Microsoft.Tye.ConfigModel;
namespace Microsoft.Tye
{
public class ApplicationFactoryFilter
{
public Func<ConfigService, bool>? ServicesFilter { get; set; }
public Func<ConfigIngress, bool>? IngressFilter { get; set; }
public static ApplicationFactoryFilter? GetApplicationFactoryFilter(string[] tags)
{
ApplicationFactoryFilter? filter = null;
if (tags != null && tags.Any())
{
filter = new ApplicationFactoryFilter
{
ServicesFilter = service => tags.Any(b => service.Tags.Contains(b)),
IngressFilter = service => tags.Any(b => service.Tags.Contains(b))
};
}
return filter;
}
}
}

1
src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs

@ -14,5 +14,6 @@ namespace Microsoft.Tye.ConfigModel
public int? Replicas { get; set; }
public List<ConfigIngressRule> Rules { get; set; } = new List<ConfigIngressRule>();
public List<ConfigIngressBinding> Bindings { get; set; } = new List<ConfigIngressBinding>();
public List<string> Tags { get; set; } = new List<string>();
}
}

2
src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs

@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Tye;
using YamlDotNet.Serialization;
namespace Microsoft.Tye.ConfigModel
@ -37,6 +36,7 @@ namespace Microsoft.Tye.ConfigModel
[YamlMember(Alias = "env")]
public List<ConfigConfigurationSource> Configuration { get; set; } = new List<ConfigConfigurationSource>();
public List<BuildProperty> BuildProperties { get; set; } = new List<BuildProperty>();
public List<string> Tags { get; set; } = new List<string>();
public ConfigProbe? Liveness { get; set; }
public ConfigProbe? Readiness { get; set; }
}

17
src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs

@ -59,6 +59,14 @@ namespace Tye.Serialization
}
HandleIngressBindings((child.Value as YamlSequenceNode)!, configIngress.Bindings);
break;
case "tags":
if (child.Value.NodeType != YamlNodeType.Sequence)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
}
HandleIngressTags((child.Value as YamlSequenceNode)!, configIngress.Tags);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
@ -137,5 +145,14 @@ namespace Tye.Serialization
}
}
}
private static void HandleIngressTags(YamlSequenceNode yamlSequenceNode, List<string> tags)
{
foreach (var child in yamlSequenceNode!.Children)
{
var tag = YamlParser.GetScalarValue(child);
tags.Add(tag);
}
}
}
}

19
src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs

@ -3,8 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.Build.Evaluation;
using Microsoft.Tye.ConfigModel;
using YamlDotNet.RepresentationModel;
@ -131,6 +129,14 @@ namespace Tye.Serialization
service.Readiness = new ConfigProbe();
HandleServiceProbe((YamlMappingNode)child.Value, service.Readiness!);
break;
case "tags":
if (child.Value.NodeType != YamlNodeType.Sequence)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
}
HandleServiceTags((child.Value as YamlSequenceNode)!, service.Tags);
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}
@ -444,5 +450,14 @@ namespace Tye.Serialization
}
}
}
private static void HandleServiceTags(YamlSequenceNode yamlSequenceNode, List<string> tags)
{
foreach (var child in yamlSequenceNode!.Children)
{
var tag = YamlParser.GetScalarValue(child);
tags.Add(tag);
}
}
}
}

2
src/Microsoft.Tye.Core/Serialization/YamlParser.cs

@ -62,12 +62,14 @@ namespace Tye.Serialization
service.Bindings ??= new List<ConfigServiceBinding>();
service.Configuration ??= new List<ConfigConfigurationSource>();
service.Volumes ??= new List<ConfigVolume>();
service.Tags ??= new List<string>();
}
foreach (var ingress in app.Ingress)
{
ingress.Bindings ??= new List<ConfigIngressBinding>();
ingress.Rules ??= new List<ConfigIngressRule>();
ingress.Tags ??= new List<string>();
}
return app;

16
src/Microsoft.Tye.Core/StandardOptions.cs

@ -30,6 +30,22 @@ namespace Microsoft.Tye
}
}
public static Option Tags
{
get
{
return new Option("--tags", "--filter")
{
Argument = new Argument<List<string>>("tags")
{
Arity = ArgumentArity.OneOrMore
},
Description = "Filter the group of running services by tag.",
Required = false
};
}
}
public static Option Force
{
get

5
src/tye/BuildHost.cs

@ -11,10 +11,11 @@ namespace Microsoft.Tye
{
public static class BuildHost
{
public static async Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive)
public static async Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive, string[] tags)
{
var output = new OutputContext(console, verbosity);
var application = await ApplicationFactory.CreateAsync(output, path);
var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags);
var application = await ApplicationFactory.CreateAsync(output, path, filter);
if (application.Services.Count == 0)
{
throw new CommandException($"No services found in \"{application.Source.Name}\"");

5
src/tye/GenerateHost.cs

@ -13,12 +13,13 @@ namespace Microsoft.Tye
{
public static class GenerateHost
{
public static async Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive, string ns)
public static async Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive, string ns, string[] tags)
{
var output = new OutputContext(console, verbosity);
output.WriteInfoLine("Loading Application Details...");
var application = await ApplicationFactory.CreateAsync(output, path);
var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags);
var application = await ApplicationFactory.CreateAsync(output, path, filter);
if (application.Services.Count == 0)
{
throw new CommandException($"No services found in \"{application.Source.Name}\"");

5
src/tye/Program.BuildCommand.cs

@ -21,9 +21,10 @@ namespace Microsoft.Tye
CommonArguments.Path_Required,
StandardOptions.Interactive,
StandardOptions.Verbosity,
StandardOptions.Tags
};
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool>((console, path, verbosity, interactive) =>
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, string[]>((console, path, verbosity, interactive, tags) =>
{
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
if (path is null)
@ -31,7 +32,7 @@ namespace Microsoft.Tye
throw new CommandException("No project or solution file was found.");
}
return BuildHost.BuildAsync(console, path, verbosity, interactive);
return BuildHost.BuildAsync(console, path, verbosity, interactive, tags);
});
return command;

11
src/tye/Program.DeployCommand.cs

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Tye.ConfigModel;
@ -23,6 +24,7 @@ namespace Microsoft.Tye
StandardOptions.Interactive,
StandardOptions.Verbosity,
StandardOptions.Namespace,
StandardOptions.Tags,
};
command.AddOption(new Option(new[] { "-f", "--force" })
@ -31,7 +33,7 @@ namespace Microsoft.Tye
Required = false
});
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, bool, string>(async (console, path, verbosity, interactive, force, @namespace) =>
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, bool, string, string[]>(async (console, path, verbosity, interactive, force, @namespace, tags) =>
{
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
if (path is null)
@ -42,12 +44,15 @@ namespace Microsoft.Tye
var output = new OutputContext(console, verbosity);
output.WriteInfoLine("Loading Application Details...");
var application = await ApplicationFactory.CreateAsync(output, path);
var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags);
var application = await ApplicationFactory.CreateAsync(output, path, filter);
if (application.Services.Count == 0)
{
throw new CommandException($"No services found in \"{application.Source.Name}\"");
}
if (!String.IsNullOrEmpty(@namespace))
if (!string.IsNullOrEmpty(@namespace))
{
application.Namespace = @namespace;
}

5
src/tye/Program.GenerateCommand.cs

@ -22,13 +22,14 @@ namespace Microsoft.Tye
StandardOptions.Interactive,
StandardOptions.Verbosity,
StandardOptions.Namespace,
StandardOptions.Tags
};
// This is a super-secret VIP-only command! It's useful for testing, but we're
// not documenting it right now.
command.IsHidden = true;
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, string>((console, path, verbosity, interactive, @namespace) =>
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, string, string[]>((console, path, verbosity, interactive, @namespace, tags) =>
{
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
if (path is null)
@ -36,7 +37,7 @@ namespace Microsoft.Tye
throw new CommandException("No project or solution file was found.");
}
return GenerateHost.GenerateAsync(console, path, verbosity, interactive, @namespace);
return GenerateHost.GenerateAsync(console, path, verbosity, interactive, @namespace, tags);
});
return command;

7
src/tye/Program.PushCommand.cs

@ -18,6 +18,7 @@ namespace Microsoft.Tye
CommonArguments.Path_Required,
StandardOptions.Interactive,
StandardOptions.Verbosity,
StandardOptions.Tags
};
command.AddOption(new Option(new[] { "-f", "--force" })
@ -26,7 +27,7 @@ namespace Microsoft.Tye
Required = false
});
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, bool>(async (console, path, verbosity, interactive, force) =>
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, bool, string[]>(async (console, path, verbosity, interactive, force, tags) =>
{
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
if (path is null)
@ -37,7 +38,9 @@ namespace Microsoft.Tye
var output = new OutputContext(console, verbosity);
output.WriteInfoLine("Loading Application Details...");
var application = await ApplicationFactory.CreateAsync(output, path);
var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags);
var application = await ApplicationFactory.CreateAsync(output, path, filter);
if (application.Services.Count == 0)
{
throw new CommandException($"No services found in \"{application.Source.Name}\"");

16
src/tye/Program.RunCommand.cs

@ -3,15 +3,13 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.Tye.ConfigModel;
using Microsoft.Tye.Extensions;
using Microsoft.Tye.Hosting;
using Microsoft.Tye.Hosting.Diagnostics;
namespace Microsoft.Tye
{
@ -76,8 +74,7 @@ namespace Microsoft.Tye
Description = "Watches for code changes for all dotnet projects.",
Required = false
},
StandardOptions.Tags,
StandardOptions.Verbosity,
};
@ -92,7 +89,10 @@ namespace Microsoft.Tye
var output = new OutputContext(args.Console, args.Verbosity);
output.WriteInfoLine("Loading Application Details...");
var application = await ApplicationFactory.CreateAsync(output, args.Path);
var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(args.Tags);
var application = await ApplicationFactory.CreateAsync(output, args.Path, filter);
if (application.Services.Count == 0)
{
throw new CommandException($"No services found in \"{application.Source.Name}\"");
@ -170,6 +170,8 @@ namespace Microsoft.Tye
public Verbosity Verbosity { get; set; }
public bool Watch { get; set; }
public string[] Tags { get; set; } = Array.Empty<string>();
}
}
}

5
src/tye/Program.UndeployCommand.cs

@ -22,6 +22,7 @@ namespace Microsoft.Tye
StandardOptions.Namespace,
StandardOptions.Interactive,
StandardOptions.Verbosity,
StandardOptions.Tags,
new Option(new[]{ "--what-if", }, "print what would be deleted without making changes")
{
@ -29,7 +30,7 @@ namespace Microsoft.Tye
},
};
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, string, bool, bool>((console, path, verbosity, @namespace, interactive, whatIf) =>
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, string, bool, bool, string[]>((console, path, verbosity, @namespace, interactive, whatIf, tags) =>
{
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
if (path is null)
@ -37,7 +38,7 @@ namespace Microsoft.Tye
throw new CommandException("No project or solution file was found.");
}
return UndeployHost.UndeployAsync(console, path, verbosity, @namespace, interactive, whatIf);
return UndeployHost.UndeployAsync(console, path, verbosity, @namespace, interactive, whatIf, tags);
});
return command;

8
src/tye/UndeployHost.cs

@ -17,7 +17,7 @@ namespace Microsoft.Tye
{
public static class UndeployHost
{
public static async Task UndeployAsync(IConsole console, FileInfo path, Verbosity verbosity, string @namespace, bool interactive, bool whatIf)
public static async Task UndeployAsync(IConsole console, FileInfo path, Verbosity verbosity, string @namespace, bool interactive, bool whatIf, string[] tags)
{
var watch = System.Diagnostics.Stopwatch.StartNew();
@ -25,8 +25,8 @@ namespace Microsoft.Tye
output.WriteInfoLine("Loading Application Details...");
// We don't need to know anything about the services, just the application name.
var application = ConfigFactory.FromFile(path);
var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags);
var application = await ApplicationFactory.CreateAsync(output, path, filter);
if (!string.IsNullOrEmpty(@namespace))
{
application.Namespace = @namespace;
@ -41,7 +41,7 @@ namespace Microsoft.Tye
output.WriteAlwaysLine($"Time Elapsed: {elapsedTime.Hours:00}:{elapsedTime.Minutes:00}:{elapsedTime.Seconds:00}:{elapsedTime.Milliseconds / 10:00}");
}
public static async Task ExecuteUndeployAsync(OutputContext output, ConfigApplication application, string @namespace, bool interactive, bool whatIf)
public static async Task ExecuteUndeployAsync(OutputContext output, ApplicationBuilder application, string @namespace, bool interactive, bool whatIf)
{
var config = KubernetesClientConfiguration.BuildDefaultConfig();

16
test/Test.Infrastructure/TyeAssert.cs

@ -44,6 +44,7 @@ namespace Test.Infrastructure
Assert.NotNull(otherBinding);
}
Assert.Equal(otherIngress.Tags, ingress.Tags);
}
foreach (var service in actual.Services)
@ -61,20 +62,7 @@ namespace Test.Infrastructure
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);
}
Assert.Equal(otherService.Tags, service.Tags);
foreach (var binding in service.Bindings)
{

97
test/UnitTests/TyeDeserializationTests.cs

@ -46,6 +46,9 @@ ingress:
- host: b.example.com
service: appB
replicas: 2
tags:
- tagA
- tagC
services:
- name: appA
project: ApplicationA/ApplicationA.csproj
@ -53,6 +56,9 @@ services:
- name: Configuration
- value: Debug
replicas: 2
tags:
- tagA
- tagC
external: false
image: abc
build: false
@ -66,8 +72,8 @@ services:
value: ""test2""
volumes:
- name: volume
source: /data
target: /data
source: /data
target: /data
bindings:
- name: test
port: 4444
@ -77,7 +83,10 @@ services:
protocol: http
- name: appB
project: ApplicationB/ApplicationB.csproj
replicas: 2";
replicas: 2
tags:
- tagB
- tagD";
using var parser = new YamlParser(input);
var app = parser.ParseConfigApplication();
@ -115,10 +124,14 @@ ingress:
[Fact]
public void ServicesSetCorrectly()
{
var input = @"services:
var input = @"
services:
- name: appA
project: ApplicationA/ApplicationA.csproj
replicas: 2
tags:
- A
- B
external: false
image: abc
build: false
@ -132,8 +145,8 @@ ingress:
value: ""test2""
volumes:
- name: volume
source: /data
target: /data
source: /data
target: /data
bindings:
- name: test
port: 4444
@ -143,7 +156,10 @@ ingress:
protocol: http
- name: appB
project: ApplicationB/ApplicationB.csproj
replicas: 2";
replicas: 2
tags:
- tC
- tD";
using var parser = new YamlParser(input);
var actual = parser.ParseConfigApplication();
@ -436,6 +452,38 @@ services:
Assert.Contains(CoreStrings.FormatUnrecognizedKey("abc"), exception.Message);
}
[Fact]
public void Ingress_Tags_MustBeSequence()
{
using var parser = new YamlParser(
@"ingress:
- name: ingress
tags: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("tags"), exception.Message);
}
[Fact]
public void Ingress_Tags_SetCorrectly()
{
var input = @"
ingress:
- name: ingress
tags:
- tagA
- with space
- ""C.X""
";
using var parser = new YamlParser(input);
var actual = parser.ParseConfigApplication();
var expected = _deserializer.Deserialize<ConfigApplication>(new StringReader(input));
TyeAssert.Equal(expected, actual);
}
[Fact]
public void Services_External_MustBeBool()
{
@ -497,16 +545,47 @@ services:
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("env"), exception.Message);
}
[Fact]
public void Services_Tags_MustBeSequence()
{
using var parser = new YamlParser(
@"services:
- name: ingress
tags: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("tags"), exception.Message);
}
[Fact]
public void Services_Tags_SetCorrectly()
{
var input = @"
services:
- name: ingress
tags:
- tagA
- with space
- ""C.X""
";
using var parser = new YamlParser(input);
var actual = parser.ParseConfigApplication();
var expected = _deserializer.Deserialize<ConfigApplication>(new StringReader(input));
TyeAssert.Equal(expected, actual);
}
[Fact]
public void Services_UnrecognizedKey()
{
using var parser = new YamlParser(
@"services:
- name: ingress
env: abc");
xyz: abc");
var exception = Assert.Throws<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatExpectedYamlSequence("env"), exception.Message);
Assert.Contains(CoreStrings.FormatUnrecognizedKey("xyz"), exception.Message);
}
[Theory]

Loading…
Cancel
Save