@ankhorage/infra 5.1.1 → 5.1.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ankhorage/infra
2
2
 
3
+ ## 5.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 343eefe: Prove in the real local Minikube acceptance that reconciliation prunes stale Infra-owned resources without deleting unrelated Kubernetes resources.
8
+
3
9
  ## 5.1.1
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/infra",
3
3
  "type": "module",
4
- "version": "5.1.1",
4
+ "version": "5.1.2",
5
5
  "description": "Project-aware infrastructure generation library and executable Ankh provider for Ankhorage.",
6
6
  "homepage": "https://github.com/ankhorage/infra#readme",
7
7
  "bugs": {
@@ -17,8 +17,12 @@ import { upInfraEnvironmentAsync } from './features/environment-lifecycle/applic
17
17
  import type { InfraOrchestrationDependencies } from './types/infraOrchestration.js';
18
18
 
19
19
  const projectId = 'infra145-minikube-supabase';
20
+ const acceptanceNamespace = `${projectId}-local`;
20
21
  const baseUrl = 'http://127.0.0.1:54321';
21
22
  const bucket = 'phase8-acceptance';
23
+ const staleOwnedConfigMap = 'phase8-stale-owned';
24
+ const unrelatedConfigMap = 'phase8-unrelated';
25
+ const staleOwnedResourceId = 'stale:phase8-owned';
22
26
  const jwtSecret = 'phase8-jwt-secret-that-is-at-least-thirty-two-characters';
23
27
  const credentials = {
24
28
  postgresPassword: 'phase8-postgres-password',
@@ -94,6 +98,32 @@ test.skipIf(process.env.ANKH_INFRA_MINIKUBE_SUPABASE_E2E !== '1')(
94
98
  );
95
99
  expect(convergedPlan.actions.every(({ operation }) => operation === 'noop')).toBe(true);
96
100
 
101
+ await createStaleOwnershipFixturesAsync();
102
+ const stalePlan = requireSuccess(
103
+ await planInfraEnvironmentAsync({ projectId, manifest, previous: ledger }, dependencies),
104
+ );
105
+ expect(
106
+ stalePlan.actions.some(
107
+ ({ operation, owner }) =>
108
+ operation === 'delete' && owner.resourceId === staleOwnedResourceId,
109
+ ),
110
+ ).toBe(true);
111
+
112
+ const { ledger: prunedLedger } = requireSuccess(
113
+ await upInfraEnvironmentAsync({ projectId, manifest, previous: ledger }, dependencies),
114
+ );
115
+ ledger = prunedLedger;
116
+ expect(await configMapExistsAsync(staleOwnedConfigMap)).toBe(false);
117
+ expect(await configMapExistsAsync(unrelatedConfigMap)).toBe(true);
118
+ await runKubectlAsync([
119
+ 'delete',
120
+ 'configmap',
121
+ unrelatedConfigMap,
122
+ '--namespace',
123
+ acceptanceNamespace,
124
+ '--ignore-not-found=true',
125
+ ]);
126
+
97
127
  const { ledger: downLedger } = requireSuccess(
98
128
  await downInfraEnvironmentAsync({ projectId, manifest, previous: ledger }, dependencies),
99
129
  );
@@ -128,6 +158,75 @@ test.skipIf(process.env.ANKH_INFRA_MINIKUBE_SUPABASE_E2E !== '1')(
128
158
  900_000,
129
159
  );
130
160
 
161
+ async function createStaleOwnershipFixturesAsync(): Promise<void> {
162
+ await runKubectlAsync([
163
+ 'create',
164
+ 'configmap',
165
+ staleOwnedConfigMap,
166
+ '--namespace',
167
+ acceptanceNamespace,
168
+ '--from-literal=marker=stale',
169
+ ]);
170
+ await runKubectlAsync([
171
+ 'label',
172
+ 'configmap',
173
+ staleOwnedConfigMap,
174
+ '--namespace',
175
+ acceptanceNamespace,
176
+ 'app.kubernetes.io/managed-by=ankhorage-infra',
177
+ `infra.ankhorage.dev/project=${projectId}`,
178
+ 'infra.ankhorage.dev/environment=local',
179
+ ]);
180
+ await runKubectlAsync([
181
+ 'annotate',
182
+ 'configmap',
183
+ staleOwnedConfigMap,
184
+ '--namespace',
185
+ acceptanceNamespace,
186
+ `infra.ankhorage.dev/project-id=${projectId}`,
187
+ 'infra.ankhorage.dev/environment=local',
188
+ 'infra.ankhorage.dev/adapter=minikube',
189
+ `infra.ankhorage.dev/resource-id=${staleOwnedResourceId}`,
190
+ 'infra.ankhorage.dev/persistent=false',
191
+ 'infra.ankhorage.dev/retention=retain',
192
+ 'infra.ankhorage.dev/depends-on=[]',
193
+ ]);
194
+ await runKubectlAsync([
195
+ 'create',
196
+ 'configmap',
197
+ unrelatedConfigMap,
198
+ '--namespace',
199
+ acceptanceNamespace,
200
+ '--from-literal=marker=unrelated',
201
+ ]);
202
+ }
203
+
204
+ async function configMapExistsAsync(name: string): Promise<boolean> {
205
+ const output = await runKubectlAsync([
206
+ 'get',
207
+ 'configmap',
208
+ name,
209
+ '--namespace',
210
+ acceptanceNamespace,
211
+ '--ignore-not-found=true',
212
+ '--output=name',
213
+ ]);
214
+ return output === `configmap/${name}`;
215
+ }
216
+
217
+ async function runKubectlAsync(args: readonly string[]): Promise<string> {
218
+ const process = Bun.spawn(['kubectl', ...args], { stdout: 'pipe', stderr: 'pipe' });
219
+ const [exitCode, stdout, stderr] = await Promise.all([
220
+ process.exited,
221
+ new Response(process.stdout).text(),
222
+ new Response(process.stderr).text(),
223
+ ]);
224
+ if (exitCode !== 0) {
225
+ throw new Error(`kubectl ${args.join(' ')} failed: ${stderr.trim()}`);
226
+ }
227
+ return stdout.trim();
228
+ }
229
+
131
230
  function createDependencies(): InfraOrchestrationDependencies {
132
231
  return {
133
232
  adapterResolver: createNodeInfraAdapterPackageResolver(),