Skip to content
Draft
2 changes: 2 additions & 0 deletions src/batch/data-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ const metadata = {
sync: [
'ref',
'name',
'specVersion',
'title',
'version',
'spec',
Expand All @@ -443,6 +444,7 @@ const metadata = {
},
example: {
ref: 'my-api-spec',
specVersion: 'openapi-3.1.0',
title: 'My API Spec',
description: 'Description of my API Spec',
version: '1.0.0',
Expand Down
1 change: 1 addition & 0 deletions src/controllers/sdx/v1/CatalogController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export class CatalogController extends Controller {
@SuccessResponse('200', 'OK')
@Example<ServiceCatalogEntry[]>([
{
specVersion: 'openapi-3.1.0',
name: 'LAB.MIN.CITZ.SAMPLE-API.v1',
title: 'Sample OAS Service',
version: '1.0.0',
Expand Down
52 changes: 39 additions & 13 deletions src/controllers/sdx/v1/OrgGatewaysController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { GWAService } from '../../../services/gwaapi';
import YAML from 'js-yaml';
import getSubjectToken from '../../../auth/auth-token';
import { Logger } from '../../../logger';
import { publishAPEConfig } from '../../../services/ape/publish-config';

const logger = Logger('OrgGatewaysController');

Expand Down Expand Up @@ -137,6 +138,12 @@ export class OrgGatewaysController extends Controller {

const config = await GetConfigUsingPattern(ctx, body);

if (action === 'preview') {
request.res?.header('Content-Type', 'application/yaml; charset=utf-8');
request.res?.send(YAML.dump(config.documents, { noRefs: true }));
return '';
}

const gwaService = new GWAService(process.env.GWA_API_URL);

const payload: any = {
Expand All @@ -162,23 +169,42 @@ export class OrgGatewaysController extends Controller {

const artifact = YAML.dump(payload, { noRefs: true });

if (action === 'preview') {
request.res?.header('Content-Type', 'application/yaml; charset=utf-8');
request.res?.send(artifact);
return '';
let result;
if (
payload.services.length > 0 ||
payload.keys.length > 0 ||
payload.key_sets.length > 0
) {
// Validate the generated config to ensure it only contains allowed configurations for the organization
result = await gwaService.publishGatewayConfiguration(
action === 'remove' ? 'DELETE' : 'PUT',
getSubjectToken(request),
config._gateway_id,
dryRun,
artifact
);
}

// Validate the generated config to ensure it only contains allowed configurations for the organization
const result = await gwaService.publishGatewayConfiguration(
action === 'remove' ? 'DELETE' : 'PUT',
getSubjectToken(request),
config._gateway_id,
dryRun,
artifact
);
// Handle the processing of these (dryRun not supported atm)
// - Webhook
// - RegoPolicy
// - PolicyDataSource
const apeResult = dryRun
? [{ message: 'Dry run not supported for APE' }]
: await publishAPEConfig(action, config.documents);

request.res?.header('Content-Type', 'application/yaml; charset=utf-8');
request.res?.send(YAML.dump(result, { noRefs: true }));
request.res?.send(
YAML.dump(
[
...(result
? [{ resource: 'GatewayResources', response: result }]
: []),
...apeResult,
],
{ noRefs: true }
)
);
return '';
}
}
2 changes: 2 additions & 0 deletions src/controllers/v3/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export interface Application {
* @tsoaModel
* @example {
* "ref": "my-api-spec",
* "specVersion": "openapi-3.1.0",
* "title": "My API Spec",
* "description": "Description of my API Spec",
* "version": "1.0.0",
Expand All @@ -283,6 +284,7 @@ export interface Application {
export interface OpenAPISpec {
name?: string; // Primary Key
ref?: string;
specVersion?: string;
title?: string;
version?: string;
spec?: string;
Expand Down
5 changes: 5 additions & 0 deletions src/lists/OpenAPISpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ module.exports = {
isUnique: true,
access: { update: false },
},
specVersion: {
type: Text,
isRequired: true,
access: { update: false },
},
name: {
type: Text,
isRequired: true,
Expand Down
22 changes: 22 additions & 0 deletions src/services/ape/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const APEConfig = {
// used by the webhook for sending messages to the RS via SDX
pubsub_dispatch_url: 'http://sdx-edge-share0',

// SDX exchange
pubsub_forward_url: 'http://share0.servers.sdx',
pubsub_dispatch_ip: '142.34.229.4',

// publish destination
events_publisher_url: 'http://pubsub-kafka',

// These are for administration, not for runtime routes/plugins

webhook_admin_url: 'http://pubsub-webhook',

opal_policy_url: 'https://opal-policies-api-gov-bc-ca.dev.api.gov.bc.ca',

opal_pip_catalog_url:
'https://opal-pip-catalog-api-gov-bc-ca.dev.api.gov.bc.ca',

opal_client_url: 'https://opal-client-api-gov-bc-ca.dev.api.gov.bc.ca',
};
42 changes: 42 additions & 0 deletions src/services/ape/events-webhooks-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Manage webhooks
*/
import { checkStatus } from '../checkStatus';
import { Logger } from '../../logger';

const logger = Logger('ape.EventsWebhooksService');

export interface WebhookRequest {
conn_id: string;
topic: string;
webhook_url: string;
}

export interface WebhookResponse {
conn_id: string;
topic: string;
webhook_url: string;
}

export class EventsWebhooksService {
private webhookAdminUrl: string;

constructor(webhookAdminUrl: string) {
this.webhookAdminUrl = webhookAdminUrl;
}

public async upsertWebhook(
webhook: WebhookRequest
): Promise<WebhookResponse> {
const url = `${this.webhookAdminUrl}/webhooks`;
return await fetch(url, {
method: 'put',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(webhook),
})
.then(checkStatus)
.then((res) => res.json());
}
}
47 changes: 47 additions & 0 deletions src/services/ape/opal-pip-catalog-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Manage OPAL data sources
*/
import { checkStatus } from '../checkStatus';
import { Logger } from '../../logger';

const logger = Logger('ape.OpalPIPCatalogService');

export interface DataSourceRequest {
name: string;
url: string;
topics: string[];
dst_path: string;
}

export interface CatalogEntry {
id: string;
name: string;
url: string;
topics: string[];
dst_path: string;
}

export class OpalPIPCatalogService {
private OpalPIPCatalogUrl: string;

constructor(OpalPIPCatalogUrl: string) {
this.OpalPIPCatalogUrl = OpalPIPCatalogUrl;
}

public async upsertDataSource(
dataSource: DataSourceRequest
): Promise<CatalogEntry> {
const url = `${this.OpalPIPCatalogUrl}/entries`;
logger.debug(`Upserting data source at ${url}`);

return await fetch(url, {
method: 'put',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(dataSource),
})
.then(checkStatus)
.then((res) => res.json());
}
}
40 changes: 40 additions & 0 deletions src/services/ape/opal-policies-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Manage OPAL policies
*/
import { checkStatus } from '../checkStatus';
import { Logger } from '../../logger';

const logger = Logger('ape.OpalPoliciesService');

export interface PolicyRequest {
package: string;
policy: string;
}

export interface PolicyResponse {
package: string;
policy: string;
}

export class OpalPoliciesService {
private opalPoliciesUrl: string;

constructor(opalPoliciesUrl: string) {
this.opalPoliciesUrl = opalPoliciesUrl;
}

public async upsertPolicy(policy: PolicyRequest): Promise<PolicyResponse> {
const url = `${this.opalPoliciesUrl}/policies/${policy.package}`;
logger.debug(`Upserting policy at ${url}`);

return await fetch(url, {
method: 'put',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(policy),
})
.then(checkStatus)
.then((res) => res.json());
}
}
92 changes: 92 additions & 0 deletions src/services/ape/publish-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* This module is responsible for generating the configuration for the publish component of the APE pattern.
* It will generate the necessary configuration for the publish component based on the inputs provided to the pattern.
* This includes generating the necessary policies, routes, and plugins for the publish component.
* It will also handle the generation of the necessary configuration for the events component of the APE pattern.
* This includes generating the necessary policies, routes, and plugins for the events component.
*
* The publish component is responsible for receiving events from the gateway and forwarding them to the APE events component.
* The events component is responsible for receiving events from the publish component and forwarding them to the appropriate destination (e.g. Event Grid, Service Bus, etc.).
*
*/

import { APEConfig } from './config';
import { EventsWebhooksService } from './events-webhooks-service';
import { OpalPIPCatalogService } from './opal-pip-catalog-service';
import { OpalPoliciesService } from './opal-policies-service';

export async function publishAPEConfig(
action: 'preview' | 'apply' | 'remove',
documents: any[]
) {
if (action === 'apply') {
return await applyAPEConfig(documents);
} else if (action === 'preview') {
return { message: 'Preview not implemented yet' };
} else if (action === 'remove') {
return { message: 'Remove not implemented yet' };
} else {
throw new Error(`Unsupported action: ${action}`);
}
}

async function applyAPEConfig(documents: any[]): Promise<any> {
const results: any[] = [];
const tasks = documents
.filter((doc) => doc.kind)
.map(async (doc) => {
switch (doc.kind) {
case 'Webhook':
// handle publish config for Webhook
const webhookService = new EventsWebhooksService(
APEConfig.webhook_admin_url
);
results.push({
resource: 'Webhook',
result: 'success',
response: await webhookService.upsertWebhook({
conn_id: doc.conn_id,
topic: doc.topic,
webhook_url: doc.url,
}),
});
break;

case 'RegoPolicy':
// handle publish config for RegoPolicy
const policyService = new OpalPoliciesService(
APEConfig.opal_policy_url
);
results.push({
resource: 'RegoPolicy',
result: 'success',
response: await policyService.upsertPolicy({
package: doc.package,
policy: doc.policy,
}),
});
break;

case 'PolicyDataSource':
const dataSourceService = new OpalPIPCatalogService(
APEConfig.opal_pip_catalog_url
);
results.push({
resource: 'PolicyDataSource',
result: 'success',
response: await dataSourceService.upsertDataSource({
name: doc.name,
url: doc.url,
topics: doc.topics,
dst_path: doc.dst_path,
}),
});
break;

default:
throw new Error(`Unsupported document kind: ${doc.kind}`);
}
});
await Promise.all(tasks);
return results;
}
2 changes: 2 additions & 0 deletions src/services/batch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export interface Application {
* @tsoaModel
* @example {
* "ref": "my-api-spec",
* "specVersion": "openapi-3.1.0",
* "title": "My API Spec",
* "description": "Description of my API Spec",
* "version": "1.0.0",
Expand All @@ -283,6 +284,7 @@ export interface Application {
export interface OpenAPISpec {
name?: string; // Primary Key
ref?: string;
specVersion?: string;
title?: string;
version?: string;
spec?: string;
Expand Down
Loading
Loading