@di-framework/cloudfoundry 5.2.3
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 +128 -0
- package/dist/application-info.d.ts +6 -0
- package/dist/application-info.js +126 -0
- package/dist/bindings.d.ts +9 -0
- package/dist/bindings.js +61 -0
- package/dist/creators/amqp.d.ts +6 -0
- package/dist/creators/amqp.js +109 -0
- package/dist/creators/blob-storage.d.ts +6 -0
- package/dist/creators/blob-storage.js +95 -0
- package/dist/creators/redis.d.ts +6 -0
- package/dist/creators/redis.js +95 -0
- package/dist/creators/relational.d.ts +6 -0
- package/dist/creators/relational.js +179 -0
- package/dist/creators/user-provided.d.ts +6 -0
- package/dist/creators/user-provided.js +36 -0
- package/dist/decorators.d.ts +34 -0
- package/dist/decorators.js +202 -0
- package/dist/detector.d.ts +22 -0
- package/dist/detector.js +35 -0
- package/dist/environment.d.ts +62 -0
- package/dist/environment.js +326 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +11 -0
- package/dist/spi/creator.d.ts +15 -0
- package/dist/spi/creator.js +1 -0
- package/dist/spi/registry.d.ts +28 -0
- package/dist/spi/registry.js +161 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +5 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @di-framework/cloudfoundry
|
|
2
|
+
|
|
3
|
+
Cloud Foundry connector for `@di-framework` — automatic `VCAP_SERVICES` and `VCAP_APPLICATION` discovery, strongly typed ServiceInfo models, SPI creator extensible registry, and automated DI bindings.
|
|
4
|
+
|
|
5
|
+
Inspired by Spring Cloud Connectors (`spring-cloud-cloudfoundry-connector`), this package streamlines running `@di-framework` applications on Cloud Foundry and Tanzu Application Service.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Automatic Environment Detection**: Fast detection via `CloudFoundryDetector` and `isCloudFoundry()`.
|
|
10
|
+
- **Application Info Parsing**: Strongly typed `CloudFoundryApplicationInfo` containing application names, URIs, instance index, space, limits, etc.
|
|
11
|
+
- **Service Info Creators (SPI)**: Extensible discovery and normalized models for:
|
|
12
|
+
- **Relational Databases**: PostgreSQL, MySQL, MariaDB, SQLite (`RelationalServiceInfo`)
|
|
13
|
+
- **Redis / Cache**: Redis standalone, TLS, cluster (`RedisServiceInfo`)
|
|
14
|
+
- **AMQP / Message Brokers**: RabbitMQ, CloudAMQP (`AmqpServiceInfo`)
|
|
15
|
+
- **Blob Storage / S3**: MinIO, AWS S3, Object Store (`BlobStorageServiceInfo`)
|
|
16
|
+
- **User-Provided Services**: `cf cups` custom services (`UserProvidedServiceInfo`)
|
|
17
|
+
- **DI & Decorator Integration**:
|
|
18
|
+
- `@CloudFoundryService('service-name')` for parameter and property injection
|
|
19
|
+
- `@VcapApplication()` for application metadata injection
|
|
20
|
+
- `@EnableCloudFoundryConnectors()` for container-wide auto-wiring
|
|
21
|
+
- **Local Fallback**: Automatically falls back to standard local environment variables (`DATABASE_URL`, `REDIS_URL`, `AMQP_URL`, `S3_BUCKET`) when running outside Cloud Foundry.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun add @di-framework/cloudfoundry
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### 1. Using `@EnableCloudFoundryConnectors` and `@CloudFoundryService`
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Container } from '@di-framework/core/decorators';
|
|
35
|
+
import {
|
|
36
|
+
CloudFoundryService,
|
|
37
|
+
EnableCloudFoundryConnectors,
|
|
38
|
+
RelationalServiceInfo,
|
|
39
|
+
RedisServiceInfo,
|
|
40
|
+
VcapApplication,
|
|
41
|
+
CloudFoundryApplicationInfo,
|
|
42
|
+
} from '@di-framework/cloudfoundry';
|
|
43
|
+
|
|
44
|
+
@EnableCloudFoundryConnectors()
|
|
45
|
+
@Container()
|
|
46
|
+
export class ApplicationService {
|
|
47
|
+
@VcapApplication()
|
|
48
|
+
private appInfo!: CloudFoundryApplicationInfo;
|
|
49
|
+
|
|
50
|
+
@CloudFoundryService('my-postgres-db')
|
|
51
|
+
private dbService!: RelationalServiceInfo;
|
|
52
|
+
|
|
53
|
+
@CloudFoundryService('my-redis-cache')
|
|
54
|
+
private redisService!: RedisServiceInfo;
|
|
55
|
+
|
|
56
|
+
start() {
|
|
57
|
+
console.log(`Running app ${this.appInfo?.applicationName}`);
|
|
58
|
+
console.log(`Database URI: ${this.dbService.uri}`);
|
|
59
|
+
console.log(`Redis host: ${this.redisService.host}:${this.redisService.port}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Programmatic Discovery via `CloudFoundryEnvironment`
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { CloudFoundryEnvironment } from '@di-framework/cloudfoundry';
|
|
68
|
+
|
|
69
|
+
const cfEnv = new CloudFoundryEnvironment();
|
|
70
|
+
|
|
71
|
+
if (cfEnv.isCloudFoundry()) {
|
|
72
|
+
const appInfo = cfEnv.getApplicationInfo();
|
|
73
|
+
console.log(`Space: ${appInfo?.spaceName}, Instance: ${appInfo?.instanceIndex}`);
|
|
74
|
+
|
|
75
|
+
// Retrieve primary relational database
|
|
76
|
+
const db = cfEnv.getRelationalServiceInfo();
|
|
77
|
+
if (db) {
|
|
78
|
+
console.log(`Connected to ${db.dialect} at ${db.host}:${db.port}/${db.database}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Retrieve Redis service
|
|
82
|
+
const redis = cfEnv.getRedisServiceInfo();
|
|
83
|
+
if (redis) {
|
|
84
|
+
console.log(`Redis URI: ${redis.uri}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3. Custom Service Creators (SPI)
|
|
90
|
+
|
|
91
|
+
You can register custom service info creators with `ServiceInfoCreatorRegistry`:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import {
|
|
95
|
+
CloudFoundryServiceInfoCreator,
|
|
96
|
+
getDefaultRegistry,
|
|
97
|
+
RawVcapServiceData,
|
|
98
|
+
CloudFoundryServiceInfo,
|
|
99
|
+
} from '@di-framework/cloudfoundry';
|
|
100
|
+
|
|
101
|
+
export interface ElasticsearchServiceInfo extends CloudFoundryServiceInfo {
|
|
102
|
+
readonly uri: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class ElasticsearchServiceInfoCreator implements CloudFoundryServiceInfoCreator<ElasticsearchServiceInfo> {
|
|
106
|
+
accept(data: RawVcapServiceData): boolean {
|
|
107
|
+
return data.label.includes('elasticsearch') || (data.tags || []).includes('elasticsearch');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
createServiceInfo(data: RawVcapServiceData): ElasticsearchServiceInfo {
|
|
111
|
+
const creds = data.credentials || {};
|
|
112
|
+
return {
|
|
113
|
+
id: data.name,
|
|
114
|
+
name: data.name,
|
|
115
|
+
label: data.label,
|
|
116
|
+
tags: [...(data.tags || [])],
|
|
117
|
+
credentials: creds,
|
|
118
|
+
uri: String(creds.uri ?? 'http://localhost:9200'),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getDefaultRegistry().registerCreator(new ElasticsearchServiceInfoCreator());
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
Licensed under either [MIT](../../LICENSE-MIT) or [Apache-2.0](../../LICENSE-APACHE), at your option.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CloudFoundryApplicationInfo } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses VCAP_APPLICATION JSON string, object payload, or process environment
|
|
4
|
+
* into a strongly typed CloudFoundryApplicationInfo model.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseVcapApplication(input?: string | Record<string, unknown>): CloudFoundryApplicationInfo | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { CloudFoundryDetector } from './detector.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses VCAP_APPLICATION JSON string, object payload, or process environment
|
|
4
|
+
* into a strongly typed CloudFoundryApplicationInfo model.
|
|
5
|
+
*/
|
|
6
|
+
export function parseVcapApplication(input) {
|
|
7
|
+
if (input === null)
|
|
8
|
+
return null;
|
|
9
|
+
let rawObj = null;
|
|
10
|
+
if (input === undefined) {
|
|
11
|
+
const rawEnv = CloudFoundryDetector.getApplicationJson();
|
|
12
|
+
if (!rawEnv)
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
rawObj = JSON.parse(rawEnv);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (typeof input === 'string') {
|
|
22
|
+
try {
|
|
23
|
+
rawObj = JSON.parse(input);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (typeof input === 'object') {
|
|
30
|
+
if ('VCAP_APPLICATION' in input) {
|
|
31
|
+
const vcapVal = input.VCAP_APPLICATION;
|
|
32
|
+
if (typeof vcapVal === 'string' && vcapVal.trim().length > 0) {
|
|
33
|
+
try {
|
|
34
|
+
rawObj = JSON.parse(vcapVal);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (typeof vcapVal === 'object' && vcapVal !== null) {
|
|
41
|
+
rawObj = vcapVal;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Validate that the object actually has VCAP application fields
|
|
49
|
+
const hasAppFields = 'application_id' in input ||
|
|
50
|
+
'applicationId' in input ||
|
|
51
|
+
'app_id' in input ||
|
|
52
|
+
'application_name' in input ||
|
|
53
|
+
'applicationName' in input ||
|
|
54
|
+
'space_id' in input ||
|
|
55
|
+
'spaceId' in input ||
|
|
56
|
+
'space_name' in input ||
|
|
57
|
+
'spaceName' in input;
|
|
58
|
+
if (!hasAppFields) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
rawObj = input;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!rawObj || typeof rawObj !== 'object') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const applicationId = String(rawObj.application_id ?? rawObj.applicationId ?? rawObj.app_id ?? '');
|
|
68
|
+
const applicationName = String(rawObj.application_name ?? rawObj.applicationName ?? rawObj.name ?? '');
|
|
69
|
+
const rawUris = rawObj.application_uris ?? rawObj.applicationUris ?? rawObj.uris ?? [];
|
|
70
|
+
const applicationUris = Array.isArray(rawUris)
|
|
71
|
+
? rawUris.map(String)
|
|
72
|
+
: typeof rawUris === 'string'
|
|
73
|
+
? [rawUris]
|
|
74
|
+
: [];
|
|
75
|
+
const applicationVersion = rawObj.application_version ?? rawObj.applicationVersion ?? rawObj.version;
|
|
76
|
+
const spaceId = String(rawObj.space_id ?? rawObj.spaceId ?? '');
|
|
77
|
+
const spaceName = String(rawObj.space_name ?? rawObj.spaceName ?? '');
|
|
78
|
+
const organizationId = rawObj.organization_id ?? rawObj.organizationId ?? rawObj.org_id;
|
|
79
|
+
const organizationName = rawObj.organization_name ?? rawObj.organizationName ?? rawObj.org_name;
|
|
80
|
+
const instanceId = rawObj.instance_id ?? rawObj.instanceId;
|
|
81
|
+
let instanceIndex;
|
|
82
|
+
const rawIdx = rawObj.instance_index ?? rawObj.instanceIndex;
|
|
83
|
+
if (typeof rawIdx === 'number') {
|
|
84
|
+
instanceIndex = rawIdx;
|
|
85
|
+
}
|
|
86
|
+
else if (typeof rawIdx === 'string' && rawIdx.trim().length > 0) {
|
|
87
|
+
const parsed = parseInt(rawIdx, 10);
|
|
88
|
+
if (!Number.isNaN(parsed))
|
|
89
|
+
instanceIndex = parsed;
|
|
90
|
+
}
|
|
91
|
+
const host = typeof rawObj.host === 'string' ? rawObj.host : undefined;
|
|
92
|
+
let port;
|
|
93
|
+
if (typeof rawObj.port === 'number') {
|
|
94
|
+
port = rawObj.port;
|
|
95
|
+
}
|
|
96
|
+
else if (typeof rawObj.port === 'string' && rawObj.port.trim().length > 0) {
|
|
97
|
+
const parsed = parseInt(rawObj.port, 10);
|
|
98
|
+
if (!Number.isNaN(parsed))
|
|
99
|
+
port = parsed;
|
|
100
|
+
}
|
|
101
|
+
let limits;
|
|
102
|
+
if (typeof rawObj.limits === 'object' && rawObj.limits !== null) {
|
|
103
|
+
const rawLimits = rawObj.limits;
|
|
104
|
+
limits = {
|
|
105
|
+
disk: typeof rawLimits.disk === 'number' ? rawLimits.disk : undefined,
|
|
106
|
+
fds: typeof rawLimits.fds === 'number' ? rawLimits.fds : undefined,
|
|
107
|
+
mem: typeof rawLimits.mem === 'number' ? rawLimits.mem : undefined,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
applicationId,
|
|
112
|
+
applicationName,
|
|
113
|
+
applicationUris,
|
|
114
|
+
...(applicationVersion !== undefined ? { applicationVersion: String(applicationVersion) } : {}),
|
|
115
|
+
spaceId,
|
|
116
|
+
spaceName,
|
|
117
|
+
...(organizationId !== undefined ? { organizationId: String(organizationId) } : {}),
|
|
118
|
+
...(organizationName !== undefined ? { organizationName: String(organizationName) } : {}),
|
|
119
|
+
...(instanceId !== undefined ? { instanceId: String(instanceId) } : {}),
|
|
120
|
+
...(instanceIndex !== undefined ? { instanceIndex } : {}),
|
|
121
|
+
...(host !== undefined ? { host } : {}),
|
|
122
|
+
...(port !== undefined ? { port } : {}),
|
|
123
|
+
...(limits !== undefined ? { limits } : {}),
|
|
124
|
+
rawConfig: Object.freeze({ ...rawObj }),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CloudFoundryEnvironment } from './environment.js';
|
|
2
|
+
import type { EnableCloudFoundryConnectorsOptions } from './types.js';
|
|
3
|
+
export declare const CF_ENVIRONMENT_TOKEN = "cf:environment";
|
|
4
|
+
export declare const CF_APPLICATION_TOKEN = "cf:application";
|
|
5
|
+
export declare const CF_RELATIONAL_TOKEN = "cf:relational";
|
|
6
|
+
export declare const CF_REDIS_TOKEN = "cf:redis";
|
|
7
|
+
export declare const CF_AMQP_TOKEN = "cf:amqp";
|
|
8
|
+
export declare const CF_BLOB_TOKEN = "cf:blob";
|
|
9
|
+
export declare function bindCloudFoundryConnectors(targetContainer?: any, options?: EnableCloudFoundryConnectorsOptions): CloudFoundryEnvironment;
|
package/dist/bindings.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useContainer } from '@di-framework/core/container';
|
|
2
|
+
import { CloudFoundryEnvironment } from './environment.js';
|
|
3
|
+
export const CF_ENVIRONMENT_TOKEN = 'cf:environment';
|
|
4
|
+
export const CF_APPLICATION_TOKEN = 'cf:application';
|
|
5
|
+
export const CF_RELATIONAL_TOKEN = 'cf:relational';
|
|
6
|
+
export const CF_REDIS_TOKEN = 'cf:redis';
|
|
7
|
+
export const CF_AMQP_TOKEN = 'cf:amqp';
|
|
8
|
+
export const CF_BLOB_TOKEN = 'cf:blob';
|
|
9
|
+
// biome-ignore lint/suspicious/noExplicitAny: container interface
|
|
10
|
+
export function bindCloudFoundryConnectors(targetContainer, options = {}) {
|
|
11
|
+
const container = targetContainer ?? options.container ?? useContainer();
|
|
12
|
+
const cfEnv = new CloudFoundryEnvironment({
|
|
13
|
+
env: options.env,
|
|
14
|
+
localFallback: options.localFallback,
|
|
15
|
+
});
|
|
16
|
+
// 1. Register CloudFoundryEnvironment
|
|
17
|
+
if (typeof container.registerValue === 'function') {
|
|
18
|
+
container.registerValue(CloudFoundryEnvironment, cfEnv);
|
|
19
|
+
container.registerValue(CF_ENVIRONMENT_TOKEN, cfEnv);
|
|
20
|
+
}
|
|
21
|
+
else if (typeof container.registerInstance === 'function') {
|
|
22
|
+
container.registerInstance(CloudFoundryEnvironment, cfEnv);
|
|
23
|
+
container.registerInstance(CF_ENVIRONMENT_TOKEN, cfEnv);
|
|
24
|
+
}
|
|
25
|
+
// 2. Register Application Info
|
|
26
|
+
const appInfo = cfEnv.getApplicationInfo();
|
|
27
|
+
if (typeof container.registerValue === 'function') {
|
|
28
|
+
container.registerValue(CF_APPLICATION_TOKEN, appInfo);
|
|
29
|
+
}
|
|
30
|
+
// 3. Register Discovered Services
|
|
31
|
+
const allServices = cfEnv.getServiceInfos();
|
|
32
|
+
for (const service of allServices) {
|
|
33
|
+
if (typeof container.registerValue === 'function') {
|
|
34
|
+
// By specific name
|
|
35
|
+
container.registerValue(`cf:service:${service.name}`, service);
|
|
36
|
+
// By label
|
|
37
|
+
container.registerValue(`cf:service:${service.label}`, service);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// 4. Register primary service singletons for common types
|
|
41
|
+
const relational = cfEnv.getRelationalServiceInfo();
|
|
42
|
+
if (relational && typeof container.registerValue === 'function') {
|
|
43
|
+
container.registerValue(CF_RELATIONAL_TOKEN, relational);
|
|
44
|
+
container.registerValue(`cf:${relational.dialect}`, relational);
|
|
45
|
+
}
|
|
46
|
+
const redis = cfEnv.getRedisServiceInfo();
|
|
47
|
+
if (redis && typeof container.registerValue === 'function') {
|
|
48
|
+
container.registerValue(CF_REDIS_TOKEN, redis);
|
|
49
|
+
}
|
|
50
|
+
const amqp = cfEnv.getAmqpServiceInfo();
|
|
51
|
+
if (amqp && typeof container.registerValue === 'function') {
|
|
52
|
+
container.registerValue(CF_AMQP_TOKEN, amqp);
|
|
53
|
+
container.registerValue('cf:rabbitmq', amqp);
|
|
54
|
+
}
|
|
55
|
+
const blob = cfEnv.getBlobStorageServiceInfo();
|
|
56
|
+
if (blob && typeof container.registerValue === 'function') {
|
|
57
|
+
container.registerValue(CF_BLOB_TOKEN, blob);
|
|
58
|
+
container.registerValue('cf:s3', blob);
|
|
59
|
+
}
|
|
60
|
+
return cfEnv;
|
|
61
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CloudFoundryServiceInfoCreator } from '../spi/creator.js';
|
|
2
|
+
import type { AmqpServiceInfo, RawVcapServiceData } from '../types.js';
|
|
3
|
+
export declare class AmqpServiceInfoCreator implements CloudFoundryServiceInfoCreator<AmqpServiceInfo> {
|
|
4
|
+
accept(serviceData: RawVcapServiceData): boolean;
|
|
5
|
+
createServiceInfo(serviceData: RawVcapServiceData): AmqpServiceInfo;
|
|
6
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const AMQP_PATTERNS = ['rabbitmq', 'amqp', 'cloudamqp'];
|
|
2
|
+
function matchesAny(str, patterns) {
|
|
3
|
+
const lower = str.toLowerCase();
|
|
4
|
+
return patterns.some((p) => lower.includes(p));
|
|
5
|
+
}
|
|
6
|
+
export class AmqpServiceInfoCreator {
|
|
7
|
+
accept(serviceData) {
|
|
8
|
+
const label = serviceData.label || '';
|
|
9
|
+
const tags = (serviceData.tags || []).map((t) => String(t).toLowerCase());
|
|
10
|
+
const creds = (serviceData.credentials || {});
|
|
11
|
+
if (matchesAny(label, AMQP_PATTERNS)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (tags.some((t) => matchesAny(t, AMQP_PATTERNS) || t === 'messaging' || t === 'message-queue')) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const uriCandidate = String(creds.uri ?? creds.url ?? '');
|
|
18
|
+
if (uriCandidate.startsWith('amqp://') || uriCandidate.startsWith('amqps://')) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const urisArray = creds.uris;
|
|
22
|
+
if (Array.isArray(urisArray) &&
|
|
23
|
+
urisArray.some((u) => typeof u === 'string' && (u.startsWith('amqp://') || u.startsWith('amqps://')))) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
createServiceInfo(serviceData) {
|
|
29
|
+
const creds = (serviceData.credentials || {});
|
|
30
|
+
const tags = (serviceData.tags || []).map(String);
|
|
31
|
+
const rawUris = Array.isArray(creds.uris)
|
|
32
|
+
? creds.uris.map(String)
|
|
33
|
+
: typeof creds.uri === 'string'
|
|
34
|
+
? [creds.uri]
|
|
35
|
+
: typeof creds.url === 'string'
|
|
36
|
+
? [creds.url]
|
|
37
|
+
: [];
|
|
38
|
+
let primaryUri = rawUris[0] ?? '';
|
|
39
|
+
let host = String(creds.hostname ?? creds.host ?? 'localhost');
|
|
40
|
+
let port = typeof creds.port === 'number' ? creds.port : 5672;
|
|
41
|
+
if (typeof creds.port === 'string' && creds.port.trim().length > 0) {
|
|
42
|
+
const p = parseInt(creds.port, 10);
|
|
43
|
+
if (!Number.isNaN(p))
|
|
44
|
+
port = p;
|
|
45
|
+
}
|
|
46
|
+
let username = (creds.username ?? creds.user) ? String(creds.username ?? creds.user) : undefined;
|
|
47
|
+
let password = (creds.password ?? creds.pass) ? String(creds.password ?? creds.pass) : undefined;
|
|
48
|
+
let virtualHost = (creds.vhost ?? creds.virtual_host ?? creds.virtualHost)
|
|
49
|
+
? String(creds.vhost ?? creds.virtual_host ?? creds.virtualHost)
|
|
50
|
+
: undefined;
|
|
51
|
+
let ssl = typeof creds.ssl === 'boolean'
|
|
52
|
+
? creds.ssl
|
|
53
|
+
: typeof creds.tls === 'boolean'
|
|
54
|
+
? creds.tls
|
|
55
|
+
: undefined;
|
|
56
|
+
const httpManagementUri = (creds.http_api_uri ?? creds.http_management_uri ?? creds.httpManagementUri)
|
|
57
|
+
? String(creds.http_api_uri ?? creds.http_management_uri ?? creds.httpManagementUri)
|
|
58
|
+
: undefined;
|
|
59
|
+
if (primaryUri.length > 0) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = new URL(primaryUri);
|
|
62
|
+
if (parsed.protocol === 'amqps:') {
|
|
63
|
+
ssl = true;
|
|
64
|
+
}
|
|
65
|
+
else if (parsed.protocol === 'amqp:' && ssl === undefined) {
|
|
66
|
+
ssl = false;
|
|
67
|
+
}
|
|
68
|
+
if (parsed.hostname)
|
|
69
|
+
host = parsed.hostname;
|
|
70
|
+
if (parsed.port)
|
|
71
|
+
port = parseInt(parsed.port, 10);
|
|
72
|
+
if (parsed.username)
|
|
73
|
+
username = decodeURIComponent(parsed.username);
|
|
74
|
+
if (parsed.password)
|
|
75
|
+
password = decodeURIComponent(parsed.password);
|
|
76
|
+
if (parsed.pathname && parsed.pathname.length > 1) {
|
|
77
|
+
virtualHost = decodeURIComponent(parsed.pathname.slice(1));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore parse error
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!primaryUri) {
|
|
85
|
+
const scheme = ssl ? 'amqps' : 'amqp';
|
|
86
|
+
const auth = username ? (password ? `${username}:${password}@` : `${username}@`) : '';
|
|
87
|
+
const vhostPart = virtualHost ? `/${encodeURIComponent(virtualHost)}` : '';
|
|
88
|
+
primaryUri = `${scheme}://${auth}${host}:${port}${vhostPart}`;
|
|
89
|
+
}
|
|
90
|
+
const urisList = rawUris.length > 0 ? rawUris : [primaryUri];
|
|
91
|
+
return {
|
|
92
|
+
id: serviceData.name,
|
|
93
|
+
name: serviceData.name,
|
|
94
|
+
label: serviceData.label,
|
|
95
|
+
...(serviceData.plan ? { plan: serviceData.plan } : {}),
|
|
96
|
+
tags,
|
|
97
|
+
credentials: Object.freeze({ ...creds }),
|
|
98
|
+
host,
|
|
99
|
+
port,
|
|
100
|
+
...(virtualHost !== undefined ? { virtualHost } : {}),
|
|
101
|
+
...(username !== undefined ? { username } : {}),
|
|
102
|
+
...(password !== undefined ? { password } : {}),
|
|
103
|
+
uri: primaryUri,
|
|
104
|
+
uris: urisList,
|
|
105
|
+
...(ssl !== undefined ? { ssl } : {}),
|
|
106
|
+
...(httpManagementUri !== undefined ? { httpManagementUri } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CloudFoundryServiceInfoCreator } from '../spi/creator.js';
|
|
2
|
+
import type { BlobStorageServiceInfo, RawVcapServiceData } from '../types.js';
|
|
3
|
+
export declare class BlobStorageServiceInfoCreator implements CloudFoundryServiceInfoCreator<BlobStorageServiceInfo> {
|
|
4
|
+
accept(serviceData: RawVcapServiceData): boolean;
|
|
5
|
+
createServiceInfo(serviceData: RawVcapServiceData): BlobStorageServiceInfo;
|
|
6
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const S3_PATTERNS = ['s3', 'minio', 'objectstore', 'blobstore', 'blob-storage', 'ecs-s3'];
|
|
2
|
+
function matchesAny(str, patterns) {
|
|
3
|
+
const lower = str.toLowerCase();
|
|
4
|
+
return patterns.some((p) => lower.includes(p));
|
|
5
|
+
}
|
|
6
|
+
export class BlobStorageServiceInfoCreator {
|
|
7
|
+
accept(serviceData) {
|
|
8
|
+
const label = serviceData.label || '';
|
|
9
|
+
const tags = (serviceData.tags || []).map((t) => String(t).toLowerCase());
|
|
10
|
+
const creds = (serviceData.credentials || {});
|
|
11
|
+
if (matchesAny(label, S3_PATTERNS)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (tags.some((t) => matchesAny(t, S3_PATTERNS))) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if ((creds.bucket || creds.bucket_name || creds.bucketName) &&
|
|
18
|
+
(creds.access_key_id ||
|
|
19
|
+
creds.accessKeyId ||
|
|
20
|
+
creds.access_key ||
|
|
21
|
+
creds.aws_access_key_id ||
|
|
22
|
+
creds.endpoint)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
const uriCandidate = String(creds.uri ?? creds.url ?? '');
|
|
26
|
+
if (uriCandidate.startsWith('s3://')) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
createServiceInfo(serviceData) {
|
|
32
|
+
const creds = (serviceData.credentials || {});
|
|
33
|
+
const tags = (serviceData.tags || []).map(String);
|
|
34
|
+
let bucketName = String(creds.bucket_name ?? creds.bucketName ?? creds.bucket ?? creds.name ?? serviceData.name);
|
|
35
|
+
const endpoint = (creds.endpoint ?? creds.endpoint_url ?? creds.endpointUrl ?? creds.host)
|
|
36
|
+
? String(creds.endpoint ?? creds.endpoint_url ?? creds.endpointUrl ?? creds.host)
|
|
37
|
+
: undefined;
|
|
38
|
+
const region = (creds.region ?? creds.aws_region ?? creds.s3_region)
|
|
39
|
+
? String(creds.region ?? creds.aws_region ?? creds.s3_region)
|
|
40
|
+
: undefined;
|
|
41
|
+
let accessKeyId = (creds.access_key_id ?? creds.accessKeyId ?? creds.access_key ?? creds.aws_access_key_id)
|
|
42
|
+
? String(creds.access_key_id ?? creds.accessKeyId ?? creds.access_key ?? creds.aws_access_key_id)
|
|
43
|
+
: undefined;
|
|
44
|
+
let secretAccessKey = (creds.secret_access_key ??
|
|
45
|
+
creds.secretAccessKey ??
|
|
46
|
+
creds.secret_key ??
|
|
47
|
+
creds.aws_secret_access_key)
|
|
48
|
+
? String(creds.secret_access_key ??
|
|
49
|
+
creds.secretAccessKey ??
|
|
50
|
+
creds.secret_key ??
|
|
51
|
+
creds.aws_secret_access_key)
|
|
52
|
+
: undefined;
|
|
53
|
+
const pathStyle = typeof creds.path_style === 'boolean'
|
|
54
|
+
? creds.path_style
|
|
55
|
+
: typeof creds.pathStyle === 'boolean'
|
|
56
|
+
? creds.pathStyle
|
|
57
|
+
: undefined;
|
|
58
|
+
let rawUri = (creds.uri ?? creds.url) ? String(creds.uri ?? creds.url) : undefined;
|
|
59
|
+
if (rawUri?.startsWith('s3://')) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = new URL(rawUri);
|
|
62
|
+
if (parsed.hostname)
|
|
63
|
+
bucketName = parsed.hostname;
|
|
64
|
+
if (parsed.username)
|
|
65
|
+
accessKeyId = decodeURIComponent(parsed.username);
|
|
66
|
+
if (parsed.password)
|
|
67
|
+
secretAccessKey = decodeURIComponent(parsed.password);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// ignore parse error
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!rawUri && endpoint) {
|
|
74
|
+
rawUri =
|
|
75
|
+
endpoint.startsWith('http://') || endpoint.startsWith('https://')
|
|
76
|
+
? `${endpoint}/${bucketName}`
|
|
77
|
+
: `https://${endpoint}/${bucketName}`;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id: serviceData.name,
|
|
81
|
+
name: serviceData.name,
|
|
82
|
+
label: serviceData.label,
|
|
83
|
+
...(serviceData.plan ? { plan: serviceData.plan } : {}),
|
|
84
|
+
tags,
|
|
85
|
+
credentials: Object.freeze({ ...creds }),
|
|
86
|
+
bucketName,
|
|
87
|
+
...(endpoint !== undefined ? { endpoint } : {}),
|
|
88
|
+
...(region !== undefined ? { region } : {}),
|
|
89
|
+
...(accessKeyId !== undefined ? { accessKeyId } : {}),
|
|
90
|
+
...(secretAccessKey !== undefined ? { secretAccessKey } : {}),
|
|
91
|
+
...(rawUri !== undefined ? { uri: rawUri } : {}),
|
|
92
|
+
...(pathStyle !== undefined ? { pathStyle } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CloudFoundryServiceInfoCreator } from '../spi/creator.js';
|
|
2
|
+
import type { RawVcapServiceData, RedisServiceInfo } from '../types.js';
|
|
3
|
+
export declare class RedisServiceInfoCreator implements CloudFoundryServiceInfoCreator<RedisServiceInfo> {
|
|
4
|
+
accept(serviceData: RawVcapServiceData): boolean;
|
|
5
|
+
createServiceInfo(serviceData: RawVcapServiceData): RedisServiceInfo;
|
|
6
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const REDIS_PATTERNS = ['redis', 'rediscloud', 'redislabs'];
|
|
2
|
+
function matchesAny(str, patterns) {
|
|
3
|
+
const lower = str.toLowerCase();
|
|
4
|
+
return patterns.some((p) => lower.includes(p));
|
|
5
|
+
}
|
|
6
|
+
export class RedisServiceInfoCreator {
|
|
7
|
+
accept(serviceData) {
|
|
8
|
+
const label = serviceData.label || '';
|
|
9
|
+
const tags = (serviceData.tags || []).map((t) => String(t).toLowerCase());
|
|
10
|
+
const creds = (serviceData.credentials || {});
|
|
11
|
+
if (matchesAny(label, REDIS_PATTERNS)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (tags.some((t) => matchesAny(t, REDIS_PATTERNS) || t === 'cache')) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const uriCandidate = String(creds.uri ?? creds.url ?? '');
|
|
18
|
+
if (uriCandidate.startsWith('redis://') || uriCandidate.startsWith('rediss://')) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
createServiceInfo(serviceData) {
|
|
24
|
+
const creds = (serviceData.credentials || {});
|
|
25
|
+
const tags = (serviceData.tags || []).map(String);
|
|
26
|
+
let rawUri = String(creds.uri ?? creds.url ?? '');
|
|
27
|
+
let host = String(creds.host ?? creds.hostname ?? 'localhost');
|
|
28
|
+
let port = typeof creds.port === 'number' ? creds.port : 6379;
|
|
29
|
+
if (typeof creds.port === 'string' && creds.port.trim().length > 0) {
|
|
30
|
+
const p = parseInt(creds.port, 10);
|
|
31
|
+
if (!Number.isNaN(p))
|
|
32
|
+
port = p;
|
|
33
|
+
}
|
|
34
|
+
let password = creds.password ? String(creds.password) : undefined;
|
|
35
|
+
let database;
|
|
36
|
+
if (typeof creds.database === 'number') {
|
|
37
|
+
database = creds.database;
|
|
38
|
+
}
|
|
39
|
+
else if (typeof creds.db === 'number') {
|
|
40
|
+
database = creds.db;
|
|
41
|
+
}
|
|
42
|
+
let tls = typeof creds.tls === 'boolean'
|
|
43
|
+
? creds.tls
|
|
44
|
+
: typeof creds.ssl === 'boolean'
|
|
45
|
+
? creds.ssl
|
|
46
|
+
: undefined;
|
|
47
|
+
const cluster = typeof creds.cluster === 'boolean' ? creds.cluster : undefined;
|
|
48
|
+
if (rawUri.length > 0) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = new URL(rawUri);
|
|
51
|
+
if (parsed.protocol === 'rediss:') {
|
|
52
|
+
tls = true;
|
|
53
|
+
}
|
|
54
|
+
else if (parsed.protocol === 'redis:' && tls === undefined) {
|
|
55
|
+
tls = false;
|
|
56
|
+
}
|
|
57
|
+
if (parsed.hostname)
|
|
58
|
+
host = parsed.hostname;
|
|
59
|
+
if (parsed.port)
|
|
60
|
+
port = parseInt(parsed.port, 10);
|
|
61
|
+
if (parsed.password)
|
|
62
|
+
password = decodeURIComponent(parsed.password);
|
|
63
|
+
if (parsed.pathname && parsed.pathname.length > 1) {
|
|
64
|
+
const dbIndex = parseInt(parsed.pathname.slice(1), 10);
|
|
65
|
+
if (!Number.isNaN(dbIndex))
|
|
66
|
+
database = dbIndex;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// ignore url parse error
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!rawUri) {
|
|
74
|
+
const scheme = tls ? 'rediss' : 'redis';
|
|
75
|
+
const auth = password ? `:${encodeURIComponent(password)}@` : '';
|
|
76
|
+
const dbPath = database !== undefined ? `/${database}` : '';
|
|
77
|
+
rawUri = `${scheme}://${auth}${host}:${port}${dbPath}`;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id: serviceData.name,
|
|
81
|
+
name: serviceData.name,
|
|
82
|
+
label: serviceData.label,
|
|
83
|
+
...(serviceData.plan ? { plan: serviceData.plan } : {}),
|
|
84
|
+
tags,
|
|
85
|
+
credentials: Object.freeze({ ...creds }),
|
|
86
|
+
host,
|
|
87
|
+
port,
|
|
88
|
+
...(password !== undefined ? { password } : {}),
|
|
89
|
+
...(database !== undefined ? { database } : {}),
|
|
90
|
+
uri: rawUri,
|
|
91
|
+
...(tls !== undefined ? { tls } : {}),
|
|
92
|
+
...(cluster !== undefined ? { cluster } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CloudFoundryServiceInfoCreator } from '../spi/creator.js';
|
|
2
|
+
import type { RawVcapServiceData, RelationalServiceInfo } from '../types.js';
|
|
3
|
+
export declare class RelationalServiceInfoCreator implements CloudFoundryServiceInfoCreator<RelationalServiceInfo> {
|
|
4
|
+
accept(serviceData: RawVcapServiceData): boolean;
|
|
5
|
+
createServiceInfo(serviceData: RawVcapServiceData): RelationalServiceInfo;
|
|
6
|
+
}
|