diff --git a/src/pepr/operator/controllers/istio/service-entry.ts b/src/pepr/operator/controllers/istio/service-entry.ts new file mode 100644 index 000000000..53b079e03 --- /dev/null +++ b/src/pepr/operator/controllers/istio/service-entry.ts @@ -0,0 +1,107 @@ +import { K8s, Log } from "pepr"; + +import { UDSConfig } from "../../../config"; +import { Expose, Gateway, IstioServiceEntry, IstioLocation, IstioResolution, IstioPort, IstioEndpoint, UDSPackage } from "../../crd"; +import { getOwnerRef, sanitizeResourceName } from "../utils"; + +/** + * Creates a ServiceEntry for each exposed service in the package + * + * @param pkg + * @param namespace + */ +export async function serviceEntry(pkg: UDSPackage, namespace: string) { + const pkgName = pkg.metadata!.name!; + const generation = (pkg.metadata?.generation ?? 0).toString(); + + // Get the list of exposed services + const exposeList = pkg.spec?.network?.expose ?? []; + + // Track which ServiceEntries we've created + const serviceEntryNames: Map = new Map(); + + // Iterate over each exposed service + for (const expose of exposeList) { + const { gateway = Gateway.Tenant, host } = expose; + + const name = generateSEName(pkg, expose); + + // If we have already made a ServiceEntry with this name, skip (i.e. if advancedHTTP was used) + if (serviceEntryNames.get(name)) { + continue + } + + // For the admin gateway, we need to add the path prefix + const domain = (gateway === Gateway.Admin ? "admin." : "") + UDSConfig.domain; + + // Append the domain to the host + const fqdn = `${host}.${domain}`; + + const serviceEntryPort: IstioPort = { + name: "https", + number: 443, + protocol: "HTTPS", + } + + const serviceEntryEndpoint: IstioEndpoint = { + // Map the gateway (admin, passthrough or tenant) to the ServiceEntry + address: `${gateway}-ingressgateway.istio-${gateway}-gateway.svc.cluster.local` + } + + const payload: IstioServiceEntry = { + metadata: { + name, + namespace, + labels: { + "uds/package": pkgName, + "uds/generation": generation, + }, + // Use the CR as the owner ref for each ServiceEntry + ownerReferences: getOwnerRef(pkg), + }, + spec: { + // Append the UDS Domain to the host + hosts: [fqdn], + location: IstioLocation.MeshInternal, + resolution: IstioResolution.DNS, + ports: [serviceEntryPort], + endpoints: [serviceEntryEndpoint], + }, + }; + + + Log.debug(payload, `Applying ServiceEntry ${payload.metadata?.name}`); + + // Apply the ServiceEntry and force overwrite any existing policy + await K8s(IstioServiceEntry).Apply(payload, { force: true }); + + serviceEntryNames.set(name, true) + } + + // Get all related ServiceEntries in the namespace + const serviceEntries = await K8s(IstioServiceEntry) + .InNamespace(namespace) + .WithLabel("uds/package", pkgName) + .Get(); + + // Find any orphaned ServiceEntries (not matching the current generation) + const orphanedSE = serviceEntries.items.filter( + vs => vs.metadata?.labels?.["uds/generation"] !== generation, + ); + + // Delete any orphaned ServiceEntries + for (const vs of orphanedSE) { + Log.debug(vs, `Deleting orphaned ServiceEntry ${vs.metadata!.name}`); + await K8s(IstioServiceEntry).Delete(vs); + } +} + +export function generateSEName(pkg: UDSPackage, expose: Expose) { + const { gateway = Gateway.Tenant, host, port, service, description } = expose; + + // Ensure the resource name is valid + const nameSuffix = description || `${host}-${port}-${service}`; + const name = sanitizeResourceName(`${pkg.metadata!.name}-${gateway}-${nameSuffix}`); + + return name; +} diff --git a/src/pepr/operator/controllers/istio/virtual-service.ts b/src/pepr/operator/controllers/istio/virtual-service.ts index 913625699..7a619868b 100644 --- a/src/pepr/operator/controllers/istio/virtual-service.ts +++ b/src/pepr/operator/controllers/istio/virtual-service.ts @@ -1,7 +1,7 @@ import { K8s, Log } from "pepr"; import { UDSConfig } from "../../../config"; -import { Expose, Gateway, Istio, UDSPackage } from "../../crd"; +import { Expose, Gateway, IstioVirtualService, IstioHTTP, IstioHTTPRoute, UDSPackage } from "../../crd"; import { getOwnerRef, sanitizeResourceName } from "../utils"; /** @@ -18,7 +18,7 @@ export async function virtualService(pkg: UDSPackage, namespace: string) { const exposeList = pkg.spec?.network?.expose ?? []; // Create a list of generated VirtualServices - const payloads: Istio.VirtualService[] = []; + const payloads: IstioVirtualService[] = []; // Iterate over each exposed service for (const expose of exposeList) { @@ -32,10 +32,10 @@ export async function virtualService(pkg: UDSPackage, namespace: string) { // Append the domain to the host const fqdn = `${host}.${domain}`; - const http: Istio.HTTP = { ...advancedHTTP }; + const http: IstioHTTP = { ...advancedHTTP }; // Create the route to the service - const route: Istio.HTTPRoute[] = [ + const route: IstioHTTPRoute[] = [ { destination: { // Use the service name as the host @@ -51,7 +51,7 @@ export async function virtualService(pkg: UDSPackage, namespace: string) { http.route = route; } - const payload: Istio.VirtualService = { + const payload: IstioVirtualService = { metadata: { name, namespace, @@ -85,13 +85,13 @@ export async function virtualService(pkg: UDSPackage, namespace: string) { Log.debug(payload, `Applying VirtualService ${name}`); // Apply the VirtualService and force overwrite any existing policy - await K8s(Istio.VirtualService).Apply(payload, { force: true }); + await K8s(IstioVirtualService).Apply(payload, { force: true }); payloads.push(payload); } // Get all related VirtualServices in the namespace - const virtualServices = await K8s(Istio.VirtualService) + const virtualServices = await K8s(IstioVirtualService) .InNamespace(namespace) .WithLabel("uds/package", pkgName) .Get(); @@ -104,7 +104,7 @@ export async function virtualService(pkg: UDSPackage, namespace: string) { // Delete any orphaned VirtualServices for (const vs of orphanedVS) { Log.debug(vs, `Deleting orphaned VirtualService ${vs.metadata!.name}`); - await K8s(Istio.VirtualService).Delete(vs); + await K8s(IstioVirtualService).Delete(vs); } // Return the list of unique hostnames diff --git a/src/pepr/operator/crd/generated/istio/serviceentry-v1beta1.ts b/src/pepr/operator/crd/generated/istio/serviceentry-v1beta1.ts new file mode 100644 index 000000000..854633580 --- /dev/null +++ b/src/pepr/operator/crd/generated/istio/serviceentry-v1beta1.ts @@ -0,0 +1,141 @@ +// This file is auto-generated by kubernetes-fluent-client, do not edit manually + +import { GenericKind, RegisterKind } from "kubernetes-fluent-client"; + +export class ServiceEntry extends GenericKind { + /** + * Configuration affecting service registry. See more details at: + * https://istio.io/docs/reference/config/networking/service-entry.html + */ + spec?: Spec; + status?: { [key: string]: any }; +} + +/** + * Configuration affecting service registry. See more details at: + * https://istio.io/docs/reference/config/networking/service-entry.html + */ +export interface Spec { + /** + * The virtual IP addresses associated with the service. + */ + addresses?: string[]; + /** + * One or more endpoints associated with the service. + */ + endpoints?: Endpoint[]; + /** + * A list of namespaces to which this service is exported. + */ + exportTo?: string[]; + /** + * The hosts associated with the ServiceEntry. + */ + hosts: string[]; + /** + * Specify whether the service should be considered external to the mesh or part of the mesh. + */ + location?: Location; + /** + * The ports associated with the external service. + */ + ports?: Port[]; + /** + * Service resolution mode for the hosts. + */ + resolution?: Resolution; + /** + * If specified, the proxy will verify that the server certificate's subject alternate name + * matches one of the specified values. + */ + subjectAltNames?: string[]; + /** + * Applicable only for MESH_INTERNAL services. + */ + workloadSelector?: WorkloadSelector; +} + +export interface Endpoint { + /** + * Address associated with the network endpoint without the port. + */ + address?: string; + /** + * One or more labels associated with the endpoint. + */ + labels?: { [key: string]: string }; + /** + * The locality associated with the endpoint. + */ + locality?: string; + /** + * Network enables Istio to group endpoints resident in the same L3 domain/network. + */ + network?: string; + /** + * Set of ports associated with the endpoint. + */ + ports?: { [key: string]: number }; + /** + * The service account associated with the workload if a sidecar is present in the workload. + */ + serviceAccount?: string; + /** + * The load balancing weight associated with the endpoint. + */ + weight?: number; +} + +/** + * Specify whether the service should be considered external to the mesh or part of the mesh. + */ +export enum Location { + MeshExternal = "MESH_EXTERNAL", + MeshInternal = "MESH_INTERNAL", +} + +export interface Port { + /** + * Label assigned to the port. + */ + name: string; + /** + * A valid non-negative integer port number. + */ + number: number; + /** + * The protocol exposed on the port. + */ + protocol?: string; + /** + * The port number on the endpoint where the traffic will be received. + */ + targetPort?: number; +} + +/** + * Service resolution mode for the hosts. + */ +export enum Resolution { + DNS = "DNS", + DNSRoundRobin = "DNS_ROUND_ROBIN", + None = "NONE", + Static = "STATIC", +} + +/** + * Applicable only for MESH_INTERNAL services. + */ +export interface WorkloadSelector { + /** + * One or more labels that indicate a specific set of pods/VMs on which the configuration + * should be applied. + */ + labels?: { [key: string]: string }; +} + +RegisterKind(ServiceEntry, { + group: "networking.istio.io", + version: "v1beta1", + kind: "ServiceEntry", +}); \ No newline at end of file diff --git a/src/pepr/operator/crd/index.ts b/src/pepr/operator/crd/index.ts index 11982ead1..1968931b5 100644 --- a/src/pepr/operator/crd/index.ts +++ b/src/pepr/operator/crd/index.ts @@ -20,5 +20,18 @@ export { Exemption as UDSExemption, } from "./generated/exemption-v1alpha1"; -export * as Istio from "./generated/istio/virtualservice-v1beta1"; +export { + VirtualService as IstioVirtualService, + HTTPRoute as IstioHTTPRoute, + HTTP as IstioHTTP, +} from "./generated/istio/virtualservice-v1beta1"; + +export { + ServiceEntry as IstioServiceEntry, + Location as IstioLocation, + Resolution as IstioResolution, + Endpoint as IstioEndpoint, + Port as IstioPort, +} from "./generated/istio/serviceentry-v1beta1"; + export * as Prometheus from "./generated/prometheus/servicemonitor-v1"; diff --git a/src/pepr/operator/reconcilers/package-reconciler.ts b/src/pepr/operator/reconcilers/package-reconciler.ts index 2c500301d..a111afa1e 100644 --- a/src/pepr/operator/reconcilers/package-reconciler.ts +++ b/src/pepr/operator/reconcilers/package-reconciler.ts @@ -4,6 +4,7 @@ import { handleFailure, shouldSkip, updateStatus } from "."; import { UDSConfig } from "../../config"; import { enableInjection } from "../controllers/istio/injection"; import { virtualService } from "../controllers/istio/virtual-service"; +import { serviceEntry } from "../controllers/istio/service-entry"; import { keycloak } from "../controllers/keycloak/client-sync"; import { serviceMonitor } from "../controllers/monitoring/service-monitor"; import { networkPolicies } from "../controllers/network/policies"; @@ -43,6 +44,9 @@ export async function packageReconciler(pkg: UDSPackage) { // Create the VirtualService for each exposed service endpoints = await virtualService(pkg, namespace!); + // Create the ServiceEntry for each exposed service + await serviceEntry(pkg, namespace!); + // Only configure the ServiceMonitors if not running in single test mode let monitors: string[] = []; if (!UDSConfig.isSingleTest) {