@saws/rustfs-file-service 2.0.0-beta.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/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@saws/rustfs-file-service",
3
+ "version": "2.0.0-beta.3",
4
+ "description": "",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "dependencies": {
16
+ "@saws/core": "2.0.0-beta.3",
17
+ "@saws/docker-service": "2.0.0-beta.3"
18
+ }
19
+ }
@@ -0,0 +1,205 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { SecretsManager, type ServiceEnvironmentTarget } from "@saws/core";
5
+ import type { Outputs } from "@saws/core/utils/stage-outputs";
6
+ import {
7
+ DockerService,
8
+ type DockerRunConfig,
9
+ type DockerServiceConfig,
10
+ } from "@saws/docker-service";
11
+
12
+ export type RustFsFileConnectionTarget = "container" | "host";
13
+
14
+ export type RustFsFileConnectionInfo = {
15
+ endpoint: string;
16
+ dashboardEndpoint: string;
17
+ accessKeyId: string;
18
+ secretAccessKey: string;
19
+ region: string;
20
+ bucket: string;
21
+ };
22
+
23
+ export interface RustFsFileServiceConfig extends Omit<
24
+ DockerServiceConfig,
25
+ "image" | "dockerfile" | "buildContext" | "volumes" | "ports" | "command"
26
+ > {
27
+ image?: string;
28
+ /** Host port to expose the S3-compatible API on. Defaults to 9000. */
29
+ apiPort?: number;
30
+ /** Host port to expose the RustFS dashboard on. Defaults to 9001. */
31
+ dashboardPort?: number;
32
+ accessKeyId?: string;
33
+ /** Explicit RustFS secret access key. Omit to persist a generated key in SAWS secrets. */
34
+ secretAccessKey?: string;
35
+ region?: string;
36
+ bucket?: string;
37
+ /** Override the Docker volume name. By default SAWS derives a stable stage/service volume. */
38
+ volume?: string;
39
+ dataDirectory?: string;
40
+ }
41
+
42
+ export class RustFsFileService extends DockerService {
43
+ readonly apiPort?: number;
44
+ readonly dashboardPort?: number;
45
+ readonly accessKeyId: string;
46
+ readonly secretAccessKey?: string;
47
+ readonly region: string;
48
+ readonly bucket?: string;
49
+ readonly volume?: string;
50
+ readonly dataDirectory: string;
51
+ protected override readonly serviceType = "rustfs-file";
52
+
53
+ constructor(config: RustFsFileServiceConfig) {
54
+ super({
55
+ ...config,
56
+ image: config.image ?? "rustfs/rustfs:latest",
57
+ command: [config.dataDirectory ?? "/data"],
58
+ });
59
+
60
+ this.apiPort = config.apiPort;
61
+ this.dashboardPort = config.dashboardPort;
62
+ this.accessKeyId = config.accessKeyId ?? "rustfsadmin";
63
+ this.secretAccessKey = config.secretAccessKey;
64
+ this.region = config.region ?? "us-east-1";
65
+ this.bucket = config.bucket;
66
+ this.volume = config.volume;
67
+ this.dataDirectory = config.dataDirectory ?? "/data";
68
+ }
69
+
70
+ override async getEnvironmentVariables(
71
+ stage: string,
72
+ target: ServiceEnvironmentTarget = "container",
73
+ ): Promise<Record<string, string>> {
74
+ const connection = await this.getConnectionInfo(stage, target);
75
+ const prefix = this.environmentVariablePrefix;
76
+
77
+ return {
78
+ [`${prefix}_FILES_ENDPOINT`]: connection.endpoint,
79
+ [`${prefix}_FILES_DASHBOARD_ENDPOINT`]: connection.dashboardEndpoint,
80
+ [`${prefix}_FILES_ACCESS_KEY_ID`]: connection.accessKeyId,
81
+ [`${prefix}_FILES_SECRET_ACCESS_KEY`]: connection.secretAccessKey,
82
+ [`${prefix}_FILES_REGION`]: connection.region,
83
+ [`${prefix}_FILES_BUCKET`]: connection.bucket,
84
+ };
85
+ }
86
+
87
+ async getConnectionInfo(
88
+ stage: string,
89
+ target: RustFsFileConnectionTarget = "host",
90
+ ): Promise<RustFsFileConnectionInfo> {
91
+ const host = target === "container" ? this.getContainerName(stage) : this.getHost(stage);
92
+ const apiPort = target === "container" ? 9000 : this.getApiHostPort();
93
+ const dashboardPort = target === "container" ? 9001 : this.getDashboardHostPort();
94
+
95
+ return {
96
+ endpoint: this.toHttpUrl(host, apiPort),
97
+ dashboardEndpoint: this.toHttpUrl(host, dashboardPort),
98
+ accessKeyId: this.accessKeyId,
99
+ secretAccessKey: this.secretAccessKey ?? (await this.getOrCreateSecretAccessKey(stage)),
100
+ region: this.region,
101
+ bucket: this.bucket ?? this.defaultBucketName(stage),
102
+ };
103
+ }
104
+
105
+ protected override async getContainerEnvironment(stage: string): Promise<Record<string, string>> {
106
+ const connection = await this.getConnectionInfo(stage, "container");
107
+
108
+ return {
109
+ ...(await super.getContainerEnvironment(stage)),
110
+ RUSTFS_ACCESS_KEY: connection.accessKeyId,
111
+ RUSTFS_SECRET_KEY: connection.secretAccessKey,
112
+ RUSTFS_CONSOLE_ENABLE: "true",
113
+ };
114
+ }
115
+
116
+ protected override async getDockerRunConfig(
117
+ stage: string,
118
+ deploy: boolean,
119
+ ): Promise<DockerRunConfig> {
120
+ const config = await super.getDockerRunConfig(stage, deploy);
121
+
122
+ return {
123
+ ...config,
124
+ volumes: [`${this.getVolumeName(stage)}:${this.dataDirectory}`],
125
+ ports: [`${this.getApiHostPort()}:9000`, `${this.getDashboardHostPort()}:9001`],
126
+ };
127
+ }
128
+
129
+ protected override async onContainerStarted(stage: string) {
130
+ await this.setOutputs(this.toOutputs(await this.getConnectionInfo(stage, "host")), stage);
131
+ }
132
+
133
+ private get environmentVariablePrefix() {
134
+ return this.name.replace(/[^a-zA-Z\d]/g, "_").toUpperCase();
135
+ }
136
+
137
+ private getHost(stage: string) {
138
+ if (stage === "local") return "localhost";
139
+ return this.host?.address ?? this.getContainerName(stage);
140
+ }
141
+
142
+ private getApiHostPort() {
143
+ return this.apiPort ?? 9000;
144
+ }
145
+
146
+ private getDashboardHostPort() {
147
+ return this.dashboardPort ?? 9001;
148
+ }
149
+
150
+ private getVolumeName(stage: string) {
151
+ return this.volume ?? `${stage}-${this.name}-rustfs-data`.replaceAll("_", "-").toLowerCase();
152
+ }
153
+
154
+ private defaultBucketName(stage: string) {
155
+ return `${stage}-${this.name}`.toLowerCase().replace(/[^a-z0-9.-]/g, "-");
156
+ }
157
+
158
+ private toOutputs(connection: RustFsFileConnectionInfo): Outputs {
159
+ return {
160
+ endpoint: connection.endpoint,
161
+ dashboardEndpoint: connection.dashboardEndpoint,
162
+ accessKeyId: connection.accessKeyId,
163
+ secretAccessKey: connection.secretAccessKey,
164
+ region: connection.region,
165
+ bucket: connection.bucket,
166
+ };
167
+ }
168
+
169
+ private toHttpUrl(host: string, port: number) {
170
+ const normalizedHost = host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
171
+ return `http://${normalizedHost}:${port}`;
172
+ }
173
+
174
+ private async getOrCreateSecretAccessKey(stage: string) {
175
+ const manager = new SecretsManager({ stage });
176
+ const secretName = `${this.name}-rustfs-secret-access-key`;
177
+
178
+ try {
179
+ return await manager.get(secretName);
180
+ } catch (error) {
181
+ if ((error as Error).name !== "ParameterNotFound") throw error;
182
+ }
183
+
184
+ const legacySecretAccessKey = await this.getLegacySecretAccessKey(stage, secretName);
185
+ if (legacySecretAccessKey != null) {
186
+ await manager.set(secretName, legacySecretAccessKey);
187
+ return legacySecretAccessKey;
188
+ }
189
+
190
+ const secretAccessKey = randomBytes(32).toString("base64url");
191
+ await manager.set(secretName, secretAccessKey);
192
+ return secretAccessKey;
193
+ }
194
+
195
+ private async getLegacySecretAccessKey(stage: string, secretName: string) {
196
+ const secretPath = path.resolve(".saws", "secrets", stage, secretName);
197
+
198
+ try {
199
+ return (await readFile(secretPath, "utf8")).trim();
200
+ } catch (error) {
201
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
202
+ return null;
203
+ }
204
+ }
205
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./RustFsFileService.js";
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig-node.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "tsBuildInfoFile": "./dist/.tsbuildinfo"
7
+ },
8
+ "references": [{ "path": "../core/tsconfig.json" }, { "path": "../docker-service/tsconfig.json" }]
9
+ }