Skip to content

Commit 8992fdf

Browse files
fix: resolve #2827 — CDK8s+: Support headless services without ports
Fixes #2827 Signed-off-by: tudragon154203 <76395825+tudragon154203@users.noreply.github.com>
1 parent 4344d78 commit 8992fdf

1 file changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { ApiObject, Lazy, Duration } from 'cdk8s';
2+
import { Construct, IConstruct } from 'constructs';
3+
import * as base from './base';
4+
import * as k8s from './imports/k8s';
5+
import * as selector from './selector';
6+
import * as util from './util';
7+
8+
/**
9+
* Options for `Service`.
10+
*/
11+
export interface ServiceProps {
12+
/**
13+
* The IP address of the service and is usually assigned randomly by the
14+
* master. If an address is specified manually and is not in use by others,
15+
* it will be allocated to the service; otherwise, creation of the service
16+
* will fail. This field can not be changed through updates. Valid values
17+
* are "None", empty string (""), or a valid IP address. "None" can be
18+
* specified for headless services when proxying is not required. Only
19+
* applies to types ClusterIP, NodePort, and LoadBalancer. Ignored if type
20+
* is ExternalName.
21+
*
22+
* @default - Automatically assigned.
23+
*/
24+
readonly clusterIP?: string;
25+
26+
/**
27+
* A list of IP addresses for which nodes in the cluster will also accept
28+
* traffic for this service. These IPs are not managed by Kubernetes.
29+
*
30+
* @default - No external IPs.
31+
*/
32+
readonly externalIPs?: string[];
33+
34+
/**
35+
* Determines how the Service is exposed.
36+
*
37+
* @default ServiceType.CLUSTER_IP
38+
*/
39+
readonly type?: ServiceType;
40+
41+
/**
42+
* The list of ports that are exposed by this service.
43+
*
44+
* @default - No ports.
45+
*/
46+
readonly ports?: ServicePort[];
47+
48+
/**
49+
* Label selector for pods. Only pods matching the selector will be
50+
* considered by this service.
51+
*
52+
* @default - No selector.
53+
*/
54+
readonly selector?: selector.PodSelector;
55+
56+
/**
57+
* The external name of the service. Only applies to services of
58+
* type ExternalName.
59+
*
60+
* @default - No external name.
61+
*/
62+
readonly externalName?: string;
63+
}
64+
65+
/**
66+
* Constant values for clusterIP.
67+
*/
68+
export class ClusterIP {
69+
/**
70+
* Used to specify a headless service.
71+
*/
72+
public static readonly NONE = 'None';
73+
}
74+
75+
/**
76+
* Service types.
77+
*/
78+
export enum ServiceType {
79+
/**
80+
* Exposes the service on a cluster-internal IP.
81+
*/
82+
CLUSTER_IP = 'ClusterIP',
83+
84+
/**
85+
* Exposes the service on each Node's IP at a static port.
86+
*/
87+
NODE_PORT = 'NodePort',
88+
89+
/**
90+
* Exposes the service externally using a cloud load balancer.
91+
*/
92+
LOAD_BALANCER = 'LoadBalancer',
93+
94+
/**
95+
* Maps the service to the contents of the externalName field.
96+
*/
97+
EXTERNAL_NAME = 'ExternalName',
98+
}
99+
100+
/**
101+
* Options for a service port.
102+
*/
103+
export interface ServicePort {
104+
/**
105+
* The port number.
106+
*/
107+
readonly port: number;
108+
109+
/**
110+
* The port on the pod to target.
111+
*
112+
* @default - The value of `port` is used.
113+
*/
114+
readonly targetPort?: number;
115+
116+
/**
117+
* The name of the port.
118+
*
119+
* @default - No name.
120+
*/
121+
readonly name?: string;
122+
123+
/**
124+
* The IP protocol for this port. Supports "TCP", "UDP", and "SCTP".
125+
*
126+
* @default Protocol.TCP
127+
*/
128+
readonly protocol?: Protocol;
129+
}
130+
131+
/**
132+
* Network protocols.
133+
*/
134+
export enum Protocol {
135+
TCP = 'TCP',
136+
UDP = 'UDP',
137+
SCTP = 'SCTP',
138+
}
139+
140+
/**
141+
* A Kubernetes service.
142+
*/
143+
export class Service extends base.Resource {
144+
145+
/**
146+
* The type of the service.
147+
*/
148+
public readonly type: ServiceType;
149+
150+
/**
151+
* The cluster IP address of the service.
152+
*/
153+
public readonly clusterIP?: string;
154+
155+
/**
156+
* The external name of the service.
157+
*/
158+
public readonly externalName?: string;
159+
160+
/**
161+
* The external IP addresses of the service.
162+
*/
163+
public readonly externalIPs: string[];
164+
165+
private readonly _ports: ServicePort[];
166+
private readonly _selector: { [key: string]: string };
167+
168+
/**
169+
* Whether the service is headless.
170+
*/
171+
private get _isHeadless(): boolean {
172+
return this.clusterIP === ClusterIP.NONE;
173+
}
174+
175+
constructor(scope: Construct, id: string, props: ServiceProps = {}) {
176+
super(scope, id);
177+
178+
this.type = props.type ?? ServiceType.CLUSTER_IP;
179+
this.clusterIP = props.clusterIP;
180+
this.externalName = props.externalName;
181+
this.externalIPs = props.externalIPs ?? [];
182+
this._ports = props.ports ?? [];
183+
this._selector = props.selector?._selector() ?? {};
184+
}
185+
186+
/**
187+
* Add a port to the service.
188+
*/
189+
public addPort(port: ServicePort): void {
190+
this._ports.push(port);
191+
}
192+
193+
/**
194+
* The ports exposed by the service.
195+
*/
196+
public get ports(): ServicePort[] {
197+
return [...this._ports];
198+
}
199+
200+
/**
201+
* @internal
202+
*/
203+
public _toKube(): k8s.KubeServiceProps {
204+
const ports: k8s.ServicePort[] = this._ports.map(p => ({
205+
port: p.port,
206+
targetPort: p.targetPort ?? p.port,
207+
name: p.name,
208+
protocol: p.protocol,
209+
}));
210+
211+
return {
212+
spec: {
213+
type: this.type,
214+
clusterIP: this.clusterIP,
215+
externalName: this.externalName,
216+
externalIPs: this.externalIPs,
217+
selector: this._selector,
218+
ports: ports.length > 0 ? ports : undefined,
219+
},
220+
};
221+
}
222+
223+
/**
224+
* @internal
225+
*/
226+
public _toKubeApiObject(): k8s.KubeService {
227+
return new k8s.KubeService(this, 'Service', this._toKube());
228+
}
229+
230+
/**
231+
* Validate the service.
232+
*/
233+
protected validate(): string[] {
234+
const errors: string[] = [];
235+
236+
if (this._ports.length === 0 && !this._isHeadless) {
237+
errors.push('A service must be configured with a port');
238+
}
239+
240+
if (this._ports.length === 0 && this.type !== ServiceType.CLUSTER_IP && !this._isHeadless) {
241+
errors.push('A non-headless service of type ' + this.type + ' must have at least one port');
242+
}
243+
244+
return errors;
245+
}
246+
}

0 commit comments

Comments
 (0)