Skip to content

Commit 1bde4cc

Browse files
Racer159chance-colemanmjnagel
authored
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 <[email protected]> Co-authored-by: Micah Nagel <[email protected]>
1 parent e7cb33e commit 1bde4cc

File tree

16 files changed

+600
-126
lines changed

16 files changed

+600
-126
lines changed

.eslintrc.json

Lines changed: 9 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 4 additions & 4 deletions
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"]
Lines changed: 92 additions & 0 deletions
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+
}
Lines changed: 53 additions & 0 deletions
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+
});
Lines changed: 79 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)