Skip to content
This repository was archived by the owner on Apr 13, 2020. It is now read-only.

Commit 658a0e8

Browse files
evanlouiebnookala
authored andcommitted
RFC1123 Compliant DNS Routes
- `spk hld reconcile` now generates RFC1123 compliant parameters for the generated Treafik IngressRoutes. - `TraefikIngressRoute()` now throws when any combination of `ring` `serviceName` or `k8sBackend` generate a non RFC1123 compliant DNS name. fixes microsoft/bedrock#1103
1 parent cd18f00 commit 658a0e8

File tree

6 files changed

+186
-5
lines changed

6 files changed

+186
-5
lines changed

src/commands/hld/reconcile.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,10 @@ describe("normalizedName", () => {
755755
"fabrikam-frontend-cartservice"
756756
);
757757
});
758+
759+
it("replaces non-(alphanumeric|dash) with dashes", () => {
760+
expect(normalizedName("foo-!@#.#$%")).toBe("foo--------");
761+
});
758762
});
759763

760764
describe("execAndLog", () => {

src/commands/hld/reconcile.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { assertIsStringWithContent } from "../../lib/assertions";
1010
import { build as buildCmd, exit as exitCmd } from "../../lib/commandBuilder";
1111
import { generateAccessYaml } from "../../lib/fileutils";
1212
import { tryGetGitOrigin } from "../../lib/gitutils";
13+
import * as dns from "../../lib/net/dns";
1314
import { TraefikIngressRoute } from "../../lib/traefik/ingress-route";
1415
import {
1516
ITraefikMiddleware,
@@ -74,11 +75,13 @@ export interface IReconcileDependencies {
7475
createMiddlewareForRing: typeof createMiddlewareForRing;
7576
}
7677

78+
/**
79+
* Normalizes the provided service name to a DNS-1123 and Fabrikate command safe
80+
* name.
81+
* All non-alphanumerics and non-dashes are converted to dashes
82+
*/
7783
export const normalizedName = (name: string): string => {
78-
return name
79-
.toLowerCase()
80-
.replace(/\//g, "-")
81-
.replace(/\./g, "-");
84+
return dns.replaceIllegalCharacters(name).replace(/\./g, "-");
8285
};
8386

8487
export const execute = async (

src/lib/net/dns.test.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as dns from "./dns";
2+
3+
describe("containsValidCharacters", () => {
4+
test("alphanumerics, dots, and dashes pass", () => {
5+
expect(dns.containsValidCharacters(".string-starting.with-dot")).toBe(true);
6+
expect(dns.containsValidCharacters("-string.starting-with-dash")).toBe(
7+
true
8+
);
9+
expect(dns.containsValidCharacters("string-starting.with.dot")).toBe(true);
10+
expect(dns.containsValidCharacters("string-ending.with.dot.")).toBe(true);
11+
expect(dns.containsValidCharacters("string-ending.with.dash-")).toBe(true);
12+
expect(dns.containsValidCharacters("string-with.with.char-x")).toBe(true);
13+
});
14+
15+
test("non alphanumerics, non-dots, non-dashes fail", () => {
16+
expect(dns.containsValidCharacters("string-with a-space")).toBe(false);
17+
expect(dns.containsValidCharacters("string-with-!")).toBe(false);
18+
expect(dns.containsValidCharacters("string-with-@")).toBe(false);
19+
expect(dns.containsValidCharacters("string-with-#")).toBe(false);
20+
expect(dns.containsValidCharacters("string-with-$")).toBe(false);
21+
expect(dns.containsValidCharacters("string-with-%")).toBe(false);
22+
expect(dns.containsValidCharacters("string-with-^")).toBe(false);
23+
expect(dns.containsValidCharacters("string-with-&")).toBe(false);
24+
});
25+
});
26+
27+
describe("isValid", () => {
28+
test("valid DNS pass", () => {
29+
expect(dns.isValid("foo.com")).toBe(true);
30+
expect(dns.isValid("foo.bar.baz.123.com")).toBe(true);
31+
expect(dns.isValid("1.foo.bar.123")).toBe(true);
32+
});
33+
34+
test("invalid DNS fail", () => {
35+
expect(dns.isValid("$foo.com")).toBe(false);
36+
expect(dns.isValid("!foo.bar.baz.123.com")).toBe(false);
37+
expect(dns.isValid("%1.foo.bar.123")).toBe(false);
38+
expect(dns.isValid("foo.com%")).toBe(false);
39+
expect(dns.isValid("foo.bar.baz.123.com!")).toBe(false);
40+
expect(dns.isValid("1.foo.bar.123#")).toBe(false);
41+
expect(dns.isValid("foo/com%")).toBe(false);
42+
expect(dns.isValid("foo.bar#!@#baz.123.com")).toBe(false);
43+
expect(dns.isValid("1.foo*&bar.123")).toBe(false);
44+
});
45+
});
46+
47+
describe("replaceIllegalCharacters", () => {
48+
test("all characters become lowercase", () => {
49+
expect(dns.replaceIllegalCharacters("ABC")).toBe("abc");
50+
});
51+
test("all non-alphanumerics become dashes", () => {
52+
expect(dns.replaceIllegalCharacters("a!a@a#a$a%a^a&a*a")).toBe(
53+
"a-a-a-a-a-a-a-a-a"
54+
);
55+
});
56+
});
57+
58+
describe("assertIsValid", () => {
59+
test("does not throw when valid", () => {
60+
expect(() => dns.assertIsValid("foo", "foo.bar.com")).not.toThrow();
61+
expect(() => dns.assertIsValid("foo", "foo.bar-com")).not.toThrow();
62+
});
63+
64+
test("throws when invalid", () => {
65+
expect(() => dns.assertIsValid("foo", "-foo")).toThrow();
66+
expect(() => dns.assertIsValid("foo", "_foo")).toThrow();
67+
expect(() => dns.assertIsValid("foo", "foo-")).toThrow();
68+
expect(() => dns.assertIsValid("foo", "foo_")).toThrow();
69+
expect(() => dns.assertIsValid("foo", "invalid#dns$name")).toThrow();
70+
expect(() => dns.assertIsValid("foo", "invalid/dns/name")).toThrow();
71+
expect(() => dns.assertIsValid("foo", "invalid@dns%name")).toThrow();
72+
});
73+
});

src/lib/net/dns.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
// DNS helper functions
3+
//
4+
// Use these functions for sanitization and validation of DNS names and DNS name
5+
// segments.
6+
////////////////////////////////////////////////////////////////////////////////
7+
8+
export const validDnsRegex = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/;
9+
10+
/**
11+
* Returns if the dns string `segment` is composed only of DNS compliant
12+
* characters.
13+
* Passing this does not make the `segment` a valid DNS name -- use isValid()
14+
* for that functionality.
15+
*
16+
* @param segment the string to validate contains only DNS compliant characters
17+
*/
18+
export const containsValidCharacters = (segment: string): boolean => {
19+
return !!segment.match(/^[0-9a-z-.]+$/);
20+
};
21+
22+
/**
23+
* Returns if the `dns` string provided is a valid DNS-1123 name.
24+
*
25+
* @param dns string to validate is a DNS compliant string
26+
*/
27+
export const isValid = (dns: string): boolean => {
28+
return !!dns.match(validDnsRegex);
29+
};
30+
31+
/**
32+
* Makes the provided `dns` string contain only DNS-1123 compliant characters.
33+
*
34+
* - All characters are converted to lower case
35+
* - All non-alphanumerics, non-dashes ('-'), and non-dots ('.') are converted
36+
* to dashes ('-')
37+
*
38+
* @param dns the dns string to replace all illegal characters from
39+
*/
40+
export const replaceIllegalCharacters = (dns: string): string => {
41+
return dns.toLowerCase().replace(/[^0-9a-z-.]/g, "-");
42+
};
43+
44+
/**
45+
* Asserts that the provided `dns` is a valid RFC1123 name/value.
46+
* @throws {Error} when `dns` is not RFC1123 compliant
47+
*
48+
* @params fieldName the name of thing being validated, used to create Error
49+
* message
50+
* @params dns to validate
51+
*/
52+
export function assertIsValid(
53+
fieldName: string,
54+
dns: string
55+
): asserts dns is string {
56+
if (!isValid(dns)) {
57+
throw Error(
58+
`Invalid ${fieldName} '${dns}' provided for Traefik IngressRoute. Must be RFC1123 compliant and match regex: ${validDnsRegex}`
59+
);
60+
}
61+
}

src/lib/traefik/ingress-route.test.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import uuid from "uuid/v4";
1+
import uuid = require("uuid/v4");
22
import { TraefikIngressRoute } from "./ingress-route";
33

44
describe("TraefikIngressRoute", () => {
@@ -159,4 +159,35 @@ describe("TraefikIngressRoute", () => {
159159
"PathPrefix(`/version/and/Path`) && Headers(`Ring`, `prod`)"
160160
);
161161
});
162+
163+
test("does not throw when meta.name and spec.routes[].services[].name is valid", () => {
164+
expect(() =>
165+
TraefikIngressRoute("valid-service", "valid-ring", 80, "v1")
166+
).not.toThrow();
167+
expect(() =>
168+
TraefikIngressRoute("valid-service", "valid-ring", 80, "v1", {
169+
k8sBackend: "my.valid.service"
170+
})
171+
).not.toThrow();
172+
});
173+
174+
test("throws when meta.name is invalid", () => {
175+
expect(() =>
176+
TraefikIngressRoute("-invalid-serivce&name", "valid-ring", 80, "v1")
177+
).toThrow();
178+
expect(() =>
179+
TraefikIngressRoute("valid-service-name", "invalid-ring-!@#", 80, "v1")
180+
).toThrow();
181+
});
182+
183+
test("throws when spec.routes[].services[].name is invalid", () => {
184+
expect(() =>
185+
TraefikIngressRoute("-invalid-service", "valid-ring", 80, "v1")
186+
).toThrow();
187+
expect(() =>
188+
TraefikIngressRoute("valid-service", "valid-ring", 80, "v1", {
189+
k8sBackend: "-invalid"
190+
})
191+
).toThrow();
192+
});
162193
});

src/lib/traefik/ingress-route.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as dns from "../net/dns";
2+
13
type TraefikEntryPoints = Array<"web" | "web-secure">; // web === 80; web-secure === 443;
24

35
/**
@@ -46,6 +48,9 @@ interface ITraefikIngressRoute {
4648
*
4749
* If `ringName` is an empty string, the header match rule is not included.
4850
*
51+
* @throws {Error} when meta.name or any spec.routes[].service[].name are not
52+
* RFC1123 compliant
53+
*
4954
* @param serviceName name of the service to create the IngressRoute for
5055
* @param ringName name of the ring to which the service belongs
5156
* @param opts options to specify the manifest namespace, IngressRoute entryPoints, pathPrefix, backend service, and version
@@ -74,6 +79,10 @@ export const TraefikIngressRoute = (
7479
const backendService =
7580
k8sBackend && ringName ? `${k8sBackend}-${ringName}` : name;
7681

82+
// validate fields
83+
dns.assertIsValid("metadata.name", name);
84+
dns.assertIsValid("spec.routes[].services[].name", backendService);
85+
7786
return {
7887
apiVersion: "traefik.containo.us/v1alpha1",
7988
kind: "IngressRoute",

0 commit comments

Comments
 (0)