Browse Source

Manifest generation and deployment for Ingress

pull/412/head
Ryan Nowak 6 years ago
parent
commit
43a47944a9
  1. 23
      src/Microsoft.Tye.Core/ApplicationExecutor.cs
  2. 9
      src/Microsoft.Tye.Core/ApplicationYamlWriter.cs
  3. 21
      src/Microsoft.Tye.Core/GenerateIngressKubernetesManifestStep.cs
  4. 3
      src/Microsoft.Tye.Core/GenerateServiceKubernetesManifestStep.cs
  5. 2
      src/Microsoft.Tye.Core/IngressBuilder.cs
  6. 10
      src/Microsoft.Tye.Core/IngressOutput.cs
  7. 31
      src/Microsoft.Tye.Core/KubernetesIngressOutput.cs
  8. 97
      src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs
  9. 7
      src/tye/GenerateHost.cs
  10. 7
      src/tye/Program.DeployCommand.cs
  11. 37
      test/E2ETest/TyeGenerateTests.cs
  12. 146
      test/E2ETest/testassets/generate/apps-with-ingress.yaml

23
src/Microsoft.Tye.Core/ApplicationExecutor.cs

@ -19,7 +19,9 @@ namespace Microsoft.Tye
this.output = output;
}
public List<ApplicationStep> ApplicationSteps = new List<ApplicationStep>();
public List<ApplicationStep> ApplicationSteps { get; } = new List<ApplicationStep>();
public List<IngressStep> IngressSteps { get; } = new List<IngressStep>();
public List<ServiceStep> ServiceSteps { get; } = new List<ServiceStep>();
@ -37,6 +39,18 @@ namespace Microsoft.Tye
tracker.MarkComplete();
}
foreach (var ingress in application.Ingress)
{
using var tracker = output.BeginStep($"Processing Ingress '{ingress.Name}'...");
foreach (var step in IngressSteps)
{
using var stepTracker = output.BeginStep(step.DisplayText);
await step.ExecuteAsync(output, application, ingress);
stepTracker.MarkComplete();
}
tracker.MarkComplete();
}
{
foreach (var step in ApplicationSteps)
{
@ -54,6 +68,13 @@ namespace Microsoft.Tye
public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application);
}
public abstract class IngressStep
{
public abstract string DisplayText { get; }
public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingres);
}
public abstract class ServiceStep
{
public abstract string DisplayText { get; }

9
src/Microsoft.Tye.Core/ApplicationYamlWriter.cs

@ -2,6 +2,7 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
@ -13,8 +14,12 @@ namespace Microsoft.Tye
{
public static Task WriteAsync(OutputContext output, StreamWriter writer, ApplicationBuilder application)
{
var yaml = application.Services.SelectMany(s => s.Outputs.OfType<IYamlManifestOutput>()).ToArray();
if (yaml.Length == 0)
var yaml = new List<IYamlManifestOutput>();
yaml.AddRange(application.Services.SelectMany(s => s.Outputs.OfType<IYamlManifestOutput>()));
yaml.AddRange(application.Ingress.SelectMany(i => i.Outputs.OfType<IYamlManifestOutput>()));
if (yaml.Count == 0)
{
output.WriteDebugLine($"No yaml manifests found. Skipping.");
return Task.CompletedTask;

21
src/Microsoft.Tye.Core/GenerateIngressKubernetesManifestStep.cs

@ -0,0 +1,21 @@
// 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.Threading.Tasks;
namespace Microsoft.Tye
{
public sealed class GenerateIngressKubernetesManifestStep : ApplicationExecutor.IngressStep
{
public override string DisplayText => "Generating Manifests...";
public string Environment { get; set; } = "production";
public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingress)
{
ingress.Outputs.Add(KubernetesManifestGenerator.CreateIngress(output, application, ingress));
return Task.CompletedTask;
}
}
}

3
src/Microsoft.Tye.Core/GenerateKubernetesManifestStep.cs → src/Microsoft.Tye.Core/GenerateServiceKubernetesManifestStep.cs

@ -6,12 +6,13 @@ using System.Threading.Tasks;
namespace Microsoft.Tye
{
public sealed class GenerateKubernetesManifestStep : ApplicationExecutor.ServiceStep
public sealed class GenerateServiceKubernetesManifestStep : ApplicationExecutor.ServiceStep
{
public override string DisplayText => "Generating Manifests...";
public string Environment { get; set; } = "production";
public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
if (SkipWithoutContainerOutput(output, service))

2
src/Microsoft.Tye.Core/IngressBuilder.cs

@ -20,5 +20,7 @@ namespace Microsoft.Tye
public List<IngressBindingBuilder> Bindings { get; set; } = new List<IngressBindingBuilder>();
public List<IngressRuleBuilder> Rules { get; set; } = new List<IngressRuleBuilder>();
public List<IngressOutput> Outputs { get; } = new List<IngressOutput>();
}
}

10
src/Microsoft.Tye.Core/IngressOutput.cs

@ -0,0 +1,10 @@
// 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.
namespace Microsoft.Tye
{
public abstract class IngressOutput
{
}
}

31
src/Microsoft.Tye.Core/KubernetesIngressOutput.cs

@ -0,0 +1,31 @@
// 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 YamlDotNet.RepresentationModel;
namespace Microsoft.Tye
{
internal sealed class KubernetesIngressOutput : IngressOutput, IYamlManifestOutput
{
public KubernetesIngressOutput(string name, YamlDocument yaml)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
if (yaml is null)
{
throw new ArgumentNullException(nameof(yaml));
}
Name = name;
Yaml = yaml;
}
public string Name { get; }
public YamlDocument Yaml { get; }
}
}

97
src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs

@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Linq;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
@ -11,7 +12,99 @@ namespace Microsoft.Tye
{
internal static class KubernetesManifestGenerator
{
public static ServiceOutput CreateService(
public static KubernetesIngressOutput CreateIngress(
OutputContext output,
ApplicationBuilder application,
IngressBuilder ingress)
{
var root = new YamlMappingNode();
root.Add("kind", "Ingress");
root.Add("apiVersion", "extensions/v1beta1");
var metadata = new YamlMappingNode();
root.Add("metadata", metadata);
metadata.Add("name", ingress.Name);
var annotations = new YamlMappingNode();
metadata.Add("annotations", annotations);
annotations.Add("kubernetes.io/ingress.class", new YamlScalarNode("nginx") { Style = ScalarStyle.SingleQuoted, });
annotations.Add("nginx.ingress.kubernetes.io/rewrite-target", new YamlScalarNode("/$2") { Style = ScalarStyle.SingleQuoted, });
var labels = new YamlMappingNode();
metadata.Add("labels", labels);
labels.Add("app.kubernetes.io/part-of", new YamlScalarNode(application.Name) { Style = ScalarStyle.SingleQuoted, });
var spec = new YamlMappingNode();
root.Add("spec", spec);
if (ingress.Rules.Count > 0)
{
var rules = new YamlSequenceNode();
spec.Add("rules", rules);
// k8s ingress is grouped by host first, then grouped by path
foreach (var hostgroup in ingress.Rules.GroupBy(r => r.Host))
{
var rule = new YamlMappingNode();
rules.Add(rule);
if (!string.IsNullOrEmpty(hostgroup.Key))
{
rule.Add("host", hostgroup.Key);
}
var http = new YamlMappingNode();
rule.Add("http", http);
var paths = new YamlSequenceNode();
http.Add("paths", paths);
foreach (var ingressRule in hostgroup)
{
var path = new YamlMappingNode();
paths.Add(path);
var backend = new YamlMappingNode();
path.Add("backend", backend);
backend.Add("serviceName", ingressRule.Service);
var service = application.Services.FirstOrDefault(s => s.Name == ingressRule.Service);
if (service is null)
{
throw new InvalidOperationException($"Could not resolve service '{ingressRule.Service}'.");
}
var binding = service.Bindings.FirstOrDefault(b => b.Name is null || b.Name == "http");
if (binding is null)
{
throw new InvalidOperationException($"Could not resolve an http binding for service '{service.Name}'.");
}
backend.Add("servicePort", (binding.Port ?? 80).ToString(CultureInfo.InvariantCulture));
// Tye implements path matching similar to this example:
// https://kubernetes.github.io/ingress-nginx/examples/rewrite/
//
// Therefore our rewrite-target is set to $2 - we want to make sure we have
// two capture groups.
if (string.IsNullOrEmpty(ingressRule.Path))
{
path.Add("path", "/()(.*)"); // () is an empty capture group.
}
else
{
var regex = $"{ingressRule.Path.TrimEnd('/')}(/|$)(.*)";
path.Add("path", regex);
}
}
}
}
return new KubernetesIngressOutput(ingress.Name, new YamlDocument(root));
}
public static KubernetesServiceOutput CreateService(
OutputContext output,
ApplicationBuilder application,
ProjectServiceBuilder project,
@ -93,7 +186,7 @@ namespace Microsoft.Tye
return new KubernetesServiceOutput(project.Name, new YamlDocument(root));
}
public static ServiceOutput CreateDeployment(
public static KubernetesDeploymentOutput CreateDeployment(
OutputContext output,
ApplicationBuilder application,
ProjectServiceBuilder project,

7
src/tye/GenerateHost.cs

@ -43,7 +43,12 @@ namespace Microsoft.Tye
new CombineStep() { Environment = environment, },
new PublishProjectStep(),
new BuildDockerImageStep() { Environment = environment, }, // Make an image but don't push it
new GenerateKubernetesManifestStep() { Environment = environment, },
new GenerateServiceKubernetesManifestStep() { Environment = environment, },
},
IngressSteps =
{
new GenerateIngressKubernetesManifestStep(),
},
ApplicationSteps =

7
src/tye/Program.DeployCommand.cs

@ -82,7 +82,12 @@ namespace Microsoft.Tye
new BuildDockerImageStep() { Environment = environment, },
new PushDockerImageStep() { Environment = environment, },
new ValidateSecretStep() { Environment = environment, Interactive = interactive, Force = force, },
new GenerateKubernetesManifestStep() { Environment = environment, },
new GenerateServiceKubernetesManifestStep() { Environment = environment, },
},
IngressSteps =
{
new GenerateIngressKubernetesManifestStep(),
},
ApplicationSteps =

37
test/E2ETest/TyeGenerateTests.cs

@ -317,5 +317,42 @@ namespace E2ETest
await DockerAssert.DeleteDockerImagesAsync(output, projectName);
}
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task Generate_Ingress()
{
var applicationName = "apps-with-ingress";
var environment = "production";
await DockerAssert.DeleteDockerImagesAsync(output, "app-a");
await DockerAssert.DeleteDockerImagesAsync(output, "app-b");
using var projectDirectory = TestHelpers.CopyTestProjectDirectory(applicationName);
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
try
{
await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false);
// name of application is the folder
var content = await File.ReadAllTextAsync(Path.Combine(projectDirectory.DirectoryPath, $"{applicationName}-generate-{environment}.yaml"));
var expectedContent = await File.ReadAllTextAsync($"testassets/generate/{applicationName}.yaml");
YamlAssert.Equals(expectedContent, content, output);
await DockerAssert.AssertImageExistsAsync(output, "app-a");
await DockerAssert.AssertImageExistsAsync(output, "app-b");
}
finally
{
await DockerAssert.DeleteDockerImagesAsync(output, "app-a");
await DockerAssert.DeleteDockerImagesAsync(output, "app-b");
}
}
}
}

146
test/E2ETest/testassets/generate/apps-with-ingress.yaml

@ -0,0 +1,146 @@
kind: Deployment
apiVersion: apps/v1
metadata:
name: app-a
labels:
app.kubernetes.io/name: 'app-a'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: app-a
template:
metadata:
labels:
app.kubernetes.io/name: 'app-a'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
containers:
- name: app-a
image: app-a:1.0.0
imagePullPolicy: Always
env:
- name: ASPNETCORE_URLS
value: 'http://*'
- name: PORT
value: '80'
- name: SERVICE__APP-B__PROTOCOL
value: 'http'
- name: SERVICE__APP-B__PORT
value: '80'
- name: SERVICE__APP-B__HOST
value: 'app-b'
ports:
- containerPort: 80
...
---
kind: Service
apiVersion: v1
metadata:
name: app-a
labels:
app.kubernetes.io/name: 'app-a'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
selector:
app.kubernetes.io/name: app-a
type: ClusterIP
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
...
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: app-b
labels:
app.kubernetes.io/name: 'app-b'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: app-b
template:
metadata:
labels:
app.kubernetes.io/name: 'app-b'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
containers:
- name: app-b
image: app-b:1.0.0
imagePullPolicy: Always
env:
- name: ASPNETCORE_URLS
value: 'http://*'
- name: PORT
value: '80'
- name: SERVICE__APP-A__PROTOCOL
value: 'http'
- name: SERVICE__APP-A__PORT
value: '80'
- name: SERVICE__APP-A__HOST
value: 'app-a'
ports:
- containerPort: 80
...
---
kind: Service
apiVersion: v1
metadata:
name: app-b
labels:
app.kubernetes.io/name: 'app-b'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
selector:
app.kubernetes.io/name: app-b
type: ClusterIP
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
...
---
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
name: ingress
annotations:
kubernetes.io/ingress.class: 'nginx'
nginx.ingress.kubernetes.io/rewrite-target: '/$2'
labels:
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
rules:
- http:
paths:
- backend:
serviceName: app-a
servicePort: 80
path: /A(/|$)(.*)
- backend:
serviceName: app-b
servicePort: 80
path: /B(/|$)(.*)
- host: a.example.com
http:
paths:
- backend:
serviceName: app-a
servicePort: 80
path: /()(.*)
- host: b.example.com
http:
paths:
- backend:
serviceName: app-b
servicePort: 80
path: /()(.*)
...
Loading…
Cancel
Save