Skip to content

Commit 4aebaeb

Browse files
authored
feat!: add AWS S3 Support (#165)
* chore: yarn add aws-sdk * refactor: add storageProvider interface in router * feat: add googleStorage.ts * feat: add awsS3Storage * refactor: adapt standaloneServer to new router refactoring * chore: update README with AWS S3 * feat: export standard storage providers
1 parent ac8cdf3 commit 4aebaeb

File tree

8 files changed

+233
-65
lines changed

8 files changed

+233
-65
lines changed

README.md

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,29 @@
66

77
<!-- toc -->
88

9-
- [Features](#features)
10-
- [Limitations](#limitations)
11-
- [Screenshots](#screenshots)
12-
- [Setup](#setup)
13-
- [Usage](#usage)
9+
- [Features](#features)
10+
- [Limitations](#limitations)
11+
- [Screenshots](#screenshots)
12+
- [Setup](#setup)
13+
- [Usage](#usage)
1414

1515
## Features
1616

1717
- List all dbt models and tests
1818
- Get details on dbt models and tests like:
19-
- Documentations
20-
- Stats
21-
- Columns
22-
- Dependency graph
23-
- Code source (raw and compiled)
19+
- Documentations
20+
- Stats
21+
- Columns
22+
- Dependency graph
23+
- Code source (raw and compiled)
2424

2525
## Limitations
2626

27-
**This version only support Google Cloud Storage as backend to store `manifest.json`
28-
and `catalog.json` files.**
27+
As of now, the plugin only support the following backends:
28+
29+
- [x] Google Cloud Storage
30+
- [x] AWS S3
31+
- [ ] Azure Blob Storage
2932

3033
## Screenshots
3134

@@ -59,16 +62,16 @@ yarn --cwd packages/backend add @iiben_orgii/backstage-plugin-dbt-backend
5962
```tsx
6063
// packages/app/src/components/catalog/EntityPage.tsx
6164

62-
import { DbtPage, isDBTAvailable } from "@iiben_orgii/backstage-plugin-dbt"
65+
import { DbtPage, isDBTAvailable } from "@iiben_orgii/backstage-plugin-dbt";
6366

6467
// Farther down at the serviceEntityPage declaration
6568
const serviceEntityPage = (
66-
<EntityLayout>
67-
{/* Place the following section where you want the tab to appear */}
68-
<EntityLayout.Route if={isDBTAvailable} path="/dbt" title="dbt">
69-
<DbtPage />
70-
</EntityLayout.Route>
71-
</EntityLayout>
69+
<EntityLayout>
70+
{/* Place the following section where you want the tab to appear */}
71+
<EntityLayout.Route if={isDBTAvailable} path="/dbt" title="dbt">
72+
<DbtPage />
73+
</EntityLayout.Route>
74+
</EntityLayout>
7275
);
7376
```
7477

@@ -78,15 +81,21 @@ const serviceEntityPage = (
7881

7982
```ts
8083
// packages/backend/src/plugins/dbt.ts
81-
import { createRouter } from '@iiben_orgii/backstage-plugin-dbt-backend';
82-
import { Router } from 'express';
83-
import { PluginEnvironment } from '../types';
84+
import {
85+
createRouter,
86+
GoogleStorageProvider,
87+
} from "@iiben_orgii/backstage-plugin-dbt-backend";
88+
import { Router } from "express";
89+
import { PluginEnvironment } from "../types";
90+
91+
const storageProvider = new GoogleStorageProvider();
8492

8593
export default async function createPlugin(
8694
env: PluginEnvironment,
8795
): Promise<Router> {
8896
return await createRouter({
8997
logger: env.logger,
98+
storageProvider: storageProvider,
9099
});
91100
}
92101
```
@@ -97,14 +106,14 @@ then you have to add the route as follows:
97106

98107
```ts
99108
// packages/backend/src/index.ts
100-
import dbt from './plugins/dbt';
109+
import dbt from "./plugins/dbt";
101110

102111
async function main() {
103-
//...
104-
const dbtEnv = useHotMemoize(module, () => createEnv('dbt'));
105-
//...
106-
apiRouter.use('/dbt', await dbt(dbtEnv));
107-
//...
112+
//...
113+
const dbtEnv = useHotMemoize(module, () => createEnv("dbt"));
114+
//...
115+
apiRouter.use("/dbt", await dbt(dbtEnv));
116+
//...
108117
}
109118
```
110119

@@ -115,15 +124,16 @@ async function main() {
115124
You can define one bucket with all your manifest and catalog files.
116125

117126
Add a file `application/packages/app/config.d.ts`:
127+
118128
```ts
119129
export interface Config {
120-
dbtdoc: {
121-
/**
122-
* Frontend root URL
123-
* @visibility frontend
124-
*/
125-
bucket: string;
126-
};
130+
dbtdoc: {
131+
/**
132+
* Frontend root URL
133+
* @visibility frontend
134+
*/
135+
bucket: string;
136+
};
127137
}
128138
```
129139

@@ -138,6 +148,7 @@ Update the file `application/packages/app/package.json` with
138148
```
139149

140150
Then you can add to your `app-config.yaml`:
151+
141152
```yaml
142153
dbtdoc:
143154
bucket: your-bucket-123
@@ -165,7 +176,8 @@ metadata:
165176
**Following path must be respect regardless your bucket setup (single or multi).**
166177

167178
You can upload your `manifest.json` and `catalog.json` to a GCS Bucket as follow:
179+
168180
- `{dbtdoc-bucket}/{kind}/{name}/manifest.json`
169181
- `{dbtdoc-bucket}/{kind}/{name}/catalog.json`
170182

171-
For authentification to GCS Bucket, the plugin use ADC credentials [https://cloud.google.com/docs/authentication/provide-credentials-adc](https://cloud.google.com/docs/authentication/provide-credentials-adc).
183+
For authentification to GCS Bucket, the plugin use ADC credentials [https://cloud.google.com/docs/authentication/provide-credentials-adc](https://cloud.google.com/docs/authentication/provide-credentials-adc).

packages/dbt-backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@backstage/backend-common": "^0.19.1",
3333
"@backstage/config": "^1.0.8",
3434
"@types/express": "*",
35+
"aws-sdk": "^2.1472.0",
3536
"express": "^4.17.1",
3637
"express-promise-router": "^4.1.0",
3738
"node-fetch": "^3.3.1",

packages/dbt-backend/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export * from './service/router';
1+
export * from "./service/router";
2+
export * from "./service/awsS3Storage";
3+
export * from "./service/googleStorage";
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { S3 } from 'aws-sdk';
2+
3+
/**
4+
* AwsS3StorageProvider is a class that provides functionality for downloading files
5+
* from Amazon S3 storage using the AWS SDK.
6+
*/
7+
export class AwsS3StorageProvider {
8+
private s3 = new S3();
9+
10+
/**
11+
* Downloads a file from Amazon S3 storage.
12+
* @param bucket - The name of the S3 bucket.
13+
* @param filePath - The path to the file within the bucket.
14+
* @returns A Promise that resolves to the file's contents as a Buffer.
15+
*/
16+
async downloadFile(bucket: string, filePath: string): Promise<Buffer> {
17+
const params = { Bucket: bucket, Key: filePath };
18+
const { Body } = await this.s3.getObject(params).promise();
19+
return Body as Buffer;
20+
}
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Storage } from '@google-cloud/storage';
2+
3+
/**
4+
* GoogleStorageProvider is a class that provides functionality for downloading files
5+
* from Google Cloud Storage.
6+
*/
7+
export class GoogleStorageProvider {
8+
private storage = new Storage();
9+
10+
/**
11+
* Downloads a file from Google Cloud Storage.
12+
* @param bucket - The name of the storage bucket.
13+
* @param filePath - The path to the file within the bucket.
14+
* @returns A Promise that resolves to the file's contents as a Buffer.
15+
*/
16+
async downloadFile(bucket: string, filePath: string): Promise<Buffer> {
17+
const [contents] = await this.storage.bucket(bucket).file(filePath).download();
18+
return contents;
19+
}
20+
}

packages/dbt-backend/src/service/router.ts

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,59 @@
11
import { errorHandler } from '@backstage/backend-common';
2-
import express from 'express';
2+
import express, { Request, Response } from 'express';
33
import Router from 'express-promise-router';
44
import { Logger } from 'winston';
55

6-
const { Storage } = require('@google-cloud/storage');
7-
8-
// Creates a client
9-
const storage = new Storage();
10-
6+
/**
7+
* Abstracts the storage provider to allow for different
8+
* storage providers to be used.
9+
*/
10+
export interface StorageProvider {
11+
/**
12+
* Download a file from the storage provider.
13+
* @param bucket - The name of the storage bucket.
14+
* @param filePath - The path to the file within the bucket.
15+
* @returns A Promise that resolves to the file's contents as a Buffer.
16+
*/
17+
downloadFile(bucket: string, filePath: string): Promise<Buffer>;
18+
}
1119

1220
export interface RouterOptions {
1321
logger: Logger;
22+
storageProvider: StorageProvider;
1423
}
1524

16-
export async function createRouter(
17-
options: RouterOptions,
18-
): Promise<express.Router> {
19-
const { logger } = options;
20-
25+
/**
26+
* Creates an Express router for handling storage-related requests.
27+
* @param options - Configuration options for the router.
28+
* @returns An Express router that handles storage requests.
29+
*/
30+
export function createRouter(options: RouterOptions): express.Router {
31+
const { logger, storageProvider } = options;
2132
const router = Router();
2233
router.use(express.json());
2334

24-
router.get('/manifest/:bucket/:kind/:name', async (req, response) => {
25-
const file_path = `${req.params.kind}/${req.params.name}/manifest.json`
26-
logger.info(`Get manifest under ${req.params.bucket}/${req.params.kind}/${req.params.name}/manifest.json`)
27-
const contents = await storage.bucket(req.params.bucket).file(file_path).download();
28-
const result = JSON.parse(contents.toString())
29-
response.json(result);
35+
async function handleRequest(req: Request, res: Response, type: 'manifest' | 'catalog') {
36+
const { bucket, kind, name } = req.params;
37+
const filePath = `${kind}/${name}/${type}.json`;
38+
const fullPath = `${bucket}/${filePath}`;
39+
40+
try {
41+
logger.info(`Get ${type} under ${fullPath}`);
42+
const contents = await storageProvider.downloadFile(bucket, filePath);
43+
const result = JSON.parse(contents.toString());
44+
res.json(result);
45+
} catch (error: any) {
46+
logger.error(`Error getting ${type} under ${fullPath}: ${error.message}`);
47+
res.status(500).json({ error: `Error getting ${type} under ${fullPath}: ${error.message}` });
48+
}
49+
}
50+
51+
router.get('/manifest/:bucket/:kind/:name', async (req: Request, res: Response) => {
52+
await handleRequest(req, res, 'manifest');
3053
});
3154

32-
router.get('/catalog/:bucket/:kind/:name', async (req, response) => {
33-
const file_path = `${req.params.kind}/${req.params.name}/catalog.json`
34-
logger.info(`Get catalog under ${req.params.bucket}/${req.params.kind}/${req.params.name}/catalog.json`)
35-
const contents = await storage.bucket(req.params.bucket).file(file_path).download();
36-
const result = JSON.parse(contents.toString())
37-
response.json(result);
55+
router.get('/catalog/:bucket/:kind/:name', async (req: Request, res: Response) => {
56+
await handleRequest(req, res, 'catalog');
3857
});
3958

4059
router.use(errorHandler());

packages/dbt-backend/src/service/standaloneServer.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { createServiceBuilder } from '@backstage/backend-common';
22
import { Server } from 'http';
33
import { Logger } from 'winston';
4-
import { createRouter } from './router';
4+
5+
// Import the router creation function based on your modularized structure
6+
import { createRouter, RouterOptions } from './router';
7+
import { GoogleStorageProvider } from './googleStorage';
8+
9+
// Create the storage provider instance, default
10+
// uses the Google Cloud Storage provider to maintain
11+
// regression.
12+
const storageProvider = new GoogleStorageProvider();
13+
514

615
export interface ServerOptions {
716
port: number;
@@ -14,13 +23,20 @@ export async function startStandaloneServer(
1423
): Promise<Server> {
1524
const logger = options.logger.child({ service: 'dbt-backend' });
1625
logger.debug('Starting application server...');
17-
const router = await createRouter({
26+
27+
// Create router options and include the logger
28+
const routerOptions: RouterOptions = {
1829
logger,
19-
});
30+
storageProvider,
31+
};
32+
33+
// Create the router using the modularized router creation function
34+
const router = createRouter(routerOptions);
2035

2136
let service = createServiceBuilder(module)
2237
.setPort(options.port)
2338
.addRouter('/dbt', router);
39+
2440
if (options.enableCors) {
2541
service = service.enableCors({ origin: 'http://localhost:3000' });
2642
}

0 commit comments

Comments
 (0)