@nitra/cfr 0.5.0 → 0.6.0

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/README.md CHANGED
@@ -93,10 +93,16 @@ heard of, and never will until someone points it out.
93
93
 
94
94
  `kcc-inventory` is that someone. Per namespace (any namespace carrying the
95
95
  annotation `cnrm.cloud.google.com/project-id`), it compares what's live in
96
- the GCP project against what's declared under KCC, for
97
- `IAMServiceAccount`, `IAMServiceAccountKey`, `ArtifactRegistryRepository`,
98
- `ContainerCluster`, `ContainerNodePool`, `StorageBucket`, `ComputeAddress`,
99
- `DNSManagedZone`, `DNSRecordSet`, and `IAMPolicyMember`.
96
+ the GCP project against what's declared under KCC. Besides IAM, GKE,
97
+ Artifact Registry, buckets, addresses and Cloud DNS, it includes Cloud Run
98
+ (`RunService`, `RunJob`), `CloudSchedulerJob`, `EventarcTrigger`, Pub/Sub,
99
+ Secret Manager, VPC Access, KMS, and the Cloud Run HTTP(S) load-balancer
100
+ chain (network, subnetwork, backend service, serverless NEG, URL map,
101
+ target HTTPS proxy, global forwarding rule, and SSL certificates).
102
+
103
+ Location-scoped resources use the canonical `location/name` ID, preventing
104
+ resources with the same name in different regions from being merged. IAM
105
+ bindings on a `RunService` are normalized to the same identity.
100
106
 
101
107
  Read-only — it reports, it doesn't touch anything.
102
108
 
@@ -189,6 +195,10 @@ carries the same stale-cache/GKE-managed notes described above.
189
195
  Same requirements as `kcc-inventory` — no `gcloud`/`kubectl` needed, GKE
190
196
  only, `--include-system` to see GCP-managed noise.
191
197
 
198
+ ## Changelog
199
+
200
+ See [CHANGELOG.md](https://github.com/nitra/cfr/blob/main/CHANGELOG.md).
201
+
192
202
  ## License
193
203
 
194
204
  MIT
@@ -1,5 +1,5 @@
1
1
  // Сирий збір фактів для одного KCC namespace: що є "живого" в GCP-проєкті і
2
- // що заявлено як Config Connector CR у цьому namespace — для кожного з 10
2
+ // що заявлено як Config Connector CR у цьому namespace — для кожного з
3
3
  // видів ресурсів. Жодного diff-у, жодного форматування, жодного I/O в
4
4
  // консоль: тільки звернення до GCP REST API (gcp-rest.mjs) і Kubernetes
5
5
  // REST API (k8s-rest.mjs), і нормалізація результату в плаский список.
@@ -24,6 +24,23 @@ export const KIND_ORDER = [
24
24
  'ComputeAddress',
25
25
  'DNSManagedZone',
26
26
  'DNSRecordSet',
27
+ 'RunService',
28
+ 'RunJob',
29
+ 'CloudSchedulerJob',
30
+ 'EventarcTrigger',
31
+ 'PubSubTopic',
32
+ 'PubSubSubscription',
33
+ 'SecretManagerSecret',
34
+ 'VPCAccessConnector',
35
+ 'ComputeNetwork',
36
+ 'ComputeSubnetwork',
37
+ 'KMSCryptoKey',
38
+ 'ComputeBackendService',
39
+ 'ComputeNetworkEndpointGroup',
40
+ 'ComputeURLMap',
41
+ 'ComputeTargetHTTPSProxy',
42
+ 'ComputeGlobalForwardingRule',
43
+ 'ComputeSSLCertificate',
27
44
  'IAMPolicyMember',
28
45
  ];
29
46
 
@@ -62,7 +79,7 @@ async function listRegionalAddressIds(project) {
62
79
  for (const [scopeKey, val] of Object.entries(data.items || {})) {
63
80
  if (!val.addresses) continue;
64
81
  const region = scopeKey.replace(/^regions\//, '');
65
- for (const a of val.addresses) out.push(`${a.name}/${region}`);
82
+ for (const a of val.addresses) out.push(`${region}/${a.name}`);
66
83
  }
67
84
  pageToken = data.nextPageToken;
68
85
  } while (pageToken);
@@ -71,13 +88,46 @@ async function listRegionalAddressIds(project) {
71
88
 
72
89
  async function listGlobalAddressIds(project) {
73
90
  const items = await paginate(`${COMPUTE_API}/projects/${project}/global/addresses`, { pageSize: 500 }, 'items');
74
- return items.map((a) => `${a.name}/global`);
91
+ return items.map((a) => `global/${a.name}`);
75
92
  }
76
93
 
77
94
  function stripPrefix(s, prefix) {
78
95
  return s.startsWith(prefix) ? s.slice(prefix.length) : s;
79
96
  }
80
97
 
98
+ // Один ID для всіх location/region/zone ресурсів: location/name. Це не дає
99
+ // однаковим іменам у різних регіонах зливатися в один inventory запис.
100
+ function assetScopedId(asset, collection) {
101
+ const m = (asset.name || '').match(new RegExp(`/(?:locations|regions|zones)/([^/]+)/${collection}/([^/]+)$`));
102
+ return m && `${m[1]}/${m[2]}`;
103
+ }
104
+
105
+ function kccScopedId(item, { defaultLocation, name = (i) => i.spec && i.spec.resourceID } = {}) {
106
+ const spec = item.spec || {};
107
+ const resourceName = name(item) || (item.metadata && item.metadata.name);
108
+ const location = spec.location || spec.region || spec.zone || defaultLocation;
109
+ return location && resourceName ? `${location}/${resourceName}` : null;
110
+ }
111
+
112
+ function resourceId(item) {
113
+ return (item.spec && item.spec.resourceID) || (item.metadata && item.metadata.name);
114
+ }
115
+
116
+ function liveScopedIds(assets, type, collection) {
117
+ return assets.filter((a) => a.assetType === type).map((a) => assetScopedId(a, collection)).filter(Boolean);
118
+ }
119
+
120
+ function assetGlobalOrScopedId(asset, collection) {
121
+ const scoped = assetScopedId(asset, collection);
122
+ if (scoped) return scoped;
123
+ const global = (asset.name || '').match(new RegExp(`/global/${collection}/([^/]+)$`));
124
+ return global && `global/${global[1]}`;
125
+ }
126
+
127
+ function liveGlobalOrScopedIds(assets, type, collection) {
128
+ return assets.filter((a) => a.assetType === type).map((a) => assetGlobalOrScopedId(a, collection)).filter(Boolean);
129
+ }
130
+
81
131
  // --- фільтри GCP-системного шуму (див. includeSystem) ----------------------
82
132
 
83
133
  const SA_SYSTEM = [
@@ -121,6 +171,8 @@ function assetResId(entry, project) {
121
171
  const mAr = res.match(/\/repositories\/([^/]+)$/);
122
172
  if (mAr && assetType === 'artifactregistry.googleapis.com/Repository') return `ar/${mAr[1]}`;
123
173
  if (assetType === 'storage.googleapis.com/Bucket') return 'bucket/' + stripPrefix(res, '//storage.googleapis.com/');
174
+ const mRunService = res.match(/\/projects\/[^/]+\/locations\/([^/]+)\/services\/([^/]+)$/);
175
+ if (mRunService && assetType === 'run.googleapis.com/Service') return `run-service/${mRunService[1]}/${mRunService[2]}`;
124
176
  return 'other/' + stripPrefix(res, '//');
125
177
  }
126
178
 
@@ -141,7 +193,7 @@ function kccSaId(ref, project) {
141
193
  return '?';
142
194
  }
143
195
 
144
- function kccPolicyMemberId(item, project) {
196
+ function kccPolicyMemberId(item, project, runServiceRefs) {
145
197
  const ref = (item.spec && item.spec.resourceRef) || {};
146
198
  switch (ref.kind) {
147
199
  case 'Project':
@@ -152,6 +204,12 @@ function kccPolicyMemberId(item, project) {
152
204
  return 'ar/' + kccArId(ref);
153
205
  case 'StorageBucket':
154
206
  return 'bucket/' + (ref.external || ref.name || '?');
207
+ case 'RunService': {
208
+ const external = ref.external || '';
209
+ const m = external.match(/projects\/[^/]+\/locations\/([^/]+)\/services\/([^/]+)$/);
210
+ if (m) return `run-service/${m[1]}/${m[2]}`;
211
+ return runServiceRefs.get(ref.name) || `run-service/?/${ref.name || '?'}`;
212
+ }
155
213
  default:
156
214
  return 'other/' + (ref.external || ref.name || '?');
157
215
  }
@@ -227,39 +285,36 @@ export async function collectNamespace(namespace, { includeSystem = false } = {}
227
285
  )).map((n) => n.split('/').pop()), 'kcc');
228
286
 
229
287
  // --- ArtifactRegistryRepository -------------------------------------------
230
- let liveAr = byType('artifactregistry.googleapis.com/Repository')
231
- .map((a) => {
232
- const m = (a.name || '').match(/\/repositories\/([^/]+)$/);
233
- return m && m[1];
234
- })
235
- .filter(Boolean);
288
+ let liveAr = liveScopedIds(assets, 'artifactregistry.googleapis.com/Repository', 'repositories');
236
289
  if (!includeSystem) liveAr = liveAr.filter((r) => !isSystem(AR_SYSTEM, r));
237
290
  push('ArtifactRegistryRepository', liveAr, 'gcp');
238
291
  push('ArtifactRegistryRepository', await kccField(
239
292
  'ArtifactRegistryRepository', namespace,
240
- (i) => i.spec && i.spec.resourceID,
293
+ (i) => kccScopedId(i),
241
294
  ), 'kcc');
242
295
 
243
296
  // --- ContainerCluster + ContainerNodePool ---------------------------------
244
- push('ContainerCluster', byType('container.googleapis.com/Cluster').map((a) => a.displayName), 'gcp');
245
- push('ContainerCluster', await kccField(
246
- 'ContainerCluster', namespace,
247
- (i) => i.spec && i.spec.resourceID,
248
- ), 'kcc');
297
+ push('ContainerCluster', liveScopedIds(assets, 'container.googleapis.com/Cluster', 'clusters'), 'gcp');
298
+ const containerClusterItems = await listCustomObjects('ContainerCluster', namespace);
299
+ push('ContainerCluster', containerClusterItems.map((i) => kccScopedId(i)).filter(Boolean), 'kcc');
300
+ const clusterRefs = new Map(containerClusterItems.map((i) => [i.metadata && i.metadata.name, kccScopedId(i)]));
249
301
 
250
302
  const livePool = [];
251
303
  for (const a of byType('container.googleapis.com/NodePool')) {
252
304
  const m = (a.name || '').match(/\/clusters\/([^/]+)\/nodePools\/([^/]+)$/);
253
305
  if (!m) continue;
254
306
  const [, cluster, pool] = m;
255
- if (includeSystem || !NODEPOOL_SYSTEM.test(pool)) livePool.push(`${cluster}/${pool}`);
307
+ const location = (a.name || '').match(/\/locations\/([^/]+)\/clusters\/[^/]+\/nodePools\/[^/]+$/)?.[1];
308
+ if (includeSystem || !NODEPOOL_SYSTEM.test(pool)) livePool.push(location ? `${location}/${cluster}/${pool}` : `${cluster}/${pool}`);
256
309
  }
257
310
  push('ContainerNodePool', livePool, 'gcp');
258
311
  push('ContainerNodePool', await kccField(
259
312
  'ContainerNodePool', namespace,
260
313
  (i) => {
261
314
  const ref = (i.spec && i.spec.clusterRef) || {};
262
- return `${ref.name || ref.external}/${i.spec && i.spec.resourceID}`;
315
+ const external = ref.external || '';
316
+ const m = external.match(/\/locations\/([^/]+)\/clusters\/([^/]+)$/);
317
+ return m ? `${m[1]}/${m[2]}/${resourceId(i)}` : `${clusterRefs.get(ref.name) || ref.name || external}/${resourceId(i)}`;
263
318
  },
264
319
  ), 'kcc');
265
320
 
@@ -286,14 +341,14 @@ export async function collectNamespace(namespace, { includeSystem = false } = {}
286
341
  const liveAddrIds = new Set([...regionalAddrIds, ...globalAddrIds]);
287
342
  const staleAddrCount0 = byType('compute.googleapis.com/Address', 'compute.googleapis.com/GlobalAddress').length;
288
343
  const liveAddr = byType('compute.googleapis.com/Address', 'compute.googleapis.com/GlobalAddress')
289
- .map((a) => `${a.displayName}/${a.location === 'global' ? 'global' : a.location}`)
344
+ .map((a) => `${a.location === 'global' ? 'global' : a.location}/${a.displayName}`)
290
345
  .filter((id) => liveAddrIds.has(id));
291
346
  const staleAddrCount = staleAddrCount0 - liveAddr.length;
292
347
  if (staleAddrCount) diagnostics.push({ kind: 'ComputeAddress', type: 'stale-cache', count: staleAddrCount });
293
348
  push('ComputeAddress', liveAddr, 'gcp');
294
349
  push('ComputeAddress', await kccField(
295
350
  'ComputeAddress', namespace,
296
- (i) => `${i.spec && i.spec.resourceID}/${(i.spec && i.spec.location) || 'global'}`,
351
+ (i) => kccScopedId(i, { defaultLocation: 'global' }),
297
352
  ), 'kcc');
298
353
 
299
354
  // --- DNSManagedZone + DNSRecordSet -------------------------------------------
@@ -352,6 +407,72 @@ export async function collectNamespace(namespace, { includeSystem = false } = {}
352
407
  },
353
408
  ), 'kcc');
354
409
 
410
+ // --- Cloud Run та його тригери/залежності ---------------------------------
411
+ push('RunService', liveScopedIds(assets, 'run.googleapis.com/Service', 'services'), 'gcp');
412
+ const runServiceItems = await listCustomObjects('RunService', namespace);
413
+ push('RunService', runServiceItems.map((i) => kccScopedId(i)).filter(Boolean), 'kcc');
414
+
415
+ push('RunJob', liveScopedIds(assets, 'run.googleapis.com/Job', 'jobs'), 'gcp');
416
+ push('RunJob', await kccField('RunJob', namespace, (i) => kccScopedId(i)), 'kcc');
417
+
418
+ push('CloudSchedulerJob', liveScopedIds(assets, 'cloudscheduler.googleapis.com/Job', 'jobs'), 'gcp');
419
+ push('CloudSchedulerJob', await kccField('CloudSchedulerJob', namespace, (i) => kccScopedId(i)), 'kcc');
420
+
421
+ push('EventarcTrigger', liveScopedIds(assets, 'eventarc.googleapis.com/Trigger', 'triggers'), 'gcp');
422
+ push('EventarcTrigger', await kccField('EventarcTrigger', namespace, (i) => kccScopedId(i)), 'kcc');
423
+
424
+ push('PubSubTopic', byType('pubsub.googleapis.com/Topic').map((a) => a.displayName), 'gcp');
425
+ push('PubSubTopic', await kccField('PubSubTopic', namespace, resourceId), 'kcc');
426
+ push('PubSubSubscription', byType('pubsub.googleapis.com/Subscription').map((a) => a.displayName), 'gcp');
427
+ push('PubSubSubscription', await kccField('PubSubSubscription', namespace, resourceId), 'kcc');
428
+
429
+ push('SecretManagerSecret', byType('secretmanager.googleapis.com/Secret').map((a) => a.displayName), 'gcp');
430
+ push('SecretManagerSecret', await kccField('SecretManagerSecret', namespace, resourceId), 'kcc');
431
+
432
+ push('VPCAccessConnector', liveScopedIds(assets, 'vpcaccess.googleapis.com/Connector', 'connectors'), 'gcp');
433
+ push('VPCAccessConnector', await kccField('VPCAccessConnector', namespace, (i) => kccScopedId(i)), 'kcc');
434
+ push('ComputeNetwork', liveGlobalOrScopedIds(assets, 'compute.googleapis.com/Network', 'networks'), 'gcp');
435
+ push('ComputeNetwork', await kccField('ComputeNetwork', namespace, (i) => kccScopedId(i, { defaultLocation: 'global' })), 'kcc');
436
+ push('ComputeSubnetwork', liveScopedIds(assets, 'compute.googleapis.com/Subnetwork', 'subnetworks'), 'gcp');
437
+ push('ComputeSubnetwork', await kccField('ComputeSubnetwork', namespace, (i) => kccScopedId(i)), 'kcc');
438
+
439
+ // KMS key ідентифікується не лише ім'ям: keyRing теж є частиною URI.
440
+ const cryptoKeyId = (a) => {
441
+ const m = (a.name || '').match(/\/locations\/([^/]+)\/keyRings\/([^/]+)\/cryptoKeys\/([^/]+)$/);
442
+ return m && `${m[1]}/${m[2]}/${m[3]}`;
443
+ };
444
+ push('KMSCryptoKey', byType('cloudkms.googleapis.com/CryptoKey').map(cryptoKeyId).filter(Boolean), 'gcp');
445
+ push('KMSCryptoKey', await kccField('KMSCryptoKey', namespace, (i) => {
446
+ const ref = (i.spec && i.spec.keyRingRef) || {};
447
+ const m = (ref.external || '').match(/\/locations\/([^/]+)\/keyRings\/([^/]+)$/);
448
+ return m ? `${m[1]}/${m[2]}/${resourceId(i)}` : null;
449
+ }), 'kcc');
450
+
451
+ // --- HTTP(S) LB, що часто стоїть перед Cloud Run ---------------------------
452
+ push('ComputeBackendService', liveGlobalOrScopedIds(assets, 'compute.googleapis.com/BackendService', 'backendServices'), 'gcp');
453
+ push('ComputeBackendService', await kccField('ComputeBackendService', namespace, (i) => kccScopedId(i, { defaultLocation: 'global' })), 'kcc');
454
+ const isServerlessNeg = (a) => (a.additionalAttributes && a.additionalAttributes.networkEndpointType) === 'SERVERLESS'
455
+ || (a.resource && a.resource.data && a.resource.data.networkEndpointType) === 'SERVERLESS';
456
+ push('ComputeNetworkEndpointGroup', byType('compute.googleapis.com/NetworkEndpointGroup')
457
+ .filter(isServerlessNeg).map((a) => assetGlobalOrScopedId(a, 'networkEndpointGroups')).filter(Boolean), 'gcp');
458
+ push('ComputeNetworkEndpointGroup', await kccField('ComputeNetworkEndpointGroup', namespace, (i) => {
459
+ const type = i.spec && i.spec.networkEndpointType;
460
+ return type === 'SERVERLESS' ? kccScopedId(i, { defaultLocation: 'global' }) : null;
461
+ }), 'kcc');
462
+ push('ComputeURLMap', liveGlobalOrScopedIds(assets, 'compute.googleapis.com/UrlMap', 'urlMaps'), 'gcp');
463
+ push('ComputeURLMap', await kccField('ComputeURLMap', namespace, (i) => kccScopedId(i, { defaultLocation: 'global' })), 'kcc');
464
+ push('ComputeTargetHTTPSProxy', liveGlobalOrScopedIds(assets, 'compute.googleapis.com/TargetHttpsProxy', 'targetHttpsProxies'), 'gcp');
465
+ push('ComputeTargetHTTPSProxy', await kccField('ComputeTargetHTTPSProxy', namespace, (i) => kccScopedId(i, { defaultLocation: 'global' })), 'kcc');
466
+ push('ComputeGlobalForwardingRule', byType('compute.googleapis.com/ForwardingRule')
467
+ .map((a) => assetGlobalOrScopedId(a, 'forwardingRules')).filter((id) => id && id.startsWith('global/')), 'gcp');
468
+ push('ComputeGlobalForwardingRule', await kccField('ComputeGlobalForwardingRule', namespace, (i) => kccScopedId(i, { defaultLocation: 'global' })), 'kcc');
469
+ push('ComputeSSLCertificate', liveGlobalOrScopedIds(assets, 'compute.googleapis.com/SslCertificate', 'sslCertificates'), 'gcp');
470
+ const sslKcc = await Promise.all([
471
+ kccField('ComputeSSLCertificate', namespace, (i) => kccScopedId(i, { defaultLocation: 'global' })),
472
+ kccField('ComputeManagedSSLCertificate', namespace, (i) => kccScopedId(i, { defaultLocation: 'global' })),
473
+ ]);
474
+ push('ComputeSSLCertificate', sslKcc.flat(), 'kcc');
475
+
355
476
  // --- IAMPolicyMember ---------------------------------------------------------
356
477
  // search-all-iam-policies — усі біндинги проєкту одразу, на будь-якому
357
478
  // типі ресурсу, не тільки на трьох раніше підтримуваних (Project/SA/AR).
@@ -369,10 +490,11 @@ export async function collectNamespace(namespace, { includeSystem = false } = {}
369
490
  }
370
491
  push('IAMPolicyMember', liveIam, 'gcp');
371
492
 
493
+ const runServiceRefs = new Map(runServiceItems.map((i) => [i.metadata && i.metadata.name, kccScopedId(i)]));
372
494
  push('IAMPolicyMember', await kccField(
373
495
  'IAMPolicyMember', namespace,
374
496
  (item) => {
375
- const rid = kccPolicyMemberId(item, project);
497
+ const rid = kccPolicyMemberId(item, project, runServiceRefs);
376
498
  const spec = item.spec || {};
377
499
  return `${rid}/${spec.role}/${spec.member}`;
378
500
  },
@@ -396,10 +518,10 @@ The mechanism kcc-inventory is built on, callable on its own: per KCC
396
518
  namespace (namespace carrying the annotation
397
519
  cnrm.cloud.google.com/project-id), lists every resource found live in the
398
520
  GCP project (source: gcp) and every one declared as a Config Connector
399
- custom resource in that namespace (source: kcc), for IAMServiceAccount,
400
- IAMServiceAccountKey, ArtifactRegistryRepository, ContainerCluster,
401
- ContainerNodePool, StorageBucket, ComputeAddress, DNSManagedZone,
402
- DNSRecordSet, and IAMPolicyMember. No drift/orphan diff — that's
521
+ custom resource in that namespace. It covers IAM, Artifact Registry, GKE,
522
+ Cloud Run (services and jobs), Scheduler, Eventarc, Pub/Sub, Secret Manager,
523
+ VPC Access, selected network/LB resources, KMS, Cloud DNS, and
524
+ IAMPolicyMember. No drift/orphan diff — that's
403
525
  \`cfr kcc-inventory\`.
404
526
 
405
527
  By default GCP-managed system noise is filtered out (Google-owned service
package/lib/k8s-rest.mjs CHANGED
@@ -112,10 +112,8 @@ export async function listNamespaces() {
112
112
  }
113
113
 
114
114
  // group + множина для кожного виду KCC-ресурсу, який сканує
115
- // get-resources.mjs. Версія скрізь v1beta1 — перевірено по всіх десяти
116
- // CRD на живому кластері (kubectl get crd ... -o jsonpath спрацьовує), і
117
- // це стандарт самого KCC, не наш вибір.
118
- const CRD_GVR = {
115
+ // get-resources.mjs. Версія скрізь v1beta1 — стандарт KCC, не наш вибір.
116
+ export const CRD_GVR = {
119
117
  IAMServiceAccount: { group: 'iam.cnrm.cloud.google.com', plural: 'iamserviceaccounts' },
120
118
  IAMServiceAccountKey: { group: 'iam.cnrm.cloud.google.com', plural: 'iamserviceaccountkeys' },
121
119
  ArtifactRegistryRepository: { group: 'artifactregistry.cnrm.cloud.google.com', plural: 'artifactregistryrepositories' },
@@ -125,6 +123,24 @@ const CRD_GVR = {
125
123
  ComputeAddress: { group: 'compute.cnrm.cloud.google.com', plural: 'computeaddresses' },
126
124
  DNSManagedZone: { group: 'dns.cnrm.cloud.google.com', plural: 'dnsmanagedzones' },
127
125
  DNSRecordSet: { group: 'dns.cnrm.cloud.google.com', plural: 'dnsrecordsets' },
126
+ RunService: { group: 'run.cnrm.cloud.google.com', plural: 'runservices' },
127
+ RunJob: { group: 'run.cnrm.cloud.google.com', plural: 'runjobs' },
128
+ CloudSchedulerJob: { group: 'cloudscheduler.cnrm.cloud.google.com', plural: 'cloudschedulerjobs' },
129
+ EventarcTrigger: { group: 'eventarc.cnrm.cloud.google.com', plural: 'eventarctriggers' },
130
+ PubSubTopic: { group: 'pubsub.cnrm.cloud.google.com', plural: 'pubsubtopics' },
131
+ PubSubSubscription: { group: 'pubsub.cnrm.cloud.google.com', plural: 'pubsubsubscriptions' },
132
+ SecretManagerSecret: { group: 'secretmanager.cnrm.cloud.google.com', plural: 'secretmanagersecrets' },
133
+ VPCAccessConnector: { group: 'vpcaccess.cnrm.cloud.google.com', plural: 'vpcaccessconnectors' },
134
+ ComputeNetwork: { group: 'compute.cnrm.cloud.google.com', plural: 'computenetworks' },
135
+ ComputeSubnetwork: { group: 'compute.cnrm.cloud.google.com', plural: 'computesubnetworks' },
136
+ KMSCryptoKey: { group: 'kms.cnrm.cloud.google.com', plural: 'kmscryptokeys' },
137
+ ComputeBackendService: { group: 'compute.cnrm.cloud.google.com', plural: 'computebackendservices' },
138
+ ComputeNetworkEndpointGroup: { group: 'compute.cnrm.cloud.google.com', plural: 'computenetworkendpointgroups' },
139
+ ComputeURLMap: { group: 'compute.cnrm.cloud.google.com', plural: 'computeurlmaps' },
140
+ ComputeTargetHTTPSProxy: { group: 'compute.cnrm.cloud.google.com', plural: 'computetargethttpsproxies' },
141
+ ComputeGlobalForwardingRule: { group: 'compute.cnrm.cloud.google.com', plural: 'computeglobalforwardingrules' },
142
+ ComputeSSLCertificate: { group: 'compute.cnrm.cloud.google.com', plural: 'computesslcertificates' },
143
+ ComputeManagedSSLCertificate: { group: 'compute.cnrm.cloud.google.com', plural: 'computemanagedsslcertificates' },
128
144
  IAMPolicyMember: { group: 'iam.cnrm.cloud.google.com', plural: 'iampolicymembers' },
129
145
  };
130
146
 
@@ -14,10 +14,9 @@ Usage:
14
14
  Compares, per KCC namespace (namespace carrying the annotation
15
15
  cnrm.cloud.google.com/project-id), what actually exists in the GCP project
16
16
  against what's declared as Config Connector custom resources in that
17
- namespace for IAMServiceAccount, IAMServiceAccountKey,
18
- ArtifactRegistryRepository, ContainerCluster, ContainerNodePool,
19
- StorageBucket, ComputeAddress, DNSManagedZone, DNSRecordSet, and
20
- IAMPolicyMember.
17
+ namespace. It covers IAM, Artifact Registry, GKE, Cloud Run, Scheduler,
18
+ Eventarc, Pub/Sub, Secret Manager, VPC Access, selected network/LB
19
+ resources, KMS, Cloud DNS, and IAMPolicyMember.
21
20
 
22
21
  Reports two directions per kind:
23
22
  DRIFT — live in GCP, not declared under KCC (adopt it or delete it)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cfr",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A handful of small k8s/GitOps CLI utilities: `check` verifies every YAML file in a Kustomize directory is listed in kustomization.yaml's explicit resources: (and vice versa); `kcc-inventory` diffs a GCP Config Connector namespace against the live project to find drift; `get-resources` is that same scan without the diff.",
5
5
  "keywords": [
6
6
  "kustomize",
@@ -36,7 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "scripts": {
39
- "test": "node --test test/cli.test.mjs"
39
+ "test": "node --test test/*.test.mjs"
40
40
  },
41
41
  "dependencies": {
42
42
  "google-auth-library": "^10.9.1",