Skip to content

Commit

Permalink
Merge pull request opensearch-project#547 from Swiddis/osints/main
Browse files Browse the repository at this point in the history
Stub router for integrations project

Signed-off-by: Simeon Widdis <[email protected]>
Swiddis committed Jul 11, 2023
1 parent bdc4701 commit 00bc374
Showing 9 changed files with 789 additions and 12 deletions.
30 changes: 30 additions & 0 deletions .husky/_/husky.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/sh
if [ -z "$husky_skip_init" ]; then
debug () {
[ "$HUSKY_DEBUG" = "1" ] && echo "husky (debug) - $1"
}

readonly hook_name="$(basename "$0")"
debug "starting $hook_name..."

if [ "$HUSKY" = "0" ]; then
debug "HUSKY env variable is set to 0, skipping hook"
exit 0
fi

if [ -f ~/.huskyrc ]; then
debug "sourcing ~/.huskyrc"
. ~/.huskyrc
fi

export readonly husky_skip_init=1
sh -e "$0" "$@"
exitCode="$?"

if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
exit $exitCode
fi

exit 0
fi
6 changes: 6 additions & 0 deletions common/constants/shared.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ export const DSL_SEARCH = '/search';
export const DSL_CAT = '/cat.indices';
export const DSL_MAPPING = '/indices.getFieldMapping';
export const OBSERVABILITY_BASE = '/api/observability';
export const INTEGRATIONS_BASE = '/api/integrations';
export const EVENT_ANALYTICS = '/event_analytics';
export const SAVED_OBJECTS = '/saved_objects';
export const SAVED_QUERY = '/query';
@@ -51,6 +52,10 @@ export const observabilityPanelsID = 'observability-dashboards';
export const observabilityPanelsTitle = 'Dashboards';
export const observabilityPanelsPluginOrder = 5095;

export const observabilityIntegrationsID = 'observability-integrations';
export const observabilityIntegrationsTitle = 'Integrations';
export const observabilityIntegrationsPluginOrder = 5096;

// Shared Constants
export const SQL_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/search-plugins/sql/index/';
export const PPL_DOCUMENTATION_URL =
@@ -69,6 +74,7 @@ export const PPL_NEWLINE_REGEX = /[\n\r]+/g;

// Observability plugin URI
const BASE_OBSERVABILITY_URI = '/_plugins/_observability';
const BASE_INTEGRATIONS_URI = '/_plugins/_integrations'; // Used later in front-end for routing
export const OPENSEARCH_PANELS_API = {
OBJECT: `${BASE_OBSERVABILITY_URI}/object`,
};
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -19,8 +19,10 @@
"@reduxjs/toolkit": "^1.6.1",
"ag-grid-community": "^27.3.0",
"ag-grid-react": "^27.3.0",
"ajv": "^8.11.0",
"antlr4": "4.8.0",
"antlr4ts": "^0.5.0-alpha.4",
"mime": "^3.0.0",
"performance-now": "^2.1.0",
"plotly.js-dist": "^2.2.0",
"postinstall": "^0.7.4",
@@ -32,13 +34,16 @@
"devDependencies": {
"@cypress/skip-test": "^2.6.1",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/mime": "^3.0.1",
"@types/react-plotly.js": "^2.5.0",
"@types/react-test-renderer": "^16.9.1",
"antlr4ts-cli": "^0.5.0-alpha.4",
"cypress": "^6.0.0",
"cypress-watch-and-reload": "^1.10.6",
"eslint": "^6.8.0",
"jest-dom": "^4.0.0",
"lint-staged": "^13.1.0",
"mock-fs": "^4.12.0",
"ts-jest": "^29.1.0"
},
"resolutions": {
32 changes: 32 additions & 0 deletions server/adaptors/integrations/integrations_adaptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export interface IntegrationsAdaptor {
getIntegrationTemplates: (
query?: IntegrationTemplateQuery
) => Promise<IntegrationTemplateSearchResult>;

getIntegrationInstances: (
query?: IntegrationInstanceQuery
) => Promise<IntegrationInstancesSearchResult>;

getIntegrationInstance: (query?: IntegrationInstanceQuery) => Promise<IntegrationInstanceResult>;

loadIntegrationInstance: (
templateName: string,
name: string,
dataSource: string
) => Promise<IntegrationInstance>;

deleteIntegrationInstance: (id: string) => Promise<unknown>;

getStatic: (templateName: string, path: string) => Promise<Buffer>;

getSchemas: (
templateName: string
) => Promise<{ mappings: { [key: string]: unknown }; schemas: { [key: string]: unknown } }>;

getAssets: (templateName: string) => Promise<{ savedObjects?: unknown }>;
}
84 changes: 84 additions & 0 deletions server/adaptors/integrations/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

interface IntegrationTemplate {
name: string;
version: string;
displayName?: string;
integrationType: string;
license: string;
type: string;
author?: string;
description?: string;
sourceUrl?: string;
statics?: {
logo?: StaticAsset;
gallery?: StaticAsset[];
darkModeLogo?: StaticAsset;
darkModeGallery?: StaticAsset[];
};
components: IntegrationComponent[];
assets: {
savedObjects?: {
name: string;
version: string;
};
};
}

interface StaticAsset {
annotation?: string;
path: string;
}

interface IntegrationComponent {
name: string;
version: string;
}

interface DisplayAsset {
body: string;
}

interface IntegrationTemplateSearchResult {
hits: IntegrationTemplate[];
}

interface IntegrationTemplateQuery {
name?: string;
}

interface IntegrationInstance {
name: string;
templateName: string;
dataSource: {
sourceType: string;
dataset: string;
namespace: string;
};
creationDate: string;
assets: AssetReference[];
}

interface IntegrationInstanceResult extends IntegrationInstance {
id: string;
status: string;
}

interface AssetReference {
assetType: string;
assetId: string;
isDefaultAsset: boolean;
description: string;
}

interface IntegrationInstancesSearchResult {
hits: IntegrationInstanceResult[];
}

interface IntegrationInstanceQuery {
added?: boolean;
id?: string;
}
9 changes: 5 additions & 4 deletions server/routes/index.ts
Original file line number Diff line number Diff line change
@@ -20,16 +20,16 @@ import { registerSqlRoute } from './notebooks/sqlRouter';
import { registerEventAnalyticsRouter } from './event_analytics/event_analytics_router';
import { registerAppAnalyticsRouter } from './application_analytics/app_analytics_router';
import { registerMetricsRoute } from './metrics/metrics_rounter';

import { registerIntegrationsRoute } from './integrations/integrations_router';

export function setupRoutes({ router, client }: { router: IRouter; client: ILegacyClusterClient }) {
PanelsRouter(router);
VisualizationsRouter(router);
registerPplRoute({ router, facet: new PPLFacet(client) });
registerDslRoute({ router, facet: new DSLFacet(client)});
registerDslRoute({ router, facet: new DSLFacet(client) });
registerEventAnalyticsRouter({ router, savedObjectFacet: new SavedObjectFacet(client) });
registerAppAnalyticsRouter(router);

// TODO remove trace analytics route when DSL route for autocomplete is added
registerTraceAnalyticsDslRouter(router);

@@ -41,4 +41,5 @@ export function setupRoutes({ router, client }: { router: IRouter; client: ILega
registerSqlRoute(router, queryService);

registerMetricsRoute(router);
};
registerIntegrationsRoute(router);
}
53 changes: 53 additions & 0 deletions server/routes/integrations/__tests__/integrations_router.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { OpenSearchDashboardsResponseFactory } from '../../../../../../src/core/server/http/router';
import { handleWithCallback } from '../integrations_router';
import { IntegrationsAdaptor } from 'server/adaptors/integrations/integrations_adaptor';

describe('handleWithCallback', () => {
let adaptorMock: jest.Mocked<IntegrationsAdaptor>;
let responseMock: jest.Mocked<OpenSearchDashboardsResponseFactory>;

beforeEach(() => {
adaptorMock = {} as any;
responseMock = {
custom: jest.fn((data) => data),
ok: jest.fn((data) => data),
} as any;
});

it('retrieves data from the callback method', async () => {
const callback = jest.fn((_) => {
return { test: 'data' };
});

const result = await handleWithCallback(
adaptorMock as IntegrationsAdaptor,
responseMock as OpenSearchDashboardsResponseFactory,
callback
);

expect(callback).toHaveBeenCalled();
expect(responseMock.ok).toHaveBeenCalled();
expect(result.body.data).toEqual({ test: 'data' });
});

it('passes callback errors through', async () => {
const callback = jest.fn((_) => {
throw new Error('test error');
});

const result = await handleWithCallback(
adaptorMock as IntegrationsAdaptor,
responseMock as OpenSearchDashboardsResponseFactory,
callback
);

expect(callback).toHaveBeenCalled();
expect(responseMock.custom).toHaveBeenCalled();
expect(result.body).toEqual('test error');
});
});
233 changes: 233 additions & 0 deletions server/routes/integrations/integrations_router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { schema } from '@osd/config-schema';
import * as mime from 'mime';
import { IRouter, RequestHandlerContext } from '../../../../../src/core/server';
import { INTEGRATIONS_BASE } from '../../../common/constants/shared';
import { IntegrationsAdaptor } from '../../adaptors/integrations/integrations_adaptor';
import {
OpenSearchDashboardsRequest,
OpenSearchDashboardsResponseFactory,
} from '../../../../../src/core/server/http/router';

/**
* Handle an `OpenSearchDashboardsRequest` using the provided `callback` function.
* This is a convenience method that handles common error handling and response formatting.
* The callback must accept a `IntegrationsAdaptor` as its first argument.
*
* If the callback throws an error,
* the `OpenSearchDashboardsResponse` will be formatted using the error's `statusCode` and `message` properties.
* Otherwise, the callback's return value will be formatted in a JSON object under the `data` field.
*
* @param {IntegrationsAdaptor} adaptor The adaptor instance to use for making requests.
* @param {OpenSearchDashboardsResponseFactory} response The factory to use for creating responses.
* @callback callback A callback that will invoke a request on a provided adaptor.
* @returns {Promise<OpenSearchDashboardsResponse>} An `OpenSearchDashboardsResponse` with the return data from the callback.
*/
export const handleWithCallback = async (
adaptor: IntegrationsAdaptor,
response: OpenSearchDashboardsResponseFactory,
callback: (a: IntegrationsAdaptor) => any
): Promise<any> => {
try {
const data = await callback(adaptor);
return response.ok({
body: {
data,
},
});
} catch (err: any) {
console.error(`handleWithCallback: callback failed with error "${err.message}"`);
return response.custom({
statusCode: err.statusCode || 500,
body: err.message,
});
}
};

const getAdaptor = (
context: RequestHandlerContext,
_request: OpenSearchDashboardsRequest
): IntegrationsAdaptor => {
// Stub
return {} as IntegrationsAdaptor;
};

export function registerIntegrationsRoute(router: IRouter) {
router.get(
{
path: `${INTEGRATIONS_BASE}/repository`,
validate: false,
},
async (context, request, response): Promise<any> => {
const adaptor = getAdaptor(context, request);
return handleWithCallback(adaptor, response, async (a: IntegrationsAdaptor) =>
a.getIntegrationTemplates()
);
}
);

router.post(
{
path: `${INTEGRATIONS_BASE}/store/{templateName}`,
validate: {
params: schema.object({
templateName: schema.string(),
}),
body: schema.object({
name: schema.string(),
dataSource: schema.string(),
}),
},
},
async (context, request, response): Promise<any> => {
const adaptor = getAdaptor(context, request);
return handleWithCallback(adaptor, response, async (a: IntegrationsAdaptor) => {
return a.loadIntegrationInstance(
request.params.templateName,
request.body.name,
request.body.dataSource
);
});
}
);

router.get(
{
path: `${INTEGRATIONS_BASE}/repository/{name}`,
validate: {
params: schema.object({
name: schema.string(),
}),
},
},
async (context, request, response): Promise<any> => {
const adaptor = getAdaptor(context, request);
return handleWithCallback(
adaptor,
response,
async (a: IntegrationsAdaptor) =>
(
await a.getIntegrationTemplates({
name: request.params.name,
})
).hits[0]
);
}
);

router.get(
{
path: `${INTEGRATIONS_BASE}/repository/{id}/static/{path}`,
validate: {
params: schema.object({
id: schema.string(),
path: schema.string(),
}),
},
},
async (context, request, response): Promise<any> => {
const adaptor = getAdaptor(context, request);
try {
const result = await adaptor.getStatic(request.params.id, request.params.path);
return response.ok({
headers: {
'Content-Type': mime.getType(request.params.path),
},
body: result,
});
} catch (err: any) {
return response.custom({
statusCode: err.statusCode ? err.statusCode : 500,
body: err.message,
});
}
}
);

router.get(
{
path: `${INTEGRATIONS_BASE}/repository/{id}/schema`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response): Promise<any> => {
const adaptor = getAdaptor(context, request);
return handleWithCallback(adaptor, response, async (a: IntegrationsAdaptor) =>
a.getSchemas(request.params.id)
);
}
);

router.get(
{
path: `${INTEGRATIONS_BASE}/repository/{id}/assets`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response): Promise<any> => {
const adaptor = getAdaptor(context, request);
return handleWithCallback(adaptor, response, async (a: IntegrationsAdaptor) =>
a.getAssets(request.params.id)
);
}
);

router.get(
{
path: `${INTEGRATIONS_BASE}/store`,
validate: false,
},
async (context, request, response): Promise<any> => {
const adaptor = getAdaptor(context, request);
return handleWithCallback(adaptor, response, async (a: IntegrationsAdaptor) =>
a.getIntegrationInstances({})
);
}
);

router.delete(
{
path: `${INTEGRATIONS_BASE}/store/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response): Promise<any> => {
const adaptor = getAdaptor(context, request);
return handleWithCallback(adaptor, response, async (a: IntegrationsAdaptor) =>
a.deleteIntegrationInstance(request.params.id)
);
}
);

router.get(
{
path: `${INTEGRATIONS_BASE}/store/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response): Promise<any> => {
const adaptor = getAdaptor(context, request);
return handleWithCallback(adaptor, response, async (a: IntegrationsAdaptor) =>
a.getIntegrationInstance({
id: request.params.id,
})
);
}
);
}
349 changes: 341 additions & 8 deletions yarn.lock

Large diffs are not rendered by default.

0 comments on commit 00bc374

Please sign in to comment.