s3db.js 13.6.1 → 14.0.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/README.md +56 -15
- package/dist/s3db.cjs +72446 -39022
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72172 -38790
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +85 -50
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/index.js +168 -40
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes Driver for Inventory Plugin
|
|
3
|
+
*
|
|
4
|
+
* Connects to Kubernetes cluster and lists all resources using @kubernetes/client-node.
|
|
5
|
+
* Supports kubeconfig, in-cluster auth, and custom configurations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { requirePluginDependency } from '../concerns/plugin-dependencies.js';
|
|
9
|
+
import {
|
|
10
|
+
ALL_STANDARD_RESOURCE_TYPES,
|
|
11
|
+
formatResourceTypeId,
|
|
12
|
+
CORE_RESOURCE_TYPES,
|
|
13
|
+
APPS_RESOURCE_TYPES,
|
|
14
|
+
BATCH_RESOURCE_TYPES,
|
|
15
|
+
NETWORKING_RESOURCE_TYPES,
|
|
16
|
+
STORAGE_RESOURCE_TYPES,
|
|
17
|
+
RBAC_RESOURCE_TYPES,
|
|
18
|
+
POLICY_RESOURCE_TYPES,
|
|
19
|
+
AUTOSCALING_RESOURCE_TYPES,
|
|
20
|
+
SCHEDULING_RESOURCE_TYPES,
|
|
21
|
+
NODE_RESOURCE_TYPES,
|
|
22
|
+
CERTIFICATES_RESOURCE_TYPES,
|
|
23
|
+
COORDINATION_RESOURCE_TYPES,
|
|
24
|
+
DISCOVERY_RESOURCE_TYPES,
|
|
25
|
+
EVENTS_RESOURCE_TYPES,
|
|
26
|
+
ADMISSION_RESOURCE_TYPES,
|
|
27
|
+
API_REGISTRATION_RESOURCE_TYPES,
|
|
28
|
+
FLOWCONTROL_RESOURCE_TYPES,
|
|
29
|
+
} from './resource-types.js';
|
|
30
|
+
|
|
31
|
+
export class KubernetesDriver {
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
this.clusterId = options.id;
|
|
35
|
+
this.clusterName = options.name || options.id;
|
|
36
|
+
|
|
37
|
+
// Kubernetes client objects
|
|
38
|
+
this.kubeConfig = null;
|
|
39
|
+
this.apiClients = {};
|
|
40
|
+
|
|
41
|
+
// CRD discovery cache
|
|
42
|
+
this.crdCache = null;
|
|
43
|
+
this.crdCacheTime = 0;
|
|
44
|
+
this.crdCacheTTL = options.discovery?.crdCacheTTL || 300000; // 5 minutes
|
|
45
|
+
|
|
46
|
+
// Discovery options
|
|
47
|
+
this.discovery = {
|
|
48
|
+
includeSecrets: options.discovery?.includeSecrets ?? false,
|
|
49
|
+
includeConfigMaps: options.discovery?.includeConfigMaps ?? true,
|
|
50
|
+
includeCRDs: options.discovery?.includeCRDs ?? true,
|
|
51
|
+
coreResources: options.discovery?.coreResources ?? true,
|
|
52
|
+
appsResources: options.discovery?.appsResources ?? true,
|
|
53
|
+
batchResources: options.discovery?.batchResources ?? true,
|
|
54
|
+
networkingResources: options.discovery?.networkingResources ?? true,
|
|
55
|
+
storageResources: options.discovery?.storageResources ?? true,
|
|
56
|
+
rbacResources: options.discovery?.rbacResources ?? true,
|
|
57
|
+
namespaces: options.discovery?.namespaces || null, // null = all
|
|
58
|
+
excludeNamespaces: options.discovery?.excludeNamespaces || [],
|
|
59
|
+
...options.discovery,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Performance options
|
|
63
|
+
this.concurrency = options.discovery?.concurrency || 5;
|
|
64
|
+
this.pagination = options.discovery?.pagination || { enabled: true, pageSize: 100 };
|
|
65
|
+
|
|
66
|
+
// Retries
|
|
67
|
+
this.retries = options.retries || {
|
|
68
|
+
maxRetries: 5,
|
|
69
|
+
backoffBase: 1000,
|
|
70
|
+
retryOn429: true,
|
|
71
|
+
retryOn5xx: true,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Sanitization
|
|
75
|
+
this.sanitization = options.sanitization || {
|
|
76
|
+
removeSecrets: true,
|
|
77
|
+
removeManagedFields: true,
|
|
78
|
+
removeResourceVersion: false,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Logger
|
|
82
|
+
this.logger = options.logger || (() => {});
|
|
83
|
+
this.verbose = options.verbose ?? false;
|
|
84
|
+
|
|
85
|
+
// Metadata
|
|
86
|
+
this.tags = options.tags || {};
|
|
87
|
+
this.metadata = options.metadata || {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Initialize the Kubernetes client
|
|
92
|
+
*/
|
|
93
|
+
async initialize() {
|
|
94
|
+
// Require @kubernetes/client-node as peer dependency
|
|
95
|
+
const k8s = requirePluginDependency('@kubernetes/client-node', 'KubernetesInventoryPlugin');
|
|
96
|
+
|
|
97
|
+
this.k8s = k8s;
|
|
98
|
+
this.kubeConfig = new k8s.KubeConfig();
|
|
99
|
+
|
|
100
|
+
// Load kubeconfig based on options with fallback chain
|
|
101
|
+
await this._loadKubeConfig();
|
|
102
|
+
|
|
103
|
+
// Create API clients
|
|
104
|
+
this._createApiClients();
|
|
105
|
+
|
|
106
|
+
// Test connection
|
|
107
|
+
await this._testConnection();
|
|
108
|
+
|
|
109
|
+
this.log('info', `Kubernetes driver initialized for cluster: ${this.clusterName}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Load kubeconfig from multiple sources with priority order
|
|
114
|
+
*/
|
|
115
|
+
async _loadKubeConfig() {
|
|
116
|
+
// Priority 1: In-cluster service account
|
|
117
|
+
if (this.options.inCluster) {
|
|
118
|
+
this.log('info', 'Loading in-cluster configuration');
|
|
119
|
+
this.kubeConfig.loadFromCluster();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Priority 2: Direct connection object (manual configuration)
|
|
124
|
+
if (this.options.connection) {
|
|
125
|
+
this.log('info', 'Loading kubeconfig from connection object');
|
|
126
|
+
this._loadFromConnectionObject();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Priority 3: Kubeconfig content as string (from environment variable or direct)
|
|
131
|
+
const kubeconfigContent = this._resolveKubeconfigContent();
|
|
132
|
+
if (kubeconfigContent) {
|
|
133
|
+
this.log('info', 'Loading kubeconfig from content string');
|
|
134
|
+
this.kubeConfig.loadFromString(kubeconfigContent);
|
|
135
|
+
|
|
136
|
+
// Apply context if specified
|
|
137
|
+
if (this.options.context) {
|
|
138
|
+
this.log('info', `Switching to context: ${this.options.context}`);
|
|
139
|
+
this.kubeConfig.setCurrentContext(this.options.context);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Priority 4: Kubeconfig file path (from option or environment variable)
|
|
145
|
+
const kubeconfigPath = this._resolveKubeconfigPath();
|
|
146
|
+
if (kubeconfigPath) {
|
|
147
|
+
this.log('info', `Loading kubeconfig from file: ${kubeconfigPath}`);
|
|
148
|
+
this.kubeConfig.loadFromFile(kubeconfigPath);
|
|
149
|
+
|
|
150
|
+
// Apply context if specified
|
|
151
|
+
if (this.options.context) {
|
|
152
|
+
this.log('info', `Switching to context: ${this.options.context}`);
|
|
153
|
+
this.kubeConfig.setCurrentContext(this.options.context);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Priority 5: Context only (use default kubeconfig with specific context)
|
|
159
|
+
if (this.options.context) {
|
|
160
|
+
this.log('info', `Loading default kubeconfig with context: ${this.options.context}`);
|
|
161
|
+
this.kubeConfig.loadFromDefault();
|
|
162
|
+
this.kubeConfig.setCurrentContext(this.options.context);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Priority 6: Default kubeconfig (~/.kube/config or KUBECONFIG env var)
|
|
167
|
+
this.log('info', 'Loading default kubeconfig');
|
|
168
|
+
this.kubeConfig.loadFromDefault();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve kubeconfig content from multiple sources
|
|
173
|
+
* Priority: options.kubeconfigContent > env var
|
|
174
|
+
*/
|
|
175
|
+
_resolveKubeconfigContent() {
|
|
176
|
+
// Direct content from options
|
|
177
|
+
if (this.options.kubeconfigContent) {
|
|
178
|
+
return this.options.kubeconfigContent;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Environment variable: KUBECONFIG_CONTENT (base64 or plain text)
|
|
182
|
+
const envContent = process.env.KUBECONFIG_CONTENT;
|
|
183
|
+
if (envContent) {
|
|
184
|
+
this.log('debug', 'Found KUBECONFIG_CONTENT environment variable');
|
|
185
|
+
|
|
186
|
+
// Try to decode as base64 first
|
|
187
|
+
try {
|
|
188
|
+
const decoded = Buffer.from(envContent, 'base64').toString('utf-8');
|
|
189
|
+
// Check if it looks like YAML/JSON
|
|
190
|
+
if (decoded.includes('apiVersion') || decoded.includes('clusters')) {
|
|
191
|
+
this.log('debug', 'KUBECONFIG_CONTENT is base64-encoded');
|
|
192
|
+
return decoded;
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Not base64, use as-is
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Use as plain text
|
|
199
|
+
return envContent;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Cluster-specific environment variable: KUBECONFIG_CONTENT_<CLUSTER_ID>
|
|
203
|
+
const clusterEnvKey = `KUBECONFIG_CONTENT_${this.clusterId.toUpperCase().replace(/-/g, '_')}`;
|
|
204
|
+
const clusterEnvContent = process.env[clusterEnvKey];
|
|
205
|
+
if (clusterEnvContent) {
|
|
206
|
+
this.log('debug', `Found ${clusterEnvKey} environment variable`);
|
|
207
|
+
|
|
208
|
+
// Try base64 decode
|
|
209
|
+
try {
|
|
210
|
+
const decoded = Buffer.from(clusterEnvContent, 'base64').toString('utf-8');
|
|
211
|
+
if (decoded.includes('apiVersion') || decoded.includes('clusters')) {
|
|
212
|
+
return decoded;
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
// Not base64
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return clusterEnvContent;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Resolve kubeconfig file path from multiple sources
|
|
226
|
+
* Priority: options.kubeconfig > cluster-specific env var > KUBECONFIG env var
|
|
227
|
+
*/
|
|
228
|
+
_resolveKubeconfigPath() {
|
|
229
|
+
// Direct path from options
|
|
230
|
+
if (this.options.kubeconfig) {
|
|
231
|
+
return this._expandPath(this.options.kubeconfig);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Cluster-specific environment variable: KUBECONFIG_<CLUSTER_ID>
|
|
235
|
+
const clusterEnvKey = `KUBECONFIG_${this.clusterId.toUpperCase().replace(/-/g, '_')}`;
|
|
236
|
+
const clusterEnvPath = process.env[clusterEnvKey];
|
|
237
|
+
if (clusterEnvPath) {
|
|
238
|
+
this.log('debug', `Found ${clusterEnvKey} environment variable`);
|
|
239
|
+
return this._expandPath(clusterEnvPath);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Standard KUBECONFIG environment variable
|
|
243
|
+
// Note: loadFromDefault() already handles this, so we return null here
|
|
244
|
+
// to let it fall through to default behavior
|
|
245
|
+
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Expand path with home directory (~) and environment variables
|
|
251
|
+
*/
|
|
252
|
+
_expandPath(path) {
|
|
253
|
+
if (!path) return path;
|
|
254
|
+
|
|
255
|
+
// Expand ~ to home directory
|
|
256
|
+
let expandedPath = path.replace(/^~/, process.env.HOME || '');
|
|
257
|
+
|
|
258
|
+
// Expand environment variables: ${VAR} or $VAR
|
|
259
|
+
expandedPath = expandedPath.replace(/\$\{([^}]+)\}/g, (_, varName) => {
|
|
260
|
+
return process.env[varName] || '';
|
|
261
|
+
});
|
|
262
|
+
expandedPath = expandedPath.replace(/\$([A-Z_][A-Z0-9_]*)/gi, (_, varName) => {
|
|
263
|
+
return process.env[varName] || '';
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return expandedPath;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Load from connection object (manual configuration)
|
|
271
|
+
*/
|
|
272
|
+
_loadFromConnectionObject() {
|
|
273
|
+
const conn = this.options.connection;
|
|
274
|
+
|
|
275
|
+
this.kubeConfig.loadFromOptions({
|
|
276
|
+
clusters: [{
|
|
277
|
+
name: this.clusterId,
|
|
278
|
+
server: conn.server,
|
|
279
|
+
caData: conn.caData,
|
|
280
|
+
skipTLSVerify: conn.skipTLSVerify,
|
|
281
|
+
}],
|
|
282
|
+
users: [{
|
|
283
|
+
name: this.clusterId,
|
|
284
|
+
token: conn.token,
|
|
285
|
+
certData: conn.certData,
|
|
286
|
+
keyData: conn.keyData,
|
|
287
|
+
}],
|
|
288
|
+
contexts: [{
|
|
289
|
+
name: this.clusterId,
|
|
290
|
+
cluster: this.clusterId,
|
|
291
|
+
user: this.clusterId,
|
|
292
|
+
}],
|
|
293
|
+
currentContext: this.clusterId,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create all necessary API clients
|
|
299
|
+
*/
|
|
300
|
+
_createApiClients() {
|
|
301
|
+
const { k8s, kubeConfig } = this;
|
|
302
|
+
|
|
303
|
+
this.apiClients = {
|
|
304
|
+
core: kubeConfig.makeApiClient(k8s.CoreV1Api),
|
|
305
|
+
apps: kubeConfig.makeApiClient(k8s.AppsV1Api),
|
|
306
|
+
batch: kubeConfig.makeApiClient(k8s.BatchV1Api),
|
|
307
|
+
networking: kubeConfig.makeApiClient(k8s.NetworkingV1Api),
|
|
308
|
+
storage: kubeConfig.makeApiClient(k8s.StorageV1Api),
|
|
309
|
+
rbac: kubeConfig.makeApiClient(k8s.RbacAuthorizationV1Api),
|
|
310
|
+
policy: kubeConfig.makeApiClient(k8s.PolicyV1Api),
|
|
311
|
+
autoscalingV1: kubeConfig.makeApiClient(k8s.AutoscalingV1Api),
|
|
312
|
+
autoscalingV2: kubeConfig.makeApiClient(k8s.AutoscalingV2Api),
|
|
313
|
+
scheduling: kubeConfig.makeApiClient(k8s.SchedulingV1Api),
|
|
314
|
+
node: kubeConfig.makeApiClient(k8s.NodeV1Api),
|
|
315
|
+
certificates: kubeConfig.makeApiClient(k8s.CertificatesV1Api),
|
|
316
|
+
coordination: kubeConfig.makeApiClient(k8s.CoordinationV1Api),
|
|
317
|
+
discovery: kubeConfig.makeApiClient(k8s.DiscoveryV1Api),
|
|
318
|
+
events: kubeConfig.makeApiClient(k8s.EventsV1Api),
|
|
319
|
+
admission: kubeConfig.makeApiClient(k8s.AdmissionregistrationV1Api),
|
|
320
|
+
apiRegistration: kubeConfig.makeApiClient(k8s.ApiregistrationV1Api),
|
|
321
|
+
apiExtensions: kubeConfig.makeApiClient(k8s.ApiextensionsV1Api),
|
|
322
|
+
customObjects: kubeConfig.makeApiClient(k8s.CustomObjectsApi),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Test connection to cluster
|
|
328
|
+
*/
|
|
329
|
+
async _testConnection() {
|
|
330
|
+
try {
|
|
331
|
+
const response = await this._retryOperation(async () => {
|
|
332
|
+
return await this.apiClients.core.listNamespace();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const namespaceCount = response.body.items.length;
|
|
336
|
+
this.log('info', `Successfully connected to cluster. Found ${namespaceCount} namespaces.`);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
throw new Error(`Failed to connect to Kubernetes cluster: ${error.message}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Discover all available resource types (including CRDs)
|
|
344
|
+
* @param {Object} options - Discovery options
|
|
345
|
+
* @returns {Promise<Array>} Array of resource type definitions
|
|
346
|
+
*/
|
|
347
|
+
async discoverResourceTypes(options = {}) {
|
|
348
|
+
const resourceTypes = [];
|
|
349
|
+
|
|
350
|
+
// Add standard resource types based on discovery options
|
|
351
|
+
if (this.discovery.coreResources) {
|
|
352
|
+
resourceTypes.push(...this._filterSecrets(CORE_RESOURCE_TYPES));
|
|
353
|
+
}
|
|
354
|
+
if (this.discovery.appsResources) {
|
|
355
|
+
resourceTypes.push(...APPS_RESOURCE_TYPES);
|
|
356
|
+
}
|
|
357
|
+
if (this.discovery.batchResources) {
|
|
358
|
+
resourceTypes.push(...BATCH_RESOURCE_TYPES);
|
|
359
|
+
}
|
|
360
|
+
if (this.discovery.networkingResources) {
|
|
361
|
+
resourceTypes.push(...NETWORKING_RESOURCE_TYPES);
|
|
362
|
+
}
|
|
363
|
+
if (this.discovery.storageResources) {
|
|
364
|
+
resourceTypes.push(...STORAGE_RESOURCE_TYPES);
|
|
365
|
+
}
|
|
366
|
+
if (this.discovery.rbacResources) {
|
|
367
|
+
resourceTypes.push(...RBAC_RESOURCE_TYPES);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Add other standard types
|
|
371
|
+
resourceTypes.push(
|
|
372
|
+
...POLICY_RESOURCE_TYPES,
|
|
373
|
+
...AUTOSCALING_RESOURCE_TYPES,
|
|
374
|
+
...SCHEDULING_RESOURCE_TYPES,
|
|
375
|
+
...NODE_RESOURCE_TYPES,
|
|
376
|
+
...CERTIFICATES_RESOURCE_TYPES,
|
|
377
|
+
...COORDINATION_RESOURCE_TYPES,
|
|
378
|
+
...DISCOVERY_RESOURCE_TYPES,
|
|
379
|
+
...EVENTS_RESOURCE_TYPES,
|
|
380
|
+
...ADMISSION_RESOURCE_TYPES,
|
|
381
|
+
...API_REGISTRATION_RESOURCE_TYPES,
|
|
382
|
+
...FLOWCONTROL_RESOURCE_TYPES
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Discover CRDs if enabled
|
|
386
|
+
if (this.discovery.includeCRDs) {
|
|
387
|
+
const crds = await this._discoverCRDs(options.force);
|
|
388
|
+
resourceTypes.push(...crds);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return resourceTypes;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Filter out Secrets if includeSecrets is false
|
|
396
|
+
*/
|
|
397
|
+
_filterSecrets(resourceTypes) {
|
|
398
|
+
if (this.discovery.includeSecrets) {
|
|
399
|
+
return resourceTypes;
|
|
400
|
+
}
|
|
401
|
+
return resourceTypes.filter(rt => rt.kind !== 'Secret');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Discover Custom Resource Definitions (CRDs)
|
|
406
|
+
* @param {boolean} force - Force refresh cache
|
|
407
|
+
* @returns {Promise<Array>} Array of CRD resource types
|
|
408
|
+
*/
|
|
409
|
+
async _discoverCRDs(force = false) {
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
|
|
412
|
+
// Return cached CRDs if still valid
|
|
413
|
+
if (!force && this.crdCache && (now - this.crdCacheTime < this.crdCacheTTL)) {
|
|
414
|
+
this.log('debug', `Using cached CRDs (${this.crdCache.length} types)`);
|
|
415
|
+
return this.crdCache;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
this.log('info', 'Discovering Custom Resource Definitions...');
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const response = await this._retryOperation(async () => {
|
|
422
|
+
return await this.apiClients.apiExtensions.listCustomResourceDefinition();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const crds = response.body.items.map(crd => {
|
|
426
|
+
// Find the storage version or use the first version
|
|
427
|
+
const storageVersion = crd.spec.versions.find(v => v.storage);
|
|
428
|
+
const version = storageVersion || crd.spec.versions[0];
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
group: crd.spec.group,
|
|
432
|
+
version: version.name,
|
|
433
|
+
kind: crd.spec.names.kind,
|
|
434
|
+
plural: crd.spec.names.plural,
|
|
435
|
+
namespaced: crd.spec.scope === 'Namespaced',
|
|
436
|
+
isCRD: true,
|
|
437
|
+
category: 'custom',
|
|
438
|
+
crdName: crd.metadata.name,
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
this.crdCache = crds;
|
|
443
|
+
this.crdCacheTime = now;
|
|
444
|
+
|
|
445
|
+
this.log('info', `Discovered ${crds.length} Custom Resource Definitions`);
|
|
446
|
+
|
|
447
|
+
return crds;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
this.log('warn', `Failed to discover CRDs: ${error.message}`);
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* List all resources from the Kubernetes cluster
|
|
456
|
+
* @param {Object} options - Discovery options
|
|
457
|
+
* @returns {AsyncIterator} Yields normalized resources
|
|
458
|
+
*/
|
|
459
|
+
async *listResources(options = {}) {
|
|
460
|
+
const { runtime } = options;
|
|
461
|
+
|
|
462
|
+
// Discover resource types
|
|
463
|
+
const resourceTypes = await this.discoverResourceTypes(options);
|
|
464
|
+
this.log('info', `Discovering ${resourceTypes.length} resource types from cluster ${this.clusterName}`);
|
|
465
|
+
|
|
466
|
+
// Emit progress
|
|
467
|
+
runtime?.emitProgress?.({
|
|
468
|
+
stage: 'discovery',
|
|
469
|
+
clusterId: this.clusterId,
|
|
470
|
+
totalTypes: resourceTypes.length,
|
|
471
|
+
processedTypes: 0,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
let processedTypes = 0;
|
|
475
|
+
|
|
476
|
+
// Fetch resources for each type
|
|
477
|
+
for (const resourceType of resourceTypes) {
|
|
478
|
+
const resourceTypeId = formatResourceTypeId(resourceType);
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
this.log('debug', `Fetching resources of type: ${resourceTypeId}`);
|
|
482
|
+
|
|
483
|
+
// Fetch resources based on type
|
|
484
|
+
const resources = await this._fetchResourceType(resourceType);
|
|
485
|
+
|
|
486
|
+
// Yield normalized resources
|
|
487
|
+
for (const resource of resources) {
|
|
488
|
+
yield this._normalizeResource(resourceType, resource);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
processedTypes++;
|
|
492
|
+
|
|
493
|
+
// Emit progress
|
|
494
|
+
runtime?.emitProgress?.({
|
|
495
|
+
stage: 'discovery',
|
|
496
|
+
clusterId: this.clusterId,
|
|
497
|
+
totalTypes: resourceTypes.length,
|
|
498
|
+
processedTypes,
|
|
499
|
+
currentType: resourceTypeId,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
this.log('debug', `Fetched ${resources.length} resources of type: ${resourceTypeId}`);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
this.log('warn', `Failed to fetch resource type ${resourceTypeId}: ${error.message}`);
|
|
505
|
+
|
|
506
|
+
// Continue with next resource type (don't fail entire discovery)
|
|
507
|
+
runtime?.emitProgress?.({
|
|
508
|
+
stage: 'error',
|
|
509
|
+
clusterId: this.clusterId,
|
|
510
|
+
resourceType: resourceTypeId,
|
|
511
|
+
error: error.message,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.log('info', `Completed discovery for cluster ${this.clusterName}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Fetch all resources of a specific type
|
|
521
|
+
* @param {Object} resourceType - Resource type definition
|
|
522
|
+
* @returns {Promise<Array>} Array of Kubernetes resources
|
|
523
|
+
*/
|
|
524
|
+
async _fetchResourceType(resourceType) {
|
|
525
|
+
const resources = [];
|
|
526
|
+
|
|
527
|
+
// Determine if resource is namespaced
|
|
528
|
+
if (resourceType.namespaced) {
|
|
529
|
+
// Fetch namespaced resources
|
|
530
|
+
const namespaces = await this._getNamespaces();
|
|
531
|
+
|
|
532
|
+
for (const namespace of namespaces) {
|
|
533
|
+
const namespaceResources = await this._fetchNamespacedResources(resourceType, namespace);
|
|
534
|
+
resources.push(...namespaceResources);
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
// Fetch cluster-scoped resources
|
|
538
|
+
const clusterResources = await this._fetchClusterResources(resourceType);
|
|
539
|
+
resources.push(...clusterResources);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return resources;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Get list of namespaces to query
|
|
547
|
+
* @returns {Promise<Array>} Array of namespace names
|
|
548
|
+
*/
|
|
549
|
+
async _getNamespaces() {
|
|
550
|
+
// If specific namespaces are configured, use those
|
|
551
|
+
if (this.discovery.namespaces && this.discovery.namespaces.length > 0) {
|
|
552
|
+
return this.discovery.namespaces.filter(
|
|
553
|
+
ns => !this.discovery.excludeNamespaces.includes(ns)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Otherwise, fetch all namespaces
|
|
558
|
+
try {
|
|
559
|
+
const response = await this._retryOperation(async () => {
|
|
560
|
+
return await this.apiClients.core.listNamespace();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
return response.body.items
|
|
564
|
+
.map(ns => ns.metadata.name)
|
|
565
|
+
.filter(ns => !this.discovery.excludeNamespaces.includes(ns));
|
|
566
|
+
} catch (error) {
|
|
567
|
+
this.log('warn', `Failed to list namespaces: ${error.message}`);
|
|
568
|
+
return ['default']; // Fallback to default namespace
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Fetch namespaced resources
|
|
574
|
+
* @param {Object} resourceType - Resource type definition
|
|
575
|
+
* @param {string} namespace - Namespace name
|
|
576
|
+
* @returns {Promise<Array>} Array of resources
|
|
577
|
+
*/
|
|
578
|
+
async _fetchNamespacedResources(resourceType, namespace) {
|
|
579
|
+
try {
|
|
580
|
+
if (resourceType.isCRD) {
|
|
581
|
+
// Fetch custom resources
|
|
582
|
+
return await this._fetchCustomResources(resourceType, namespace);
|
|
583
|
+
} else {
|
|
584
|
+
// Fetch standard namespaced resources
|
|
585
|
+
return await this._fetchStandardNamespacedResources(resourceType, namespace);
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
// 404 means resource type not found in this cluster (e.g., old K8s version)
|
|
589
|
+
if (error.response?.statusCode === 404 || error.statusCode === 404) {
|
|
590
|
+
this.log('debug', `Resource type ${formatResourceTypeId(resourceType)} not found in cluster`);
|
|
591
|
+
return [];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// 403 means permission denied
|
|
595
|
+
if (error.response?.statusCode === 403 || error.statusCode === 403) {
|
|
596
|
+
this.log('warn', `Permission denied for resource type ${formatResourceTypeId(resourceType)} in namespace ${namespace}`);
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Fetch cluster-scoped resources
|
|
606
|
+
* @param {Object} resourceType - Resource type definition
|
|
607
|
+
* @returns {Promise<Array>} Array of resources
|
|
608
|
+
*/
|
|
609
|
+
async _fetchClusterResources(resourceType) {
|
|
610
|
+
try {
|
|
611
|
+
if (resourceType.isCRD) {
|
|
612
|
+
// Fetch custom resources (cluster-scoped)
|
|
613
|
+
return await this._fetchCustomResources(resourceType, null);
|
|
614
|
+
} else {
|
|
615
|
+
// Fetch standard cluster-scoped resources
|
|
616
|
+
return await this._fetchStandardClusterResources(resourceType);
|
|
617
|
+
}
|
|
618
|
+
} catch (error) {
|
|
619
|
+
// 404 means resource type not found
|
|
620
|
+
if (error.response?.statusCode === 404 || error.statusCode === 404) {
|
|
621
|
+
this.log('debug', `Resource type ${formatResourceTypeId(resourceType)} not found in cluster`);
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// 403 means permission denied
|
|
626
|
+
if (error.response?.statusCode === 403 || error.statusCode === 403) {
|
|
627
|
+
this.log('warn', `Permission denied for resource type ${formatResourceTypeId(resourceType)}`);
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Fetch standard namespaced resources
|
|
637
|
+
*/
|
|
638
|
+
async _fetchStandardNamespacedResources(resourceType, namespace) {
|
|
639
|
+
const apiClient = this._getApiClient(resourceType);
|
|
640
|
+
const methodName = `listNamespaced${resourceType.kind}`;
|
|
641
|
+
|
|
642
|
+
if (!apiClient || !apiClient[methodName]) {
|
|
643
|
+
this.log('warn', `API method not found: ${methodName}`);
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const response = await this._retryOperation(async () => {
|
|
648
|
+
return await apiClient[methodName](namespace);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
return response.body.items || [];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Fetch standard cluster-scoped resources
|
|
656
|
+
*/
|
|
657
|
+
async _fetchStandardClusterResources(resourceType) {
|
|
658
|
+
const apiClient = this._getApiClient(resourceType);
|
|
659
|
+
const methodName = `list${resourceType.kind}`;
|
|
660
|
+
|
|
661
|
+
if (!apiClient || !apiClient[methodName]) {
|
|
662
|
+
this.log('warn', `API method not found: ${methodName}`);
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const response = await this._retryOperation(async () => {
|
|
667
|
+
return await apiClient[methodName]();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
return response.body.items || [];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Fetch custom resources (CRDs)
|
|
675
|
+
*/
|
|
676
|
+
async _fetchCustomResources(resourceType, namespace) {
|
|
677
|
+
const { customObjects } = this.apiClients;
|
|
678
|
+
|
|
679
|
+
const response = await this._retryOperation(async () => {
|
|
680
|
+
if (namespace) {
|
|
681
|
+
return await customObjects.listNamespacedCustomObject(
|
|
682
|
+
resourceType.group,
|
|
683
|
+
resourceType.version,
|
|
684
|
+
namespace,
|
|
685
|
+
resourceType.plural
|
|
686
|
+
);
|
|
687
|
+
} else {
|
|
688
|
+
return await customObjects.listClusterCustomObject(
|
|
689
|
+
resourceType.group,
|
|
690
|
+
resourceType.version,
|
|
691
|
+
resourceType.plural
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
return response.body.items || [];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Get appropriate API client for resource type
|
|
701
|
+
*/
|
|
702
|
+
_getApiClient(resourceType) {
|
|
703
|
+
const { group } = resourceType;
|
|
704
|
+
|
|
705
|
+
if (!group || group === '') return this.apiClients.core;
|
|
706
|
+
if (group === 'apps') return this.apiClients.apps;
|
|
707
|
+
if (group === 'batch') return this.apiClients.batch;
|
|
708
|
+
if (group === 'networking.k8s.io') return this.apiClients.networking;
|
|
709
|
+
if (group === 'storage.k8s.io') return this.apiClients.storage;
|
|
710
|
+
if (group === 'rbac.authorization.k8s.io') return this.apiClients.rbac;
|
|
711
|
+
if (group === 'policy') return this.apiClients.policy;
|
|
712
|
+
if (group === 'autoscaling' && resourceType.version === 'v1') return this.apiClients.autoscalingV1;
|
|
713
|
+
if (group === 'autoscaling' && resourceType.version === 'v2') return this.apiClients.autoscalingV2;
|
|
714
|
+
if (group === 'scheduling.k8s.io') return this.apiClients.scheduling;
|
|
715
|
+
if (group === 'node.k8s.io') return this.apiClients.node;
|
|
716
|
+
if (group === 'certificates.k8s.io') return this.apiClients.certificates;
|
|
717
|
+
if (group === 'coordination.k8s.io') return this.apiClients.coordination;
|
|
718
|
+
if (group === 'discovery.k8s.io') return this.apiClients.discovery;
|
|
719
|
+
if (group === 'events.k8s.io') return this.apiClients.events;
|
|
720
|
+
if (group === 'admissionregistration.k8s.io') return this.apiClients.admission;
|
|
721
|
+
if (group === 'apiregistration.k8s.io') return this.apiClients.apiRegistration;
|
|
722
|
+
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Normalize Kubernetes resource to standard format
|
|
728
|
+
*/
|
|
729
|
+
_normalizeResource(resourceType, resource) {
|
|
730
|
+
const metadata = resource.metadata || {};
|
|
731
|
+
|
|
732
|
+
// Sanitize configuration
|
|
733
|
+
const configuration = this._sanitizeConfiguration(resource);
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
// IDENTITY
|
|
737
|
+
provider: 'kubernetes',
|
|
738
|
+
clusterId: this.clusterId,
|
|
739
|
+
clusterName: this.clusterName,
|
|
740
|
+
namespace: metadata.namespace || null,
|
|
741
|
+
resourceType: formatResourceTypeId(resourceType),
|
|
742
|
+
resourceId: metadata.name,
|
|
743
|
+
uid: metadata.uid,
|
|
744
|
+
|
|
745
|
+
// METADATA
|
|
746
|
+
apiVersion: resource.apiVersion,
|
|
747
|
+
kind: resource.kind,
|
|
748
|
+
name: metadata.name,
|
|
749
|
+
labels: metadata.labels || {},
|
|
750
|
+
annotations: metadata.annotations || {},
|
|
751
|
+
creationTimestamp: metadata.creationTimestamp,
|
|
752
|
+
resourceVersion: this.sanitization.removeResourceVersion ? undefined : metadata.resourceVersion,
|
|
753
|
+
|
|
754
|
+
// CONFIGURATION
|
|
755
|
+
configuration,
|
|
756
|
+
|
|
757
|
+
// CONTEXT
|
|
758
|
+
tags: this.tags,
|
|
759
|
+
metadata: this.metadata,
|
|
760
|
+
raw: this.sanitization.removeRaw ? undefined : resource,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Sanitize resource configuration
|
|
766
|
+
*/
|
|
767
|
+
_sanitizeConfiguration(resource) {
|
|
768
|
+
const config = { ...resource };
|
|
769
|
+
|
|
770
|
+
// Remove metadata from configuration (already extracted)
|
|
771
|
+
delete config.metadata;
|
|
772
|
+
delete config.apiVersion;
|
|
773
|
+
delete config.kind;
|
|
774
|
+
|
|
775
|
+
// Remove managed fields (reduce size)
|
|
776
|
+
if (this.sanitization.removeManagedFields && config.metadata?.managedFields) {
|
|
777
|
+
delete config.metadata.managedFields;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Remove secret data
|
|
781
|
+
if (this.sanitization.removeSecrets && resource.kind === 'Secret' && config.data) {
|
|
782
|
+
config.data = Object.keys(config.data).reduce((acc, key) => {
|
|
783
|
+
acc[key] = '[REDACTED]';
|
|
784
|
+
return acc;
|
|
785
|
+
}, {});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Custom sanitizer
|
|
789
|
+
if (this.sanitization.customSanitizer) {
|
|
790
|
+
return this.sanitization.customSanitizer(config);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return config;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Retry operation with exponential backoff
|
|
798
|
+
*/
|
|
799
|
+
async _retryOperation(operation, attempt = 1) {
|
|
800
|
+
try {
|
|
801
|
+
return await operation();
|
|
802
|
+
} catch (error) {
|
|
803
|
+
const shouldRetry = this._shouldRetry(error, attempt);
|
|
804
|
+
|
|
805
|
+
if (shouldRetry && attempt < this.retries.maxRetries) {
|
|
806
|
+
const delay = Math.pow(2, attempt - 1) * this.retries.backoffBase;
|
|
807
|
+
this.log('debug', `Retrying operation (attempt ${attempt + 1}/${this.retries.maxRetries}) after ${delay}ms`);
|
|
808
|
+
|
|
809
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
810
|
+
return this._retryOperation(operation, attempt + 1);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Determine if error should be retried
|
|
819
|
+
*/
|
|
820
|
+
_shouldRetry(error, attempt) {
|
|
821
|
+
if (attempt >= this.retries.maxRetries) {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const statusCode = error.response?.statusCode || error.statusCode;
|
|
826
|
+
|
|
827
|
+
// Retry on 429 (rate limit)
|
|
828
|
+
if (statusCode === 429 && this.retries.retryOn429) {
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Retry on 5xx (server errors)
|
|
833
|
+
if (statusCode >= 500 && statusCode < 600 && this.retries.retryOn5xx) {
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Retry on network errors
|
|
838
|
+
if (!statusCode && error.code === 'ECONNRESET') {
|
|
839
|
+
return true;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Cleanup driver resources
|
|
847
|
+
*/
|
|
848
|
+
async destroy() {
|
|
849
|
+
this.log('info', `Destroying Kubernetes driver for cluster: ${this.clusterName}`);
|
|
850
|
+
this.kubeConfig = null;
|
|
851
|
+
this.apiClients = {};
|
|
852
|
+
this.crdCache = null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Internal logger
|
|
857
|
+
*/
|
|
858
|
+
log(level, message, meta = {}) {
|
|
859
|
+
if (this.logger) {
|
|
860
|
+
this.logger(level, message, { driver: 'kubernetes', clusterId: this.clusterId, ...meta });
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (this.verbose && level !== 'debug') {
|
|
864
|
+
console.log(`[${level.toUpperCase()}] [k8s:${this.clusterId}] ${message}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|