Browse Source

Add tye build and tests thereof

pull/96/head
Ryan Nowak 6 years ago
parent
commit
4181240096
  1. 39
      src/tye/BuildHost.cs
  2. 4
      src/tye/GenerateHost.cs
  3. 40
      src/tye/Program.BuildCommand.cs
  4. 19
      src/tye/Program.DeployCommand.cs
  5. 2
      src/tye/Program.GenerateCommand.cs
  6. 1
      src/tye/Program.cs
  7. 53
      test/E2ETest/DockerAssert.cs
  8. 8
      test/E2ETest/Properties/AssemblyInfo.cs
  9. 6
      test/E2ETest/TestHelpers.cs
  10. 115
      test/E2ETest/TyeBuildTests.cs
  11. 24
      test/E2ETest/TyeGenerateTests.cs
  12. 51
      test/E2ETest/testassets/generate/single-project-noregistry.yaml

39
src/tye/BuildHost.cs

@ -0,0 +1,39 @@
// 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 System.CommandLine;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Tye.ConfigModel;
namespace Tye
{
public static class BuildHost
{
public static Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive)
{
var application = ConfigFactory.FromFile(path);
return ExecuteBuildAsync(new OutputContext(console, verbosity), application, environment: "production", interactive);
}
public static async Task ExecuteBuildAsync(OutputContext output, ConfigApplication application, string environment, bool interactive)
{
var temporaryApplication = await Program.CreateApplicationAdapterAsync(output, application, interactive, requireRegistry: false);
var steps = new List<ServiceExecutor.Step>()
{
new CombineStep() { Environment = environment, },
new PublishProjectStep(),
new BuildDockerImageStep() { Environment = environment, },
};
var executor = new ServiceExecutor(output, temporaryApplication, steps);
foreach (var service in temporaryApplication.Services)
{
await executor.ExecuteAsync(service);
}
}
}
}

4
src/tye/GenerateHost.cs

@ -11,7 +11,7 @@ using Tye.ConfigModel;
namespace Tye
{
public class GenerateHost
public static class GenerateHost
{
public static Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive)
{
@ -21,7 +21,7 @@ namespace Tye
public static async Task ExecuteGenerateAsync(OutputContext output, ConfigApplication application, string environment, bool interactive)
{
var temporaryApplication = await Program.CreateApplicationAdapterAsync(output, application, interactive);
var temporaryApplication = await Program.CreateApplicationAdapterAsync(output, application, interactive, requireRegistry: false);
var steps = new List<ServiceExecutor.Step>()
{
new CombineStep() { Environment = environment, },

40
src/tye/Program.BuildCommand.cs

@ -0,0 +1,40 @@
// 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 System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Tye.ConfigModel;
namespace Tye
{
static partial class Program
{
public static Command CreateBuildCommand()
{
var command = new Command("build", "build container for the application")
{
CommonArguments.Path_Required,
StandardOptions.Interactive,
StandardOptions.Verbosity,
};
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool>((console, path, verbosity, interactive) =>
{
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
if (path is null)
{
throw new CommandException("No project or solution file was found.");
}
return BuildHost.BuildAsync(console, path, verbosity, interactive);
});
return command;
}
}
}

19
src/tye/Program.DeployCommand.cs

@ -16,7 +16,7 @@ namespace Tye
{
public static Command CreateDeployCommand()
{
var command = new Command("deploy", "Deploy the application")
var command = new Command("deploy", "deploy the application")
{
CommonArguments.Path_Required,
StandardOptions.Interactive,
@ -56,7 +56,7 @@ namespace Tye
throw new CommandException($"Cannot apply manifests because kubectl is not connected to a cluster.");
}
var temporaryApplication = await CreateApplicationAdapterAsync(output, application, interactive);
var temporaryApplication = await CreateApplicationAdapterAsync(output, application, interactive, requireRegistry: true);
var steps = new List<ServiceExecutor.Step>()
{
new CombineStep() { Environment = environment, },
@ -78,7 +78,7 @@ namespace Tye
await DeployApplicationManifestAsync(output, temporaryApplication, application.Source.Directory.Name, environment);
}
internal static async Task<TemporaryApplicationAdapter> CreateApplicationAdapterAsync(OutputContext output, ConfigApplication application, bool interactive)
internal static async Task<TemporaryApplicationAdapter> CreateApplicationAdapterAsync(OutputContext output, ConfigApplication application, bool interactive, bool requireRegistry)
{
var globals = new ApplicationGlobals()
{
@ -145,13 +145,20 @@ namespace Tye
var temporaryApplication = new TemporaryApplicationAdapter(application, globals, services);
if (temporaryApplication.Globals.Registry?.Hostname == null && interactive)
{
var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)");
temporaryApplication.Globals.Registry = new ContainerRegistry(registry);
var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)", allowEmpty: !requireRegistry);
if (!string.IsNullOrWhiteSpace(registry))
{
temporaryApplication.Globals.Registry = new ContainerRegistry(registry.Trim());
}
}
else if (temporaryApplication.Globals.Registry?.Hostname == null)
else if (temporaryApplication.Globals.Registry?.Hostname == null && requireRegistry)
{
throw new CommandException("A registry is required for deploy operations. Add the registry to 'tye.yaml' or use '-i' for interactive mode.");
}
else
{
// No registry specified, and that's OK!
}
foreach (var service in temporaryApplication.Services)
{

2
src/tye/Program.GenerateCommand.cs

@ -16,7 +16,7 @@ namespace Tye
{
public static Command CreateGenerateCommand()
{
var command = new Command("generate", "Generate kubernetes manifests")
var command = new Command("generate", "generate kubernetes manifests")
{
CommonArguments.Path_Required,
StandardOptions.Interactive,

1
src/tye/Program.cs

@ -26,6 +26,7 @@ namespace Tye
command.AddCommand(CreateInitCommand());
command.AddCommand(CreateGenerateCommand());
command.AddCommand(CreateRunCommand(args));
command.AddCommand(CreateBuildCommand());
command.AddCommand(CreateDeployCommand());
// Show commandline help unless a subcommand was used.

53
test/E2ETest/DockerAssert.cs

@ -0,0 +1,53 @@
// 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.CommandLine.Invocation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace E2ETest
{
public static class DockerAssert
{
// Repository is the "registry/image" format. Yeah Docker uses that term for it, and it's
// wierd and confusing.
public static async Task AssertImageExistsAsync(ITestOutputHelper output, string repository)
{
if (repository is null)
{
throw new ArgumentNullException(nameof(repository));
}
var builder = new StringBuilder();
var exitCode = await Process.ExecuteAsync(
"docker",
$"images \"{repository}\" --format \"{{{{.Repository}}}}\"",
stdOut: OnOutput,
stdErr: OnOutput);
if (exitCode != 0)
{
throw new XunitException($"Running `docker images \"{repository}\"` failed." + Environment.NewLine + builder.ToString());
}
var lines = builder.ToString().Split(new[] { '\r', '\n', });
if (lines.Any(line => line == repository))
{
return;
}
throw new XunitException($"Image '{repository}' was not found.");
void OnOutput(string text)
{
builder.AppendLine(text);
output.WriteLine(text);
}
}
}
}

8
test/E2ETest/Properties/AssemblyInfo.cs

@ -0,0 +1,8 @@
// 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 Xunit;
// We have numerous tests that manipulate machine-wide state (docker, ports, etc)
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)]

6
test/E2ETest/TestHelpers.cs

@ -1,4 +1,8 @@
using System;
// 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 System.Text;

115
test/E2ETest/TyeBuildTests.cs

@ -0,0 +1,115 @@
// 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.IO;
using System.Threading.Tasks;
using Tye;
using Tye.ConfigModel;
using Xunit;
using Xunit.Abstractions;
namespace E2ETest
{
public class TyeBuildTests
{
private readonly ITestOutputHelper output;
private readonly TestOutputLogEventSink sink;
public TyeBuildTests(ITestOutputHelper output)
{
this.output = output;
sink = new TestOutputLogEventSink(output);
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task SingleProjectBuildTest()
{
var projectName = "single-project";
var environment = "production";
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", projectName));
using var tempDirectory = TempDirectory.Create();
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
application.Registry = "test";
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test/test-project");
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task FrontendBackendBuildTest()
{
var projectName = "frontend-backend";
var environment = "production";
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", projectName));
using var tempDirectory = TempDirectory.Create();
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
application.Registry = "test";
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test/backend");
await DockerAssert.AssertImageExistsAsync(output, "test/frontend");
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task MultipleProjectBuildTest()
{
var projectName = "multi-project";
var environment = "production";
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", projectName));
using var tempDirectory = TempDirectory.Create();
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
application.Registry = "test";
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test/backend");
await DockerAssert.AssertImageExistsAsync(output, "test/frontend");
await DockerAssert.AssertImageExistsAsync(output, "test/worker");
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task BuildDoesNotRequireRegistry()
{
var projectName = "single-project";
var environment = "production";
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", projectName));
using var tempDirectory = TempDirectory.Create();
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test-project");
}
}
}

24
test/E2ETest/TyeGenerateTests.cs

@ -102,5 +102,29 @@ namespace E2ETest
Assert.Equal(expectedContent, content);
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task GenerateWorksWithoutRegistry()
{
var projectName = "single-project";
var environment = "production";
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", projectName));
using var tempDirectory = TempDirectory.Create();
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
// name of application is the folder
var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml"));
var expectedContent = File.ReadAllText($"testassets/generate/{projectName}-noregistry.yaml");
Assert.Equal(expectedContent, content);
}
}
}

51
test/E2ETest/testassets/generate/single-project-noregistry.yaml

@ -0,0 +1,51 @@
kind: Deployment
apiVersion: apps/v1
metadata:
name: test-project
labels:
app.kubernetes.io/name: test-project
app.kubernetes.io/part-of: single-project
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: test-project
template:
metadata:
labels:
app.kubernetes.io/name: test-project
app.kubernetes.io/part-of: single-project
spec:
containers:
- name: test-project
image: test-project:1.0.0
imagePullPolicy: Always
env:
- name: ASPNETCORE_URLS
value: http://*:5000
ports:
- containerPort: 5001
- containerPort: 5000
...
---
kind: Service
apiVersion: v1
metadata:
name: test-project
labels:
app.kubernetes.io/name: test-project
app.kubernetes.io/part-of: single-project
spec:
selector:
app.kubernetes.io/name: test-project
type: ClusterIP
ports:
- name: test-project
protocol: TCP
port: 5001
targetPort: 5001
- name: test-project
protocol: TCP
port: 5000
targetPort: 5000
...
Loading…
Cancel
Save