Skip to content

Commit

Permalink
Tye should validate that service names are valid DNS names (dotnet#480)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkotalik authored May 15, 2020
1 parent 32ce5b7 commit ef441e8
Show file tree
Hide file tree
Showing 19 changed files with 168 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.dotnet/
artifacts/
.tye/

**/.vscode/

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
Expand Down
7 changes: 6 additions & 1 deletion docs/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ services:

#### `name` (string) *required*

The service name. Each service must have a name, and it must be a legal DNS name: (`a-z` + `_`).
The service name. Each service must have a name, and it must be a legal DNS name: (`a-z` + `-`). Specifically, the service name must:

- Contain at most 63 characters
- Contain only alphanumeric characters or ‘-’
- Start with an alphanumeric character
- End with an alphanumeric character

#### `project` (string)

Expand Down
3 changes: 1 addition & 2 deletions samples/apps-with-ingress/tye.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ ingress:
- host: a.example.com
service: app-a
- host: b.example.com
service: app-b

service: app-b
services:
- name: app-a
project: ApplicationA/ApplicationA.csproj
Expand Down
8 changes: 6 additions & 2 deletions src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.Tye.Serialization;
using Tye.Serialization;

Expand Down Expand Up @@ -42,7 +43,7 @@ private static ConfigApplication FromProject(FileInfo file)

var service = new ConfigService()
{
Name = Path.GetFileNameWithoutExtension(file.Name).ToLowerInvariant(),
Name = NormalizeServiceName(Path.GetFileNameWithoutExtension(file.Name)),
Project = file.FullName.Replace('\\', '/'),
};

Expand Down Expand Up @@ -73,7 +74,7 @@ private static ConfigApplication FromSolution(FileInfo file)
{
var service = new ConfigService()
{
Name = Path.GetFileNameWithoutExtension(projectFile.Name).ToLowerInvariant(),
Name = NormalizeServiceName(Path.GetFileNameWithoutExtension(projectFile.Name)),
Project = projectFile.FullName.Replace('\\', '/'),
};

Expand All @@ -97,5 +98,8 @@ private static ConfigApplication FromYaml(FileInfo file)
using var parser = new YamlParser(file);
return parser.ParseConfigApplication();
}

private static string NormalizeServiceName(string name)
=> Regex.Replace(name.ToLowerInvariant(), "[^0-9A-Za-z-]+", "-");
}
}
8 changes: 8 additions & 0 deletions src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Tye;
using YamlDotNet.Serialization;

namespace Microsoft.Tye.ConfigModel
{
public class ConfigService
{
const string ErrorMessage = "A service name must consist of lower case alphanumeric characters or '-'," +
" start with an alphabetic character, and end with an alphanumeric character" +
" (e.g. 'my-name', or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?').";
const string MaxLengthErrorMessage = "Name cannot be more that 63 characters long.";

[Required]
[RegularExpression("[a-z]([-a-z0-9]*[a-z0-9])?", ErrorMessage = ErrorMessage)]
[MaxLength(63, ErrorMessage = MaxLengthErrorMessage)]
public string Name { get; set; } = default!;
public bool External { get; set; }
public string? Image { get; set; }
Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private static void HandleIngressMapping(YamlMappingNode yamlMappingNode, Config
switch (key)
{
case "name":
configIngress.Name = YamlParser.GetScalarValue(key, child.Value);
configIngress.Name = YamlParser.GetScalarValue(key, child.Value).ToLowerInvariant();
break;
case "replicas":
if (!int.TryParse(YamlParser.GetScalarValue(key, child.Value), out var replicas))
Expand Down Expand Up @@ -91,7 +91,7 @@ private static void HandleIngressRuleMapping(YamlMappingNode yamlMappingNode, Co
rule.Path = YamlParser.GetScalarValue(key, child.Value);
break;
case "service":
rule.Service = YamlParser.GetScalarValue(key, child.Value);
rule.Service = YamlParser.GetScalarValue(key, child.Value).ToLowerInvariant();
break;
default:
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private static void HandleServiceNameMapping(YamlMappingNode yamlMappingNode, Co
switch (key)
{
case "name":
service.Name = YamlParser.GetScalarValue(key, child.Value);
service.Name = YamlParser.GetScalarValue(key, child.Value).ToLowerInvariant();
break;
case "external":
if (!bool.TryParse(YamlParser.GetScalarValue(key, child.Value), out var external))
Expand Down
14 changes: 7 additions & 7 deletions test/E2ETest/TyeGenerateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public async Task GenerateWorksWithoutRegistry()
public async Task Generate_DaprApplication()
{
var applicationName = "dapr_test_application";
var projectName = "dapr_test_project";
var projectName = "dapr-test-project";
var environment = "production";

await DockerAssert.DeleteDockerImagesAsync(output, projectName);
Expand Down Expand Up @@ -363,8 +363,8 @@ 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");
await DockerAssert.DeleteDockerImagesAsync(output, "appa");
await DockerAssert.DeleteDockerImagesAsync(output, "appa");

using var projectDirectory = TestHelpers.CopyTestProjectDirectory(applicationName);

Expand All @@ -383,13 +383,13 @@ public async Task Generate_Ingress()

YamlAssert.Equals(expectedContent, content, output);

await DockerAssert.AssertImageExistsAsync(output, "app-a");
await DockerAssert.AssertImageExistsAsync(output, "app-b");
await DockerAssert.AssertImageExistsAsync(output, "appa");
await DockerAssert.AssertImageExistsAsync(output, "appb");
}
finally
{
await DockerAssert.DeleteDockerImagesAsync(output, "app-a");
await DockerAssert.DeleteDockerImagesAsync(output, "app-b");
await DockerAssert.DeleteDockerImagesAsync(output, "appa");
await DockerAssert.DeleteDockerImagesAsync(output, "appb");
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions test/E2ETest/TyeInitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,20 @@ public void Init_ProjectKinds()

YamlAssert.Equals(expectedContent, content);
}


[Fact]
public void Console_Normalization_Service_Name()
{
using var projectDirectory = CopyTestProjectDirectory("Console.Normalization.svc.Name");

var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "Console.Normalization.svc.Name.csproj"));

var (content, _) = InitHost.CreateTyeFileContent(projectFile, force: false);

var expectedContent = File.ReadAllText("testassets/init/console-normalization-svc-name.yaml");

YamlAssert.Equals(expectedContent, content);
}
}
}
8 changes: 4 additions & 4 deletions test/E2ETest/TyeRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -489,8 +489,8 @@ public async Task IngressRunTest()
await RunHostingApplication(application, new HostOptions(), async (app, uri) =>
{
var ingressUri = await GetServiceUrl(client, uri, "ingress");
var appAUri = await GetServiceUrl(client, uri, "app-a");
var appBUri = await GetServiceUrl(client, uri, "app-b");
var appAUri = await GetServiceUrl(client, uri, "appa");
var appBUri = await GetServiceUrl(client, uri, "appb");

var appAResponse = await client.GetAsync(appAUri);
var appBResponse = await client.GetAsync(appBUri);
Expand Down Expand Up @@ -538,8 +538,8 @@ public async Task NginxIngressTest()
await RunHostingApplication(application, new HostOptions(), async (app, uri) =>
{
var nginxUri = await GetServiceUrl(client, uri, "nginx");
var appAUri = await GetServiceUrl(client, uri, "appA");
var appBUri = await GetServiceUrl(client, uri, "appB");
var appAUri = await GetServiceUrl(client, uri, "appa");
var appBUri = await GetServiceUrl(client, uri, "appb");

var nginxResponse = await client.GetAsync(nginxUri);
var appAResponse = await client.GetAsync(appAUri);
Expand Down
60 changes: 30 additions & 30 deletions test/E2ETest/testassets/generate/apps-with-ingress.yaml
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
kind: Deployment
apiVersion: apps/v1
metadata:
name: app-a
name: appa
labels:
app.kubernetes.io/name: 'app-a'
app.kubernetes.io/name: 'appa'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: app-a
app.kubernetes.io/name: appa
template:
metadata:
labels:
app.kubernetes.io/name: 'app-a'
app.kubernetes.io/name: 'appa'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
containers:
- name: app-a
image: app-a:1.0.0
- name: appa
image: appa:1.0.0
imagePullPolicy: Always
env:
- name: ASPNETCORE_URLS
value: 'http://*'
- name: PORT
value: '80'
- name: SERVICE__APP-B__PROTOCOL
- name: SERVICE__APPB__PROTOCOL
value: 'http'
- name: SERVICE__APP-B__PORT
- name: SERVICE__APPB__PORT
value: '80'
- name: SERVICE__APP-B__HOST
value: 'app-b'
- name: SERVICE__APPB__HOST
value: 'appb'
ports:
- containerPort: 80
...
---
kind: Service
apiVersion: v1
metadata:
name: app-a
name: appa
labels:
app.kubernetes.io/name: 'app-a'
app.kubernetes.io/name: 'appa'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
selector:
app.kubernetes.io/name: app-a
app.kubernetes.io/name: appa
type: ClusterIP
ports:
- name: http
Expand All @@ -56,50 +56,50 @@ spec:
kind: Deployment
apiVersion: apps/v1
metadata:
name: app-b
name: appb
labels:
app.kubernetes.io/name: 'app-b'
app.kubernetes.io/name: 'appb'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: app-b
app.kubernetes.io/name: appb
template:
metadata:
labels:
app.kubernetes.io/name: 'app-b'
app.kubernetes.io/name: 'appb'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
containers:
- name: app-b
image: app-b:1.0.0
- name: appb
image: appb:1.0.0
imagePullPolicy: Always
env:
- name: ASPNETCORE_URLS
value: 'http://*'
- name: PORT
value: '80'
- name: SERVICE__APP-A__PROTOCOL
- name: SERVICE__APPA__PROTOCOL
value: 'http'
- name: SERVICE__APP-A__PORT
- name: SERVICE__APPA__PORT
value: '80'
- name: SERVICE__APP-A__HOST
value: 'app-a'
- name: SERVICE__APPA__HOST
value: 'appa'
ports:
- containerPort: 80
...
---
kind: Service
apiVersion: v1
metadata:
name: app-b
name: appb
labels:
app.kubernetes.io/name: 'app-b'
app.kubernetes.io/name: 'appb'
app.kubernetes.io/part-of: 'apps-with-ingress'
spec:
selector:
app.kubernetes.io/name: app-b
app.kubernetes.io/name: appb
type: ClusterIP
ports:
- name: http
Expand All @@ -122,25 +122,25 @@ spec:
- http:
paths:
- backend:
serviceName: app-a
serviceName: appa
servicePort: 80
path: /A(/|$)(.*)
- backend:
serviceName: app-b
serviceName: appb
servicePort: 80
path: /B(/|$)(.*)
- host: a.example.com
http:
paths:
- backend:
serviceName: app-a
serviceName: appa
servicePort: 80
path: /()(.*)
- host: b.example.com
http:
paths:
- backend:
serviceName: app-b
serviceName: appb
servicePort: 80
path: /()(.*)
...
Loading

0 comments on commit ef441e8

Please sign in to comment.