Skip to content

Commit 1bde4cc

Browse files
Racer159UnicornChancemjnagel
authoredMay 14, 2024
feat: add expose service entry for internal cluster traffic (defenseunicorns#356)
## Description This adds a service entry to allow traffic to stay inside the cluster and enable things like proper network policies when clients need to access this endpoint. ## Related Issue Fixes #N/A ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [X] Other (security config, docs update, etc) ## Checklist before merging - [X] Test, docs, adr added or updated as needed - [X] [Contributor Guide Steps](https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md)(https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md#submitting-a-pull-request) followed --------- Co-authored-by: Chance <139784371+UnicornChance@users.noreply.github.com> Co-authored-by: Micah Nagel <micah.nagel@defenseunicorns.com>
1 parent e7cb33e commit 1bde4cc

16 files changed

+600
-126
lines changed
 

‎.eslintrc.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,13 @@
1414
"root": true,
1515
"rules": {
1616
"@typescript-eslint/no-floating-promises": ["error"]
17-
}
17+
},
18+
"overrides": [
19+
{
20+
"files": [ "src/pepr/operator/crd/generated/**/*.ts", "src/pepr/operator/crd/generated/*.ts" ],
21+
"rules": {
22+
"@typescript-eslint/no-explicit-any": "off"
23+
}
24+
}
25+
]
1826
}

‎src/keycloak/chart/templates/secret-admin-password.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ apiVersion: v1
1313
kind: Secret
1414
metadata:
1515
name: {{ $secretName }}
16-
namespace: {{ .Release.Namespace }}
16+
namespace: {{ .Release.Namespace }}
1717
labels:
1818
{{- include "keycloak.labels" . | nindent 4 }}
1919
type: Opaque

‎src/keycloak/chart/templates/secret-postgresql.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: v1
33
kind: Secret
44
metadata:
55
name: {{ include "keycloak.fullname" . }}-postgresql
6-
namespace: {{ .Release.Namespace }}
6+
namespace: {{ .Release.Namespace }}
77
labels:
88
{{- include "keycloak.labels" . | nindent 4 }}
99
type: Opaque

‎src/pepr/operator/README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The UDS Operator manages the lifecycle of UDS Package CRs and their correspondin
88
- establishing default-deny ingress/egress network policies
99
- creating a layered allow-list based approach on top of the default deny network policies including some basic defaults such as Istio requirements and DNS egress
1010
- providing targeted remote endpoints network policies such as `KubeAPI` and `CloudMetadata` to make policies more DRY and provide dynamic bindings where a static definition is not possible
11-
- creating Istio Virtual Services & related ingress gateway network policies
11+
- creating Istio Virtual Services, Service Entries & related ingress gateway network policies
1212

1313
#### Exemption
1414

@@ -25,7 +25,7 @@ metadata:
2525
namespace: grafana
2626
spec:
2727
network:
28-
# Expose rules generate Istio VirtualServices and related network policies
28+
# Expose rules generate Istio VirtualServices, ServiceEntries and related network policies
2929
expose:
3030
- service: grafana
3131
selector:
@@ -196,8 +196,8 @@ graph TD
196196
G -->|Yes| H["Log: Skipping pkg"]
197197
G -->|No| I["Update pkg status to Phase.Pending"]
198198
I --> J{"Check if Istio is installed"}
199-
J -->|Yes| K["Add injection label, process expose CRs for Virtual Services"]
200-
J -->|No| L["Skip Virtual Service Creation"]
199+
J -->|Yes| K["Add injection label, process expose CRs for Istio Resources"]
200+
J -->|No| L["Skip Istio Resource Creation"]
201201
K --> M["Create default network policies in namespace"]
202202
L --> M
203203
M --> N["Process allow CRs for network policies"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { K8s, Log } from "pepr";
2+
3+
import { IstioVirtualService, IstioServiceEntry, UDSPackage } from "../../crd";
4+
import { getOwnerRef } from "../utils";
5+
import { generateVirtualService } from "./virtual-service";
6+
import { generateServiceEntry } from "./service-entry";
7+
8+
/**
9+
* Creates a VirtualService and ServiceEntry for each exposed service in the package
10+
*
11+
* @param pkg
12+
* @param namespace
13+
*/
14+
export async function istioResources(pkg: UDSPackage, namespace: string) {
15+
const pkgName = pkg.metadata!.name!;
16+
const generation = (pkg.metadata?.generation ?? 0).toString();
17+
const ownerRefs = getOwnerRef(pkg);
18+
19+
// Get the list of exposed services
20+
const exposeList = pkg.spec?.network?.expose ?? [];
21+
22+
// Create a Set of processed hosts (to maintain uniqueness)
23+
const hosts = new Set<string>();
24+
25+
// Track which ServiceEntries we've created
26+
const serviceEntryNames: Map<string, boolean> = new Map();
27+
28+
// Iterate over each exposed service
29+
for (const expose of exposeList) {
30+
// Generate a VirtualService for this `expose` entry
31+
const vsPayload = generateVirtualService(expose, namespace, pkgName, generation, ownerRefs);
32+
33+
Log.debug(vsPayload, `Applying VirtualService ${vsPayload.metadata?.name}`);
34+
35+
// Apply the VirtualService and force overwrite any existing policy
36+
await K8s(IstioVirtualService).Apply(vsPayload, { force: true });
37+
38+
vsPayload.spec!.hosts!.forEach(h => hosts.add(h));
39+
40+
// Generate a ServiceEntry for this `expose` entry
41+
const sePayload = generateServiceEntry(expose, namespace, pkgName, generation, ownerRefs);
42+
43+
// If we have already made a ServiceEntry with this name, skip (i.e. if advancedHTTP was used)
44+
if (serviceEntryNames.get(sePayload.metadata!.name!)) {
45+
continue;
46+
}
47+
48+
Log.debug(sePayload, `Applying ServiceEntry ${sePayload.metadata?.name}`);
49+
50+
// Apply the ServiceEntry and force overwrite any existing policy
51+
await K8s(IstioServiceEntry).Apply(sePayload, { force: true });
52+
53+
serviceEntryNames.set(sePayload.metadata!.name!, true);
54+
}
55+
56+
// Get all related VirtualServices in the namespace
57+
const virtualServices = await K8s(IstioVirtualService)
58+
.InNamespace(namespace)
59+
.WithLabel("uds/package", pkgName)
60+
.Get();
61+
62+
// Find any orphaned VirtualServices (not matching the current generation)
63+
const orphanedVS = virtualServices.items.filter(
64+
vs => vs.metadata?.labels?.["uds/generation"] !== generation,
65+
);
66+
67+
// Delete any orphaned VirtualServices
68+
for (const vs of orphanedVS) {
69+
Log.debug(vs, `Deleting orphaned VirtualService ${vs.metadata!.name}`);
70+
await K8s(IstioVirtualService).Delete(vs);
71+
}
72+
73+
// Get all related ServiceEntries in the namespace
74+
const serviceEntries = await K8s(IstioServiceEntry)
75+
.InNamespace(namespace)
76+
.WithLabel("uds/package", pkgName)
77+
.Get();
78+
79+
// Find any orphaned ServiceEntries (not matching the current generation)
80+
const orphanedSE = serviceEntries.items.filter(
81+
se => se.metadata?.labels?.["uds/generation"] !== generation,
82+
);
83+
84+
// Delete any orphaned ServiceEntries
85+
for (const se of orphanedSE) {
86+
Log.debug(se, `Deleting orphaned ServiceEntry ${se.metadata!.name}`);
87+
await K8s(IstioServiceEntry).Delete(se);
88+
}
89+
90+
// Return the list of unique hostnames
91+
return [...hosts];
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from "@jest/globals";
2+
import { UDSConfig } from "../../../config";
3+
import { generateServiceEntry } from "./service-entry";
4+
import { Expose, Gateway, IstioLocation, IstioResolution } from "../../crd";
5+
6+
describe("test generate service entry", () => {
7+
const ownerRefs = [
8+
{
9+
apiVersion: "uds.dev/v1alpha1",
10+
kind: "Package",
11+
name: "test",
12+
uid: "f50120aa-2713-4502-9496-566b102b1174",
13+
},
14+
];
15+
16+
const host = "test";
17+
const port = 8080;
18+
const service = "test-service";
19+
20+
const namespace = "test";
21+
const pkgName = "test";
22+
const generation = "1";
23+
24+
it("should create a simple ServiceEntry object", () => {
25+
const expose: Expose = {
26+
host,
27+
port,
28+
service,
29+
};
30+
31+
const payload = generateServiceEntry(expose, namespace, pkgName, generation, ownerRefs);
32+
33+
expect(payload).toBeDefined();
34+
expect(payload.metadata?.name).toEqual(`${pkgName}-${Gateway.Tenant}-${host}`);
35+
expect(payload.metadata?.namespace).toEqual(namespace);
36+
37+
expect(payload.spec?.hosts).toBeDefined();
38+
expect(payload.spec!.hosts![0]).toEqual(`${host}.${UDSConfig.domain}`);
39+
40+
expect(payload.spec!.location).toEqual(IstioLocation.MeshInternal);
41+
expect(payload.spec!.resolution).toEqual(IstioResolution.DNS);
42+
43+
expect(payload.spec?.ports).toBeDefined();
44+
expect(payload.spec!.ports![0].name).toEqual("https");
45+
expect(payload.spec!.ports![0].number).toEqual(443);
46+
expect(payload.spec!.ports![0].protocol).toEqual("HTTPS");
47+
48+
expect(payload.spec?.endpoints).toBeDefined();
49+
expect(payload.spec!.endpoints![0].address).toEqual(
50+
`${Gateway.Tenant}-ingressgateway.istio-${Gateway.Tenant}-gateway.svc.cluster.local`,
51+
);
52+
});
53+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { UDSConfig } from "../../../config";
2+
import { V1OwnerReference } from "@kubernetes/client-node";
3+
import {
4+
Expose,
5+
Gateway,
6+
IstioServiceEntry,
7+
IstioLocation,
8+
IstioResolution,
9+
IstioPort,
10+
IstioEndpoint,
11+
} from "../../crd";
12+
import { sanitizeResourceName } from "../utils";
13+
14+
/**
15+
* Creates a ServiceEntry for each exposed service in the package
16+
*
17+
* @param pkg
18+
* @param namespace
19+
*/
20+
export function generateServiceEntry(
21+
expose: Expose,
22+
namespace: string,
23+
pkgName: string,
24+
generation: string,
25+
ownerRefs: V1OwnerReference[],
26+
) {
27+
const { gateway = Gateway.Tenant, host } = expose;
28+
29+
const name = generateSEName(pkgName, expose);
30+
31+
// For the admin gateway, we need to add the path prefix
32+
const domain = (gateway === Gateway.Admin ? "admin." : "") + UDSConfig.domain;
33+
34+
// Append the domain to the host
35+
const fqdn = `${host}.${domain}`;
36+
37+
const serviceEntryPort: IstioPort = {
38+
name: "https",
39+
number: 443,
40+
protocol: "HTTPS",
41+
};
42+
43+
const serviceEntryEndpoint: IstioEndpoint = {
44+
// Map the gateway (admin, passthrough or tenant) to the ServiceEntry
45+
address: `${gateway}-ingressgateway.istio-${gateway}-gateway.svc.cluster.local`,
46+
};
47+
48+
const payload: IstioServiceEntry = {
49+
metadata: {
50+
name,
51+
namespace,
52+
labels: {
53+
"uds/package": pkgName,
54+
"uds/generation": generation,
55+
},
56+
// Use the CR as the owner ref for each ServiceEntry
57+
ownerReferences: ownerRefs,
58+
},
59+
spec: {
60+
// Append the UDS Domain to the host
61+
hosts: [fqdn],
62+
location: IstioLocation.MeshInternal,
63+
resolution: IstioResolution.DNS,
64+
ports: [serviceEntryPort],
65+
endpoints: [serviceEntryEndpoint],
66+
},
67+
};
68+
69+
return payload;
70+
}
71+
72+
export function generateSEName(pkgName: string, expose: Expose) {
73+
const { gateway = Gateway.Tenant, host } = expose;
74+
75+
// Ensure the resource name is valid
76+
const name = sanitizeResourceName(`${pkgName}-${gateway}-${host}`);
77+
78+
return name;
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, expect, it } from "@jest/globals";
2+
import { UDSConfig } from "../../../config";
3+
import { generateVirtualService } from "./virtual-service";
4+
import { Expose, Gateway } from "../../crd";
5+
6+
describe("test generate virtual service", () => {
7+
const ownerRefs = [
8+
{
9+
apiVersion: "uds.dev/v1alpha1",
10+
kind: "Package",
11+
name: "test",
12+
uid: "f50120aa-2713-4502-9496-566b102b1174",
13+
},
14+
];
15+
16+
const host = "test";
17+
const port = 8080;
18+
const service = "test-service";
19+
20+
const namespace = "test";
21+
const pkgName = "test";
22+
const generation = "1";
23+
24+
it("should create a simple VirtualService object", () => {
25+
const expose: Expose = {
26+
host,
27+
port,
28+
service,
29+
};
30+
31+
const payload = generateVirtualService(expose, namespace, pkgName, generation, ownerRefs);
32+
33+
expect(payload).toBeDefined();
34+
expect(payload.metadata?.name).toEqual(
35+
`${pkgName}-${Gateway.Tenant}-${host}-${port}-${service}`,
36+
);
37+
expect(payload.metadata?.namespace).toEqual(namespace);
38+
39+
expect(payload.spec?.hosts).toBeDefined();
40+
expect(payload.spec!.hosts![0]).toEqual(`${host}.${UDSConfig.domain}`);
41+
42+
expect(payload.spec?.http).toBeDefined();
43+
expect(payload.spec!.http![0].route).toBeDefined();
44+
expect(payload.spec!.http![0].route![0].destination?.host).toEqual(
45+
`${service}.${namespace}.svc.cluster.local`,
46+
);
47+
expect(payload.spec!.http![0].route![0].destination?.port?.number).toEqual(port);
48+
49+
expect(payload.spec?.gateways).toBeDefined();
50+
expect(payload.spec!.gateways![0]).toEqual(
51+
`istio-${Gateway.Tenant}-gateway/${Gateway.Tenant}-gateway`,
52+
);
53+
});
54+
55+
it("should create an admin VirtualService object", () => {
56+
const gateway = Gateway.Admin;
57+
const expose: Expose = {
58+
gateway,
59+
host,
60+
port,
61+
service,
62+
};
63+
64+
const payload = generateVirtualService(expose, namespace, pkgName, generation, ownerRefs);
65+
66+
expect(payload).toBeDefined();
67+
expect(payload.spec?.hosts).toBeDefined();
68+
expect(payload.spec!.hosts![0]).toEqual(`${host}.admin.${UDSConfig.domain}`);
69+
});
70+
71+
it("should create an advancedHttp VirtualService object", () => {
72+
const advancedHTTP = {
73+
directResponse: { status: 404 },
74+
};
75+
const expose: Expose = {
76+
host,
77+
port,
78+
service,
79+
advancedHTTP,
80+
};
81+
82+
const payload = generateVirtualService(expose, namespace, pkgName, generation, ownerRefs);
83+
84+
expect(payload).toBeDefined();
85+
expect(payload.spec?.http).toBeDefined();
86+
expect(payload.spec!.http![0].route).not.toBeDefined();
87+
expect(payload.spec!.http![0].directResponse?.status).toEqual(404);
88+
});
89+
90+
it("should create a passthrough VirtualService object", () => {
91+
const gateway = Gateway.Passthrough;
92+
const expose: Expose = {
93+
gateway,
94+
host,
95+
port,
96+
service,
97+
};
98+
99+
const payload = generateVirtualService(expose, namespace, pkgName, generation, ownerRefs);
100+
101+
expect(payload).toBeDefined();
102+
expect(payload.spec?.tls).toBeDefined();
103+
expect(payload.spec!.tls![0].match).toBeDefined();
104+
expect(payload.spec!.tls![0].match![0].port).toEqual(443);
105+
expect(payload.spec!.tls![0].match![0].sniHosts![0]).toEqual(`${host}.${UDSConfig.domain}`);
106+
expect(payload.spec!.tls![0].route).toBeDefined();
107+
expect(payload.spec!.http![0].route![0].destination?.host).toEqual(
108+
`${service}.${namespace}.svc.cluster.local`,
109+
);
110+
expect(payload.spec!.http![0].route![0].destination?.port?.number).toEqual(port);
111+
});
112+
});
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,90 @@
1-
import { K8s, Log } from "pepr";
2-
31
import { UDSConfig } from "../../../config";
4-
import { Expose, Gateway, Istio, UDSPackage } from "../../crd";
5-
import { getOwnerRef, sanitizeResourceName } from "../utils";
2+
import { V1OwnerReference } from "@kubernetes/client-node";
3+
import { Expose, Gateway, IstioVirtualService, IstioHTTP, IstioHTTPRoute } from "../../crd";
4+
import { sanitizeResourceName } from "../utils";
65

76
/**
87
* Creates a VirtualService for each exposed service in the package
98
*
109
* @param pkg
1110
* @param namespace
1211
*/
13-
export async function virtualService(pkg: UDSPackage, namespace: string) {
14-
const pkgName = pkg.metadata!.name!;
15-
const generation = (pkg.metadata?.generation ?? 0).toString();
16-
17-
// Get the list of exposed services
18-
const exposeList = pkg.spec?.network?.expose ?? [];
19-
20-
// Create a list of generated VirtualServices
21-
const payloads: Istio.VirtualService[] = [];
22-
23-
// Iterate over each exposed service
24-
for (const expose of exposeList) {
25-
const { gateway = Gateway.Tenant, host, port, service, advancedHTTP = {} } = expose;
26-
27-
const name = generateVSName(pkg, expose);
28-
29-
// For the admin gateway, we need to add the path prefix
30-
const domain = (gateway === Gateway.Admin ? "admin." : "") + UDSConfig.domain;
31-
32-
// Append the domain to the host
33-
const fqdn = `${host}.${domain}`;
34-
35-
const http: Istio.HTTP = { ...advancedHTTP };
36-
37-
// Create the route to the service
38-
const route: Istio.HTTPRoute[] = [
39-
{
40-
destination: {
41-
// Use the service name as the host
42-
host: `${service}.${namespace}.svc.cluster.local`,
43-
// The CRD only uses numeric ports
44-
port: { number: port },
45-
},
12+
export function generateVirtualService(
13+
expose: Expose,
14+
namespace: string,
15+
pkgName: string,
16+
generation: string,
17+
ownerRefs: V1OwnerReference[],
18+
) {
19+
const { gateway = Gateway.Tenant, host, port, service, advancedHTTP = {} } = expose;
20+
21+
const name = generateVSName(pkgName, expose);
22+
23+
// For the admin gateway, we need to add the path prefix
24+
const domain = (gateway === Gateway.Admin ? "admin." : "") + UDSConfig.domain;
25+
26+
// Append the domain to the host
27+
const fqdn = `${host}.${domain}`;
28+
29+
const http: IstioHTTP = { ...advancedHTTP };
30+
31+
// Create the route to the service
32+
const route: IstioHTTPRoute[] = [
33+
{
34+
destination: {
35+
// Use the service name as the host
36+
host: `${service}.${namespace}.svc.cluster.local`,
37+
// The CRD only uses numeric ports
38+
port: { number: port },
4639
},
47-
];
40+
},
41+
];
4842

49-
if (!advancedHTTP.directResponse) {
50-
// Create the route to the service if not using advancedHTTP.directResponse
51-
http.route = route;
52-
}
43+
if (!advancedHTTP.directResponse) {
44+
// Create the route to the service if not using advancedHTTP.directResponse
45+
http.route = route;
46+
}
5347

54-
const payload: Istio.VirtualService = {
55-
metadata: {
56-
name,
57-
namespace,
58-
labels: {
59-
"uds/package": pkgName,
60-
"uds/generation": generation,
61-
},
62-
// Use the CR as the owner ref for each VirtualService
63-
ownerReferences: getOwnerRef(pkg),
48+
const payload: IstioVirtualService = {
49+
metadata: {
50+
name,
51+
namespace,
52+
labels: {
53+
"uds/package": pkgName,
54+
"uds/generation": generation,
6455
},
65-
spec: {
66-
// Append the UDS Domain to the host
67-
hosts: [fqdn],
68-
// Map the gateway (admin, passthrough or tenant) to the VirtualService
69-
gateways: [`istio-${gateway}-gateway/${gateway}-gateway`],
70-
// Apply the route to the VirtualService
71-
http: [http],
56+
// Use the CR as the owner ref for each VirtualService
57+
ownerReferences: ownerRefs,
58+
},
59+
spec: {
60+
// Append the UDS Domain to the host
61+
hosts: [fqdn],
62+
// Map the gateway (admin, passthrough or tenant) to the VirtualService
63+
gateways: [`istio-${gateway}-gateway/${gateway}-gateway`],
64+
// Apply the route to the VirtualService
65+
http: [http],
66+
},
67+
};
68+
69+
// If the gateway is the passthrough gateway, apply the TLS match
70+
if (gateway === Gateway.Passthrough) {
71+
payload.spec!.tls = [
72+
{
73+
match: [{ port: 443, sniHosts: [fqdn] }],
74+
route,
7275
},
73-
};
74-
75-
// If the gateway is the passthrough gateway, apply the TLS match
76-
if (gateway === Gateway.Passthrough) {
77-
payload.spec!.tls = [
78-
{
79-
match: [{ port: 443, sniHosts: [fqdn] }],
80-
route,
81-
},
82-
];
83-
}
84-
85-
Log.debug(payload, `Applying VirtualService ${name}`);
86-
87-
// Apply the VirtualService and force overwrite any existing policy
88-
await K8s(Istio.VirtualService).Apply(payload, { force: true });
89-
90-
payloads.push(payload);
91-
}
92-
93-
// Get all related VirtualServices in the namespace
94-
const virtualServices = await K8s(Istio.VirtualService)
95-
.InNamespace(namespace)
96-
.WithLabel("uds/package", pkgName)
97-
.Get();
98-
99-
// Find any orphaned VirtualServices (not matching the current generation)
100-
const orphanedVS = virtualServices.items.filter(
101-
vs => vs.metadata?.labels?.["uds/generation"] !== generation,
102-
);
103-
104-
// Delete any orphaned VirtualServices
105-
for (const vs of orphanedVS) {
106-
Log.debug(vs, `Deleting orphaned VirtualService ${vs.metadata!.name}`);
107-
await K8s(Istio.VirtualService).Delete(vs);
76+
];
10877
}
109-
110-
// Return the list of unique hostnames
111-
return [...new Set(payloads.map(v => v.spec!.hosts!).flat())];
78+
return payload;
11279
}
11380

114-
export function generateVSName(pkg: UDSPackage, expose: Expose) {
81+
export function generateVSName(pkgName: string, expose: Expose) {
11582
const { gateway = Gateway.Tenant, host, port, service, description, advancedHTTP } = expose;
11683

11784
// Ensure the resource name is valid
11885
const matchHash = advancedHTTP?.match?.flatMap(m => m.name).join("-") || "";
11986
const nameSuffix = description || `${host}-${port}-${service}-${matchHash}`;
120-
const name = sanitizeResourceName(`${pkg.metadata!.name}-${gateway}-${nameSuffix}`);
87+
const name = sanitizeResourceName(`${pkgName}-${gateway}-${nameSuffix}`);
12188

12289
return name;
12390
}

‎src/pepr/operator/controllers/monitoring/service-monitor.spec.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import { describe, expect, it } from "@jest/globals";
22
import { generateServiceMonitor } from "./service-monitor";
3+
import { Monitor } from "../../crd";
34

45
describe("test generate service monitor", () => {
56
it("should return a valid Service Monitor object", () => {
6-
const pkg = {
7-
apiVersion: "uds.dev/v1alpha1",
8-
kind: "Package",
9-
metadata: {
7+
const ownerRefs = [
8+
{
9+
apiVersion: "uds.dev/v1alpha1",
10+
kind: "Package",
1011
name: "test",
1112
uid: "f50120aa-2713-4502-9496-566b102b1174",
1213
},
13-
};
14+
];
1415
const portName = "http-metrics";
1516
const metricsPath = "/test";
1617
const selectorApp = "test";
17-
const monitor = {
18+
const monitor: Monitor = {
1819
portName: portName,
1920
path: metricsPath,
2021
targetPort: 1234,
@@ -25,7 +26,7 @@ describe("test generate service monitor", () => {
2526
const namespace = "test";
2627
const pkgName = "test";
2728
const generation = "1";
28-
const payload = generateServiceMonitor(pkg, monitor, namespace, pkgName, generation);
29+
const payload = generateServiceMonitor(monitor, namespace, pkgName, generation, ownerRefs);
2930

3031
expect(payload).toBeDefined();
3132
expect(payload.metadata?.name).toEqual(`${pkgName}-${selectorApp}-${portName}`);

‎src/pepr/operator/controllers/monitoring/service-monitor.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { K8s, Log } from "pepr";
22

3-
import { Prometheus, UDSPackage } from "../../crd";
4-
import { Monitor } from "../../crd/generated/package-v1alpha1";
3+
import { V1OwnerReference } from "@kubernetes/client-node";
4+
import { Prometheus, UDSPackage, Monitor } from "../../crd";
55
import { getOwnerRef, sanitizeResourceName } from "../utils";
66

77
/**
@@ -13,6 +13,7 @@ import { getOwnerRef, sanitizeResourceName } from "../utils";
1313
export async function serviceMonitor(pkg: UDSPackage, namespace: string) {
1414
const pkgName = pkg.metadata!.name!;
1515
const generation = (pkg.metadata?.generation ?? 0).toString();
16+
const ownerRefs = getOwnerRef(pkg);
1617

1718
Log.debug(`Reconciling ServiceMonitors for ${pkgName}`);
1819

@@ -24,7 +25,7 @@ export async function serviceMonitor(pkg: UDSPackage, namespace: string) {
2425

2526
try {
2627
for (const monitor of monitorList) {
27-
const payload = generateServiceMonitor(pkg, monitor, namespace, pkgName, generation);
28+
const payload = generateServiceMonitor(monitor, namespace, pkgName, generation, ownerRefs);
2829

2930
Log.debug(payload, `Applying ServiceMonitor ${payload.metadata?.name}`);
3031

@@ -60,25 +61,25 @@ export async function serviceMonitor(pkg: UDSPackage, namespace: string) {
6061
return [...payloads.map(sm => sm.metadata!.name!)];
6162
}
6263

63-
export function generateSMName(pkg: UDSPackage, monitor: Monitor) {
64+
export function generateSMName(pkgName: string, monitor: Monitor) {
6465
const { selector, portName, description } = monitor;
6566

6667
// Ensure the resource name is valid
6768
const nameSuffix = description || `${Object.values(selector)}-${portName}`;
68-
const name = sanitizeResourceName(`${pkg.metadata!.name}-${nameSuffix}`);
69+
const name = sanitizeResourceName(`${pkgName}-${nameSuffix}`);
6970

7071
return name;
7172
}
7273

7374
export function generateServiceMonitor(
74-
pkg: UDSPackage,
7575
monitor: Monitor,
7676
namespace: string,
7777
pkgName: string,
7878
generation: string,
79+
ownerRefs: V1OwnerReference[],
7980
) {
8081
const { selector, portName } = monitor;
81-
const name = generateSMName(pkg, monitor);
82+
const name = generateSMName(pkgName, monitor);
8283
const payload: Prometheus.ServiceMonitor = {
8384
metadata: {
8485
name,
@@ -87,7 +88,7 @@ export function generateServiceMonitor(
8788
"uds/package": pkgName,
8889
"uds/generation": generation,
8990
},
90-
ownerReferences: getOwnerRef(pkg),
91+
ownerReferences: ownerRefs,
9192
},
9293
spec: {
9394
endpoints: [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// This file is auto-generated by kubernetes-fluent-client, do not edit manually
2+
3+
import { GenericKind, RegisterKind } from "kubernetes-fluent-client";
4+
5+
export class ServiceEntry extends GenericKind {
6+
/**
7+
* Configuration affecting service registry. See more details at:
8+
* https://istio.io/docs/reference/config/networking/service-entry.html
9+
*/
10+
spec?: Spec;
11+
status?: { [key: string]: any };
12+
}
13+
14+
/**
15+
* Configuration affecting service registry. See more details at:
16+
* https://istio.io/docs/reference/config/networking/service-entry.html
17+
*/
18+
export interface Spec {
19+
/**
20+
* The virtual IP addresses associated with the service.
21+
*/
22+
addresses?: string[];
23+
/**
24+
* One or more endpoints associated with the service.
25+
*/
26+
endpoints?: Endpoint[];
27+
/**
28+
* A list of namespaces to which this service is exported.
29+
*/
30+
exportTo?: string[];
31+
/**
32+
* The hosts associated with the ServiceEntry.
33+
*/
34+
hosts: string[];
35+
/**
36+
* Specify whether the service should be considered external to the mesh or part of the mesh.
37+
*/
38+
location?: Location;
39+
/**
40+
* The ports associated with the external service.
41+
*/
42+
ports?: Port[];
43+
/**
44+
* Service resolution mode for the hosts.
45+
*/
46+
resolution?: Resolution;
47+
/**
48+
* If specified, the proxy will verify that the server certificate's subject alternate name
49+
* matches one of the specified values.
50+
*/
51+
subjectAltNames?: string[];
52+
/**
53+
* Applicable only for MESH_INTERNAL services.
54+
*/
55+
workloadSelector?: WorkloadSelector;
56+
}
57+
58+
export interface Endpoint {
59+
/**
60+
* Address associated with the network endpoint without the port.
61+
*/
62+
address?: string;
63+
/**
64+
* One or more labels associated with the endpoint.
65+
*/
66+
labels?: { [key: string]: string };
67+
/**
68+
* The locality associated with the endpoint.
69+
*/
70+
locality?: string;
71+
/**
72+
* Network enables Istio to group endpoints resident in the same L3 domain/network.
73+
*/
74+
network?: string;
75+
/**
76+
* Set of ports associated with the endpoint.
77+
*/
78+
ports?: { [key: string]: number };
79+
/**
80+
* The service account associated with the workload if a sidecar is present in the workload.
81+
*/
82+
serviceAccount?: string;
83+
/**
84+
* The load balancing weight associated with the endpoint.
85+
*/
86+
weight?: number;
87+
}
88+
89+
/**
90+
* Specify whether the service should be considered external to the mesh or part of the mesh.
91+
*/
92+
export enum Location {
93+
MeshExternal = "MESH_EXTERNAL",
94+
MeshInternal = "MESH_INTERNAL",
95+
}
96+
97+
export interface Port {
98+
/**
99+
* Label assigned to the port.
100+
*/
101+
name: string;
102+
/**
103+
* A valid non-negative integer port number.
104+
*/
105+
number: number;
106+
/**
107+
* The protocol exposed on the port.
108+
*/
109+
protocol?: string;
110+
/**
111+
* The port number on the endpoint where the traffic will be received.
112+
*/
113+
targetPort?: number;
114+
}
115+
116+
/**
117+
* Service resolution mode for the hosts.
118+
*/
119+
export enum Resolution {
120+
DNS = "DNS",
121+
DNSRoundRobin = "DNS_ROUND_ROBIN",
122+
None = "NONE",
123+
Static = "STATIC",
124+
}
125+
126+
/**
127+
* Applicable only for MESH_INTERNAL services.
128+
*/
129+
export interface WorkloadSelector {
130+
/**
131+
* One or more labels that indicate a specific set of pods/VMs on which the configuration
132+
* should be applied.
133+
*/
134+
labels?: { [key: string]: string };
135+
}
136+
137+
RegisterKind(ServiceEntry, {
138+
group: "networking.istio.io",
139+
version: "v1beta1",
140+
kind: "ServiceEntry",
141+
plural: "serviceentries",
142+
});

‎src/pepr/operator/crd/index.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export {
22
Allow,
33
Direction,
44
Expose,
5+
Monitor,
56
Gateway,
67
Phase,
78
Status as PkgStatus,
@@ -20,5 +21,18 @@ export {
2021
Exemption as UDSExemption,
2122
} from "./generated/exemption-v1alpha1";
2223

23-
export * as Istio from "./generated/istio/virtualservice-v1beta1";
24+
export {
25+
VirtualService as IstioVirtualService,
26+
HTTPRoute as IstioHTTPRoute,
27+
HTTP as IstioHTTP,
28+
} from "./generated/istio/virtualservice-v1beta1";
29+
30+
export {
31+
ServiceEntry as IstioServiceEntry,
32+
Location as IstioLocation,
33+
Resolution as IstioResolution,
34+
Endpoint as IstioEndpoint,
35+
Port as IstioPort,
36+
} from "./generated/istio/serviceentry-v1beta1";
37+
2438
export * as Prometheus from "./generated/prometheus/servicemonitor-v1";

‎src/pepr/operator/crd/validators/package-validator.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const invalidNamespaces = ["kube-system", "kube-public", "_unknown_", "pepr-syst
1111
export async function validator(req: PeprValidateRequest<UDSPackage>) {
1212
const pkg = migrate(req.Raw);
1313

14+
const pkgName = pkg.metadata?.name ?? "_unknown_";
1415
const ns = pkg.metadata?.namespace ?? "_unknown_";
1516

1617
if (invalidNamespaces.includes(ns)) {
@@ -38,7 +39,7 @@ export async function validator(req: PeprValidateRequest<UDSPackage>) {
3839
}
3940

4041
// Ensure the service name is unique
41-
const name = generateVSName(req.Raw, expose);
42+
const name = generateVSName(pkgName, expose);
4243
if (virtualServiceNames.has(name)) {
4344
return req.Deny(
4445
`The combination of characteristics of this expose entry would create a duplicate VirtualService. ` +

‎src/pepr/operator/reconcilers/package-reconciler.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Log } from "pepr";
33
import { handleFailure, shouldSkip, updateStatus } from ".";
44
import { UDSConfig } from "../../config";
55
import { enableInjection } from "../controllers/istio/injection";
6-
import { virtualService } from "../controllers/istio/virtual-service";
6+
import { istioResources } from "../controllers/istio/istio-resources";
77
import { keycloak } from "../controllers/keycloak/client-sync";
88
import { serviceMonitor } from "../controllers/monitoring/service-monitor";
99
import { networkPolicies } from "../controllers/network/policies";
@@ -40,8 +40,8 @@ export async function packageReconciler(pkg: UDSPackage) {
4040
// Update the namespace to ensure the istio-injection label is set
4141
await enableInjection(pkg);
4242

43-
// Create the VirtualService for each exposed service
44-
endpoints = await virtualService(pkg, namespace!);
43+
// Create the VirtualService and ServiceEntry for each exposed service
44+
endpoints = await istioResources(pkg, namespace!);
4545

4646
// Only configure the ServiceMonitors if not running in single test mode
4747
let monitors: string[] = [];

‎tasks.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ tasks:
3030
- description: "Deploy the Istio source package with Zarf Dev"
3131
cmd: "uds zarf dev deploy src/istio --flavor ${FLAVOR}"
3232

33+
# Note, this abuses the --flavor flag to only install the CRDs from this package - the "crds-only" flavor is not an explicit flavor of the package
34+
- description: "Deploy the Prometheus-Stack source package with Zarf Dev to only install the CRDs"
35+
cmd: "uds zarf dev deploy src/prometheus-stack --flavor crds-only"
36+
3337
- description: "Dev instructions"
3438
cmd: |
3539
echo "Next steps:"

0 commit comments

Comments
 (0)
Please sign in to comment.