@bluedynamics/cdk8s-plone 0.1.40 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsii +281 -23
- package/API.md +204 -1
- package/docs/superpowers/plans/2026-06-12-configurable-service-spec.md +608 -0
- package/docs/superpowers/specs/2026-06-12-configurable-service-spec-design.md +192 -0
- package/documentation/sources/reference/configuration-options.md +39 -1
- package/lib/httpcache.js +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -1
- package/lib/plone.d.ts +10 -0
- package/lib/plone.js +10 -4
- package/lib/service.d.ts +81 -0
- package/lib/service.js +24 -6
- package/lib/vinylcache.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# Configurable Service Spec Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make the generated Kubernetes `Service` spec configurable — curated typed fields (incl. `trafficDistribution`) plus a generic `overrides` escape hatch — plumbed through a single grouped `service` option on backend/frontend.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add a `PloneServiceSpec` interface in `src/service.ts` with curated optional fields and an `overrides?: k8s.ServiceSpec` escape hatch. `PloneService` merges construct-managed base (`ports`/`selector`) < curated fields < `overrides` (highest precedence). A single `service?: PloneServiceSpec` prop on `PloneBaseOptions` (shared by backend & frontend) carries the config; the legacy `serviceAnnotations` is deprecated and merged for backward compatibility.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, cdk8s (`k8s.KubeService` raw API), Jest snapshot tests, Projen build, JSII (multi-language publish).
|
|
10
|
+
|
|
11
|
+
**Reference spec:** `docs/superpowers/specs/2026-06-12-configurable-service-spec-design.md`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
- `src/service.ts` — new `PloneServiceSpec` interface; `PloneServiceOptions` gains `spec?`; merge logic in `PloneService` constructor.
|
|
18
|
+
- `src/plone.ts` — import `PloneServiceSpec`; add `service?` to `PloneBaseOptions`; deprecate `serviceAnnotations`; pass `spec` in backend & frontend `PloneService` calls.
|
|
19
|
+
- `test/service.test.ts` (+ `test/__snapshots__/service.test.ts.snap`) — new test cases.
|
|
20
|
+
- `documentation/sources/reference/configuration-options.md` — document `PloneServiceSpec`.
|
|
21
|
+
- `API.md` — regenerated via `npx projen docgen` (auto-generated, no manual edit).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Task 1: Add `PloneServiceSpec` type and merge logic in `PloneService`
|
|
26
|
+
|
|
27
|
+
**Files:**
|
|
28
|
+
- Modify: `src/service.ts:5-37` (interface area), `src/service.ts:58-82` (constructor)
|
|
29
|
+
- Test: `test/service.test.ts`
|
|
30
|
+
|
|
31
|
+
- [ ] **Step 1: Write failing tests for the new behavior**
|
|
32
|
+
|
|
33
|
+
Replace the contents of `test/service.test.ts` with:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Chart, Testing } from 'cdk8s';
|
|
37
|
+
import { PloneService } from '../src/service';
|
|
38
|
+
|
|
39
|
+
function synthService(props: any) {
|
|
40
|
+
const app = Testing.app();
|
|
41
|
+
const chart = new Chart(app, 'plone');
|
|
42
|
+
new PloneService(chart, 'test', props);
|
|
43
|
+
return Testing.synth(chart)[0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test('defaults', () => {
|
|
47
|
+
const app = Testing.app();
|
|
48
|
+
const chart = new Chart(app, 'plone');
|
|
49
|
+
new PloneService(chart, 'test', { targetPort: 8080, selectorLabel: { app: 'plone' } });
|
|
50
|
+
expect(Testing.synth(chart)).toMatchSnapshot();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('curated spec fields are applied', () => {
|
|
54
|
+
const manifest = synthService({
|
|
55
|
+
targetPort: 8080,
|
|
56
|
+
selectorLabel: { app: 'plone' },
|
|
57
|
+
spec: {
|
|
58
|
+
type: 'LoadBalancer',
|
|
59
|
+
trafficDistribution: 'PreferClose',
|
|
60
|
+
sessionAffinity: 'ClientIP',
|
|
61
|
+
externalTrafficPolicy: 'Local',
|
|
62
|
+
internalTrafficPolicy: 'Local',
|
|
63
|
+
publishNotReadyAddresses: true,
|
|
64
|
+
loadBalancerClass: 'service.k8s.aws/nlb',
|
|
65
|
+
loadBalancerSourceRanges: ['10.0.0.0/8'],
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
expect(manifest.spec.type).toBe('LoadBalancer');
|
|
69
|
+
expect(manifest.spec.trafficDistribution).toBe('PreferClose');
|
|
70
|
+
expect(manifest.spec.sessionAffinity).toBe('ClientIP');
|
|
71
|
+
expect(manifest.spec.externalTrafficPolicy).toBe('Local');
|
|
72
|
+
expect(manifest.spec.internalTrafficPolicy).toBe('Local');
|
|
73
|
+
expect(manifest.spec.publishNotReadyAddresses).toBe(true);
|
|
74
|
+
expect(manifest.spec.loadBalancerClass).toBe('service.k8s.aws/nlb');
|
|
75
|
+
expect(manifest.spec.loadBalancerSourceRanges).toEqual(['10.0.0.0/8']);
|
|
76
|
+
// construct-managed base still present
|
|
77
|
+
expect(manifest.spec.ports[0].port).toBe(8080);
|
|
78
|
+
expect(manifest.spec.selector).toEqual({ app: 'plone' });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('overrides escape hatch sets arbitrary spec fields', () => {
|
|
82
|
+
const manifest = synthService({
|
|
83
|
+
targetPort: 8080,
|
|
84
|
+
selectorLabel: { app: 'plone' },
|
|
85
|
+
spec: { overrides: { ipFamilyPolicy: 'PreferDualStack' } },
|
|
86
|
+
});
|
|
87
|
+
expect(manifest.spec.ipFamilyPolicy).toBe('PreferDualStack');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('overrides take precedence over curated fields', () => {
|
|
91
|
+
const manifest = synthService({
|
|
92
|
+
targetPort: 8080,
|
|
93
|
+
selectorLabel: { app: 'plone' },
|
|
94
|
+
spec: { type: 'ClusterIP', overrides: { type: 'NodePort' } },
|
|
95
|
+
});
|
|
96
|
+
expect(manifest.spec.type).toBe('NodePort');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('spec.annotations and spec.labels reach metadata', () => {
|
|
100
|
+
const manifest = synthService({
|
|
101
|
+
targetPort: 8080,
|
|
102
|
+
selectorLabel: { app: 'plone' },
|
|
103
|
+
spec: {
|
|
104
|
+
annotations: { 'external-dns.alpha.kubernetes.io/hostname': 'plone.example.com' },
|
|
105
|
+
labels: { tier: 'web' },
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
expect(manifest.metadata.annotations['external-dns.alpha.kubernetes.io/hostname']).toBe('plone.example.com');
|
|
109
|
+
expect(manifest.metadata.labels.tier).toBe('web');
|
|
110
|
+
// construct-managed labels still win/present
|
|
111
|
+
expect(manifest.metadata.labels['app.kubernetes.io/part-of']).toBe('plone');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('legacy options.annotations still merge with spec.annotations', () => {
|
|
115
|
+
const manifest = synthService({
|
|
116
|
+
targetPort: 8080,
|
|
117
|
+
selectorLabel: { app: 'plone' },
|
|
118
|
+
annotations: { a: '1' },
|
|
119
|
+
spec: { annotations: { b: '2' } },
|
|
120
|
+
});
|
|
121
|
+
expect(manifest.metadata.annotations).toEqual({ a: '1', b: '2' });
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
126
|
+
|
|
127
|
+
Run: `npx jest test/service.test.ts`
|
|
128
|
+
Expected: FAIL — new tests fail because `spec` is ignored (e.g. `manifest.spec.type` is `undefined`).
|
|
129
|
+
|
|
130
|
+
- [ ] **Step 3: Add `PloneServiceSpec` interface and `spec?` field in `src/service.ts`**
|
|
131
|
+
|
|
132
|
+
Insert the new interface immediately before `export interface PloneServiceOptions {` (before `src/service.ts:5`):
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
/**
|
|
136
|
+
* Configuration for the generated Kubernetes Service spec.
|
|
137
|
+
*
|
|
138
|
+
* Curated fields cover the common cases; use `overrides` as an escape hatch
|
|
139
|
+
* for any other `ServiceSpec` field. `overrides` has the highest precedence and
|
|
140
|
+
* can override every field, including the construct-managed `ports`/`selector`
|
|
141
|
+
* (at your own risk).
|
|
142
|
+
*/
|
|
143
|
+
export interface PloneServiceSpec {
|
|
144
|
+
/**
|
|
145
|
+
* Service type, e.g. ClusterIP | NodePort | LoadBalancer | ExternalName.
|
|
146
|
+
* @default - ClusterIP (Kubernetes default)
|
|
147
|
+
*/
|
|
148
|
+
readonly type?: string;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Traffic distribution preference, e.g. 'PreferClose' for topology-aware routing.
|
|
152
|
+
* @default - none
|
|
153
|
+
*/
|
|
154
|
+
readonly trafficDistribution?: string;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Session affinity, 'ClientIP' | 'None'.
|
|
158
|
+
* @default - None (Kubernetes default)
|
|
159
|
+
*/
|
|
160
|
+
readonly sessionAffinity?: string;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* External traffic policy, 'Cluster' | 'Local'.
|
|
164
|
+
* @default - Cluster (Kubernetes default)
|
|
165
|
+
*/
|
|
166
|
+
readonly externalTrafficPolicy?: string;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Internal traffic policy, 'Cluster' | 'Local'.
|
|
170
|
+
* @default - Cluster (Kubernetes default)
|
|
171
|
+
*/
|
|
172
|
+
readonly internalTrafficPolicy?: string;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Publish not-ready addresses (e.g. for headless services with StatefulSets).
|
|
176
|
+
* @default - false
|
|
177
|
+
*/
|
|
178
|
+
readonly publishNotReadyAddresses?: boolean;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Load balancer implementation class.
|
|
182
|
+
* @default - none
|
|
183
|
+
*/
|
|
184
|
+
readonly loadBalancerClass?: string;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Source IP ranges allowed to access a LoadBalancer service.
|
|
188
|
+
* @default - none
|
|
189
|
+
*/
|
|
190
|
+
readonly loadBalancerSourceRanges?: string[];
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Annotations to add to the Service metadata.
|
|
194
|
+
* @default - none
|
|
195
|
+
*/
|
|
196
|
+
readonly annotations?: { [name: string]: string };
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Extra labels to add to the Service metadata.
|
|
200
|
+
* @default - none
|
|
201
|
+
*/
|
|
202
|
+
readonly labels?: { [name: string]: string };
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Raw ServiceSpec overrides. Highest precedence — merged on top of all curated
|
|
206
|
+
* fields and the construct-managed base. Use for any field not covered above.
|
|
207
|
+
* @default - none
|
|
208
|
+
*/
|
|
209
|
+
readonly overrides?: k8s.ServiceSpec;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Then add a `spec?` field inside `PloneServiceOptions`, immediately after the existing `annotations` field (after `src/service.ts:36`, before the closing `}` at line 37):
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
/**
|
|
217
|
+
* Service spec configuration (type, trafficDistribution, sessionAffinity,
|
|
218
|
+
* raw overrides, ...).
|
|
219
|
+
* @default - construct-managed defaults only
|
|
220
|
+
*/
|
|
221
|
+
readonly spec?: PloneServiceSpec;
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- [ ] **Step 4: Implement the merge in the `PloneService` constructor**
|
|
225
|
+
|
|
226
|
+
Replace the constructor body (`src/service.ts:58-82`, from `const targetPort` through `this.labels = service_labels;`) with:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const targetPort = k8s.IntOrString.fromNumber(options.targetPort);
|
|
230
|
+
const selectorLabel = options.selectorLabel;
|
|
231
|
+
const userSpec = options.spec ?? {};
|
|
232
|
+
|
|
233
|
+
const service_labels = {
|
|
234
|
+
...userSpec.labels ?? {},
|
|
235
|
+
...options.labels ?? {},
|
|
236
|
+
'app.kubernetes.io/part-of': 'plone',
|
|
237
|
+
'app.kubernetes.io/managed-by': 'cdk8s-plone',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const mergedAnnotations = {
|
|
241
|
+
...options.annotations ?? {},
|
|
242
|
+
...userSpec.annotations ?? {},
|
|
243
|
+
};
|
|
244
|
+
const annotations = Object.keys(mergedAnnotations).length > 0 ? mergedAnnotations : undefined;
|
|
245
|
+
|
|
246
|
+
const spec: k8s.ServiceSpec = {
|
|
247
|
+
ports: [{ port: options.targetPort, targetPort: targetPort, name: options.portName ?? 'http' }],
|
|
248
|
+
selector: selectorLabel,
|
|
249
|
+
type: userSpec.type,
|
|
250
|
+
trafficDistribution: userSpec.trafficDistribution,
|
|
251
|
+
sessionAffinity: userSpec.sessionAffinity,
|
|
252
|
+
externalTrafficPolicy: userSpec.externalTrafficPolicy,
|
|
253
|
+
internalTrafficPolicy: userSpec.internalTrafficPolicy,
|
|
254
|
+
publishNotReadyAddresses: userSpec.publishNotReadyAddresses,
|
|
255
|
+
loadBalancerClass: userSpec.loadBalancerClass,
|
|
256
|
+
loadBalancerSourceRanges: userSpec.loadBalancerSourceRanges,
|
|
257
|
+
// overrides win over everything, including ports/selector
|
|
258
|
+
...userSpec.overrides ?? {},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const serviceOpts: k8s.KubeServiceProps = {
|
|
262
|
+
metadata: {
|
|
263
|
+
labels: service_labels,
|
|
264
|
+
annotations: annotations,
|
|
265
|
+
},
|
|
266
|
+
spec: spec,
|
|
267
|
+
};
|
|
268
|
+
const service = new k8s.KubeService(this, 'service', serviceOpts);
|
|
269
|
+
this.name = service.name;
|
|
270
|
+
this.labels = service_labels;
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
> Note: curated fields left `undefined` are dropped by cdk8s during synth, so the `defaults` snapshot stays unchanged.
|
|
274
|
+
|
|
275
|
+
- [ ] **Step 5: Run tests to verify they pass**
|
|
276
|
+
|
|
277
|
+
Run: `npx jest test/service.test.ts`
|
|
278
|
+
Expected: PASS for all new tests. The `defaults` snapshot should still match (no change). If it reports an obsolete/changed snapshot for `defaults`, confirm the diff is empty; do not `-u` yet.
|
|
279
|
+
|
|
280
|
+
- [ ] **Step 6: Lint**
|
|
281
|
+
|
|
282
|
+
Run: `npx projen eslint`
|
|
283
|
+
Expected: no errors in `src/service.ts`.
|
|
284
|
+
|
|
285
|
+
- [ ] **Step 7: Commit**
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
git add src/service.ts test/service.test.ts
|
|
289
|
+
git commit -m "feat(service): add configurable PloneServiceSpec with curated fields and overrides
|
|
290
|
+
|
|
291
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
292
|
+
|
|
293
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Task 2: Plumb `service` through `PloneBaseOptions` (backend & frontend)
|
|
299
|
+
|
|
300
|
+
**Files:**
|
|
301
|
+
- Modify: `src/plone.ts:8` (import), `src/plone.ts:242-248` (add `service`, deprecate `serviceAnnotations`), `src/plone.ts:468-477` (backend), `src/plone.ts:562-571` (frontend)
|
|
302
|
+
- Test: `test/plone.test.ts` (snapshot regeneration)
|
|
303
|
+
|
|
304
|
+
- [ ] **Step 1: Import `PloneServiceSpec`**
|
|
305
|
+
|
|
306
|
+
In `src/plone.ts:8`, change:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { PloneService } from './service';
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
to:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { PloneService, PloneServiceSpec } from './service';
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- [ ] **Step 2: Add `service` prop and deprecate `serviceAnnotations` in `PloneBaseOptions`**
|
|
319
|
+
|
|
320
|
+
In `src/plone.ts`, replace the existing `serviceAnnotations` doc-comment + field (currently `src/plone.ts:242-248`):
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
/**
|
|
324
|
+
* Annotations to add to the Service metadata.
|
|
325
|
+
* Common for external-dns, load balancers, service mesh, etc.
|
|
326
|
+
* @example { 'external-dns.alpha.kubernetes.io/hostname': 'plone.example.com' }
|
|
327
|
+
* @default - no additional annotations
|
|
328
|
+
*/
|
|
329
|
+
readonly serviceAnnotations?: { [name: string]: string };
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
with:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
/**
|
|
336
|
+
* Annotations to add to the Service metadata.
|
|
337
|
+
* Common for external-dns, load balancers, service mesh, etc.
|
|
338
|
+
* @example { 'external-dns.alpha.kubernetes.io/hostname': 'plone.example.com' }
|
|
339
|
+
* @deprecated use `service.annotations` instead
|
|
340
|
+
* @default - no additional annotations
|
|
341
|
+
*/
|
|
342
|
+
readonly serviceAnnotations?: { [name: string]: string };
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Service configuration: type, trafficDistribution, sessionAffinity,
|
|
346
|
+
* annotations/labels, and a raw `overrides` escape hatch for any other
|
|
347
|
+
* ServiceSpec field. Applies to this component's Service.
|
|
348
|
+
* @example { type: 'LoadBalancer', trafficDistribution: 'PreferClose' }
|
|
349
|
+
* @default - construct-managed defaults only
|
|
350
|
+
*/
|
|
351
|
+
readonly service?: PloneServiceSpec;
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
- [ ] **Step 3: Pass `spec` in the backend `PloneService` call**
|
|
355
|
+
|
|
356
|
+
In `src/plone.ts:468-477`, replace:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const backendService = new PloneService(backendDeployment, 'service', {
|
|
360
|
+
labels: {
|
|
361
|
+
'app.kubernetes.io/name': 'plone-backend-service',
|
|
362
|
+
'app.kubernetes.io/component': 'service',
|
|
363
|
+
},
|
|
364
|
+
targetPort: backendPort,
|
|
365
|
+
selectorLabel: { app: Names.toLabelValue(backendDeployment) },
|
|
366
|
+
portName: 'backend-http',
|
|
367
|
+
annotations: backend.serviceAnnotations,
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
with:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
const backendService = new PloneService(backendDeployment, 'service', {
|
|
375
|
+
labels: {
|
|
376
|
+
'app.kubernetes.io/name': 'plone-backend-service',
|
|
377
|
+
'app.kubernetes.io/component': 'service',
|
|
378
|
+
},
|
|
379
|
+
targetPort: backendPort,
|
|
380
|
+
selectorLabel: { app: Names.toLabelValue(backendDeployment) },
|
|
381
|
+
portName: 'backend-http',
|
|
382
|
+
spec: {
|
|
383
|
+
...backend.service,
|
|
384
|
+
annotations: { ...backend.serviceAnnotations, ...backend.service?.annotations },
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
- [ ] **Step 4: Pass `spec` in the frontend `PloneService` call**
|
|
390
|
+
|
|
391
|
+
In `src/plone.ts:562-571`, replace:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
const frontendService = new PloneService(frontendDeployment, 'service', {
|
|
395
|
+
labels: {
|
|
396
|
+
'app.kubernetes.io/name': 'plone-frontend-service',
|
|
397
|
+
'app.kubernetes.io/component': 'service',
|
|
398
|
+
},
|
|
399
|
+
targetPort: frontendPort,
|
|
400
|
+
selectorLabel: { app: Names.toLabelValue(frontendDeployment) },
|
|
401
|
+
portName: 'frontend-http',
|
|
402
|
+
annotations: frontend.serviceAnnotations,
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
with:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
const frontendService = new PloneService(frontendDeployment, 'service', {
|
|
410
|
+
labels: {
|
|
411
|
+
'app.kubernetes.io/name': 'plone-frontend-service',
|
|
412
|
+
'app.kubernetes.io/component': 'service',
|
|
413
|
+
},
|
|
414
|
+
targetPort: frontendPort,
|
|
415
|
+
selectorLabel: { app: Names.toLabelValue(frontendDeployment) },
|
|
416
|
+
portName: 'frontend-http',
|
|
417
|
+
spec: {
|
|
418
|
+
...frontend.service,
|
|
419
|
+
annotations: { ...frontend.serviceAnnotations, ...frontend.service?.annotations },
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
- [ ] **Step 5: Verify existing snapshots are unchanged (no regression)**
|
|
425
|
+
|
|
426
|
+
Run: `npx jest test/plone.test.ts`
|
|
427
|
+
Expected: PASS with NO snapshot changes — when neither `serviceAnnotations` nor `service` is set, `spec.annotations` resolves to `{}` → dropped to `undefined`, so output is identical.
|
|
428
|
+
If a snapshot diff appears, STOP and inspect: a non-empty diff means a regression, not an expected update.
|
|
429
|
+
|
|
430
|
+
- [ ] **Step 6: Add a Plone-level integration test for `service`**
|
|
431
|
+
|
|
432
|
+
Append to `test/plone.test.ts` (a test that exercises the plumbing end-to-end). First confirm the import style at the top of that file matches (it uses `import { Plone } from '../src/plone';`). Add:
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
test('backend service config is plumbed through', () => {
|
|
436
|
+
const app = Testing.app();
|
|
437
|
+
const chart = new Chart(app, 'plone');
|
|
438
|
+
new Plone(chart, 'plone', {
|
|
439
|
+
backend: {
|
|
440
|
+
service: {
|
|
441
|
+
type: 'LoadBalancer',
|
|
442
|
+
trafficDistribution: 'PreferClose',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
const manifests = Testing.synth(chart);
|
|
447
|
+
const backendSvc = manifests.find(
|
|
448
|
+
(m: any) => m.kind === 'Service' && m.metadata.labels['app.kubernetes.io/name'] === 'plone-backend-service',
|
|
449
|
+
);
|
|
450
|
+
expect(backendSvc.spec.type).toBe('LoadBalancer');
|
|
451
|
+
expect(backendSvc.spec.trafficDistribution).toBe('PreferClose');
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
> If `test/plone.test.ts` does not already import `Chart`/`Testing` from `cdk8s`, add `import { Chart, Testing } from 'cdk8s';` at the top (check first — most test files in this repo already do).
|
|
456
|
+
|
|
457
|
+
- [ ] **Step 7: Run the new integration test**
|
|
458
|
+
|
|
459
|
+
Run: `npx jest test/plone.test.ts -t 'backend service config is plumbed through'`
|
|
460
|
+
Expected: PASS.
|
|
461
|
+
|
|
462
|
+
- [ ] **Step 8: Run full test suite + lint**
|
|
463
|
+
|
|
464
|
+
Run: `npx projen test`
|
|
465
|
+
Expected: all suites PASS, no unexpected snapshot changes.
|
|
466
|
+
Run: `npx projen eslint`
|
|
467
|
+
Expected: no errors.
|
|
468
|
+
|
|
469
|
+
- [ ] **Step 9: Commit**
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
git add src/plone.ts test/plone.test.ts test/__snapshots__/
|
|
473
|
+
git commit -m "feat(plone): expose service config on backend/frontend, deprecate serviceAnnotations
|
|
474
|
+
|
|
475
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
476
|
+
|
|
477
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Task 3: Documentation
|
|
483
|
+
|
|
484
|
+
**Files:**
|
|
485
|
+
- Modify: `documentation/sources/reference/configuration-options.md:200-220` (Annotations section → add Service section)
|
|
486
|
+
- Regenerate: `API.md`
|
|
487
|
+
|
|
488
|
+
- [ ] **Step 1: Invoke the doc-style skill**
|
|
489
|
+
|
|
490
|
+
Before editing the handwritten docs under `documentation/sources/`, invoke the `plone-doc-style:author` skill (Diataxis classification, MyST markup, style enforcement). The `configuration-options.md` page is **Reference** quadrant. Do NOT apply the skill to the auto-generated `API.md`.
|
|
491
|
+
|
|
492
|
+
- [ ] **Step 2: Document `PloneServiceSpec` in the Reference page**
|
|
493
|
+
|
|
494
|
+
In `documentation/sources/reference/configuration-options.md`, locate the "Annotations" subsection (around line 200). Mark `serviceAnnotations` as deprecated in its table row and add a new "Service" subsection after the Annotations example block (after line 220). Insert:
|
|
495
|
+
|
|
496
|
+
```markdown
|
|
497
|
+
#### Service
|
|
498
|
+
|
|
499
|
+
Configure the generated Kubernetes `Service` via the grouped `service` option
|
|
500
|
+
(available on both `backend` and `frontend`). Curated fields cover common cases;
|
|
501
|
+
`overrides` is an escape hatch for any other `ServiceSpec` field and has the
|
|
502
|
+
highest precedence.
|
|
503
|
+
|
|
504
|
+
| Property | Type | Description |
|
|
505
|
+
|----------|------|-------------|
|
|
506
|
+
| `service.type` | `string` | Service type: `ClusterIP` (default), `NodePort`, `LoadBalancer`, `ExternalName` |
|
|
507
|
+
| `service.trafficDistribution` | `string` | Traffic distribution preference, e.g. `PreferClose` |
|
|
508
|
+
| `service.sessionAffinity` | `string` | `ClientIP` or `None` |
|
|
509
|
+
| `service.externalTrafficPolicy` | `string` | `Cluster` or `Local` |
|
|
510
|
+
| `service.internalTrafficPolicy` | `string` | `Cluster` or `Local` |
|
|
511
|
+
| `service.publishNotReadyAddresses` | `boolean` | Publish not-ready addresses |
|
|
512
|
+
| `service.loadBalancerClass` | `string` | Load balancer implementation class |
|
|
513
|
+
| `service.loadBalancerSourceRanges` | `string[]` | Allowed source IP ranges for LoadBalancer |
|
|
514
|
+
| `service.annotations` | `Record<string, string>` | Service metadata annotations (replaces `serviceAnnotations`) |
|
|
515
|
+
| `service.labels` | `Record<string, string>` | Extra Service metadata labels |
|
|
516
|
+
| `service.overrides` | `ServiceSpec` | Raw spec overrides; highest precedence |
|
|
517
|
+
|
|
518
|
+
> `serviceAnnotations` is deprecated — use `service.annotations` instead.
|
|
519
|
+
|
|
520
|
+
**Example:**
|
|
521
|
+
```typescript
|
|
522
|
+
backend: {
|
|
523
|
+
service: {
|
|
524
|
+
type: 'LoadBalancer',
|
|
525
|
+
trafficDistribution: 'PreferClose',
|
|
526
|
+
loadBalancerSourceRanges: ['10.0.0.0/8'],
|
|
527
|
+
annotations: {
|
|
528
|
+
'external-dns.alpha.kubernetes.io/hostname': 'backend.example.com',
|
|
529
|
+
},
|
|
530
|
+
overrides: {
|
|
531
|
+
ipFamilyPolicy: 'PreferDualStack',
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
- [ ] **Step 3: Update the deprecated `serviceAnnotations` table row**
|
|
539
|
+
|
|
540
|
+
In the same file, change the `serviceAnnotations` row (line ~206) to:
|
|
541
|
+
|
|
542
|
+
```markdown
|
|
543
|
+
| `serviceAnnotations` | `Record<string, string>` | **Deprecated** — use `service.annotations`. Service annotations (e.g., for external-dns) |
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
- [ ] **Step 4: Regenerate API docs**
|
|
547
|
+
|
|
548
|
+
Run: `npx projen docgen`
|
|
549
|
+
Expected: `API.md` updated to include `PloneServiceSpec` and the new `service` property.
|
|
550
|
+
|
|
551
|
+
- [ ] **Step 5: Build the docs to verify**
|
|
552
|
+
|
|
553
|
+
Run: `cd documentation && make docs`
|
|
554
|
+
Expected: build succeeds with no errors/warnings for the edited page.
|
|
555
|
+
|
|
556
|
+
- [ ] **Step 6: Commit**
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
git add documentation/sources/reference/configuration-options.md API.md
|
|
560
|
+
git commit -m "docs(service): document PloneServiceSpec configuration
|
|
561
|
+
|
|
562
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
563
|
+
|
|
564
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Task 4: Final verification & build
|
|
570
|
+
|
|
571
|
+
- [ ] **Step 1: Full build**
|
|
572
|
+
|
|
573
|
+
Run: `npx projen build`
|
|
574
|
+
Expected: compile + JSII bindings succeed (confirms `k8s.ServiceSpec` in the public API is JSII-compatible), tests pass, no lint errors.
|
|
575
|
+
|
|
576
|
+
- [ ] **Step 2: Confirm snapshots intentional**
|
|
577
|
+
|
|
578
|
+
Run: `git status` and `git diff --stat`
|
|
579
|
+
Expected: only intended files changed. If `test/__snapshots__/service.test.ts.snap` changed beyond the new test cases, inspect the diff — the `defaults` snapshot must be unchanged.
|
|
580
|
+
|
|
581
|
+
- [ ] **Step 3: Push and open PR**
|
|
582
|
+
|
|
583
|
+
```bash
|
|
584
|
+
git push -u origin feat/configurable-service-spec
|
|
585
|
+
gh pr create --title "feat(service): configurable Service spec (trafficDistribution + overrides)" --body "$(cat <<'EOF'
|
|
586
|
+
## Summary
|
|
587
|
+
- Add `PloneServiceSpec` with curated fields (`type`, `trafficDistribution`, `sessionAffinity`, traffic policies, load balancer settings) plus a generic `overrides` escape hatch.
|
|
588
|
+
- Plumb a single grouped `service?` option through `PloneBaseOptions` (backend & frontend).
|
|
589
|
+
- Deprecate `serviceAnnotations` in favor of `service.annotations` (backward compatible, merged).
|
|
590
|
+
- Document in reference docs; regenerate API.md.
|
|
591
|
+
|
|
592
|
+
## Test plan
|
|
593
|
+
- New unit tests in `test/service.test.ts` (curated fields, overrides precedence, annotations/labels merge, legacy compat).
|
|
594
|
+
- Integration test in `test/plone.test.ts` (end-to-end plumbing).
|
|
595
|
+
- Existing snapshots unchanged when `service` is unset.
|
|
596
|
+
|
|
597
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
598
|
+
EOF
|
|
599
|
+
)"
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## Self-Review notes
|
|
605
|
+
|
|
606
|
+
- **Spec coverage:** §1 PloneServiceSpec → Task 1 Step 3; §2 PloneServiceOptions.spec → Task 1 Step 3; §3 merge precedence → Task 1 Step 4 + tests; §4 plumbing → Task 2; §5 backward-compat → Task 1 Step 4 (annotation merge) + Task 2 Step 5; §6 tests → Task 1 Step 1, Task 2 Step 6; §7 docs → Task 3.
|
|
607
|
+
- **Type consistency:** `PloneServiceSpec` (with `overrides?: k8s.ServiceSpec`) used identically in `service.ts`, `PloneServiceOptions.spec`, and `PloneBaseOptions.service`. Field names match across interface, merge code, tests, and docs.
|
|
608
|
+
- **No placeholders:** all steps contain concrete code/commands.
|