Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(service-discovery): Align service discovery with API service #2459

Merged
merged 5 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/angry-cows-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@equinor/fusion-framework-widget': patch
---

Update defaultScopes in WidgetConfigurator
9 changes: 9 additions & 0 deletions .changeset/funny-pumpkins-train.md
eikeland marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@equinor/fusion-framework-legacy-interopt': minor
---

Updated createServiceResolver to match service discovery module

**BREAKING CHANGES:**

now requires `@equinor/fusion-framework-module-service-discovery^8`
14 changes: 14 additions & 0 deletions .changeset/moody-parents-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@equinor/fusion-framework-cli': minor
---

**@equinor/fusion-framework-cli**

Updated the CLI to use the new service discovery API.

> [!NOTE]
> This is a quick fix until the new major version of the CLI is released.

- Updated the `baseUri` to use a more specific URL path for service discovery.
- Changed from `new URL(import.meta.url).origin` to `String(new URL('/_discovery/environments/current', import.meta.url))`.
- Changed parsing of service discovery response to match new API format.
5 changes: 5 additions & 0 deletions .changeset/nervous-clocks-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@equinor/fusion-framework-module-signalr': patch
---

Update configureFromFramework to handle authentication scopes
65 changes: 65 additions & 0 deletions .changeset/violet-phones-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
'@equinor/fusion-framework-module-service-discovery': major
---

The Service Discovery module has been totally revamped to provide a more flexible and robust solution for service discovery in the Fusion Framework.
The module now relies on the Fusion Service Discovery API to fetch services and their configurations, which allows for more dynamic and real-time service discovery.

The module now follows the "best practices" for configuration and usage, and it is now easier to configure and use the Service Discovery module in your applications. But this also means that the module has breaking changes that may require updates to existing implementations.

> [!NOTE]
> This module can still be configured to resolve custom services, as long as the client implements the `IServiceDiscoveryClient` interface.

**Documentation Updates**

- The README file has been updated to reflect the new configuration options and usage patterns for the Service Discovery module.
- Added sections for simple and advanced configurations, including examples of how to override the default HTTP client key and set a custom service discovery client.

**Code Changes:**

- 🔨 package.json: Added `zod` as a new dependency for schema validation.
- 💫 api-schema.ts: Added schema for the expected response from the `ServiceProviderClient`
- 💫 client.ts: Created `serviceResponseSelector` for parsing and validating client respons.
- 🔨 client.ts: Updated `IServiceDiscoveryClient` interface to include methods for resolving services and fetching services from the API.
- 🔨 client.ts: Updated `ServiceDiscoveryClient` to use the new `serviceResponseSelector`
- 💫 configurator.ts: Introduced new methods for setting and configuring the service discovery client.
- 🔨 configurator.ts: Updated `ServiceDiscoveryConfigurator` to extend the `BaseConfigBuilder`
- 🔨 configurator.ts: Added error handling and validation for required configurations.

**BREAKING CHANGES:**

- The type `Service` has deprecated the `defaultScopes` property in favor of `scopes`.
- The `IServiceDiscoveryClient` interface has been updated, which may require changes in implementations that use this interface.
- The `ServiceDiscoveryConfigurator` now extends `BaseConfigBuilder`, which will affect existing configurations.
- The `ServiceDiscoveryProvider.resolveServices` method now returns `Service[]` (previously `Environment`).

> [!NOTE]
> Only the `ServiceDiscoveryProvider.resolveServices` should affect end-users,
> as it changes the return type of the method.
> The other changes are internal and should not affect existing implementations.

**Consumer Migration Guide:**

`defaultScopes` has been replaced with `scopes` in the `Service` type. Update your code to use the new property.

If you are using the `ServiceDiscoveryProvider.resolveServices` method, update your code to expect an array of `Service` objects instead of an `Environment` object.

```typescript
// Before
const { services } = await serviceDiscoveryProvider.resolveServices('my-service');
// After
const services = await serviceDiscoveryProvider.resolveServices('my-service');
```

> [!WARNING]
> The preious `Environment` object had a `clientId` property, which is now removed, since every service can have its own client id, hence the `scopes` property in the `Service` object.

**Configuration Migration Guide:**

> If you are consuming the `@equinor/fusion-framework` and only configuring the http client, no changes are required.

If you are manually enabling the Service Discovery module, update your configuration to use the new methods provided by `ServiceDiscoveryConfigurator`.
Refer to the updated README for detailed configuration examples and usage patterns.

> [!WARNING]
> The `ServiceDiscoveryConfigurator` now extends `BaseConfigBuilder`, which means that the configuration methods have changed.
2 changes: 1 addition & 1 deletion packages/cli/src/bin/dev-portal/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const configure = async (config: FrameworkConfigurator) => {

config.configureServiceDiscovery({
client: {
baseUri: new URL(import.meta.url).origin,
baseUri: String(new URL('/_discovery/environments/current', import.meta.url)),
defaultScopes: ['5a842df8-3238-415d-b168-9f16a6a6031b/.default'],
},
});
Expand Down
18 changes: 11 additions & 7 deletions packages/cli/src/bin/dev-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,20 @@ export const createDevProxy = (
...proxyOptions,
onProxyRes: responseInterceptor(async (responseBuffer, _proxyRes, req) => {
const response = JSON.parse(responseBuffer.toString('utf8'));
response.environmentName = 'DEVELOPMENT';
response.services = response.services.filter(
(x: { key: string }) => x.key !== 'app',
);
/** refer service [app] to vite middleware */
response.services.push({
const services = response.services
.filter((x: { key: string }) => x.key !== 'app')
.map((service: object) => ({
...service,
scopes: [response.clientId + '/.default'],
environment: 'DEVELOPMENT',
}));
services.push({
key: 'app',
uri: new URL('/', req.headers.referer).href,
scopes: [response.clientId + '/.default'],
});
return JSON.stringify(response);

return JSON.stringify(services);
}),
}),
);
Expand Down
139 changes: 124 additions & 15 deletions packages/modules/service-discovery/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,133 @@
# Fusion Service Provider
# Fusion Service Discovery Module

This module is for resolving service endpoints from a service discovery service.

> [!WARNING]
> This module requires `@equinor/fusion-framework-module-http` to to create http clients, so the module must be enabled in runtime.

## Configure

```ts
This module requires a http client to be configured. The http client should be configured with a key that is used to resolve the service endpoints.

> [!NOTE]
> The Service Discovery Module inherits configuration when used in sub-modules (ex. Application), which means that the http client should be configured in the root module (ex Portal).
>
> Skip this step if the http client is already configured.

```typescript
import { ModulesConfigurator } from '@equinor/fusion-framework-module';
const configurator = new ModulesConfigurator();
configurator.addConfig(
configureHttpClient(
'service_discovery',
{ /* http config */ }
)
);
```

### Simple Configuration

In the simplest form, the service discovery module can be enabled with the following code:

```typescript
import enableServiceDiscovery from '@equinor/fusion-framework-service-discovery';

// the module will setup the service discovery client with default configuration
// Assumes that http client is configured with key 'service_discovery'
enableServiceDiscovery(configurator);
```

#### Override default http client key

If the http client is configured with a different key, the key can be specified as follows:

/** configure the client which should fetch service descriptions */
config.http.configureClient('my_service_discovery', {
baseUri: 'https://foo.bar',
defaultScopes: ['321312bab2-3213123bb-321312aa2/.default'],
```typescript
enableServiceDiscovery(
configurator,
async(builder: ServiceDiscoveryConfigurator) => {
builder.configureServiceDiscoveryClientByClientKey(
// assume that http client is configured with this key
'service_discovery_custom',
// optional endpoint path
'/custom/services'
);
});
```

/** key of configured http client, default `service_discovery` */
config.serviceDiscovery.clientKey = 'my_service_discovery'
### Advanced Configuration

/** endpoint for fetching services */
config.serviceDiscovery.uri = 'api/services';
#### Override default http client

/** parse http response to service discovery environment */
config.serviceDiscovery.selector = async (response: Response): Promise<Environment> => {
const services = await response.json() as Service[];
return services.reduce((acc, service) => Object.assign(acc, {[service.key]: service}), {});
}
If a custom http client is required, the client can be configured as follows:

```typescript
enableServiceDiscovery(
configurator,
async(builder: ServiceDiscoveryConfigurator) => {
builder.configureServiceDiscoveryClient(
// configurator callback
async (args: ConfigBuilderCallbackArgs) => {
// using build environment to create a http client
const httpProvider = await requireInstance('http');
const httpClient = httpProvider.createClient('some_key');
return {
httpClient: httpProvider.createClient('some_key'),
endpoint: '/custom/services'
};
}
);
});
```

#### Setting a custom service discovery client

If a custom service discovery client is required, the client can be configured as follows:

```typescript
enableServiceDiscovery(
configurator,
async(builder: ServiceDiscoveryConfigurator) => {
builder.setServiceDiscoveryClient(
{
resolveServices() {
return [
{
key: 'service1',
url: 'http://service1.com'
},
{
key: 'service2',
url: 'http://service2.com'
}
]
},
resolveService(key: string): Promise<ServiceEndpoint> {
return this.services.find(s => s.key === key);
}
}
);
});
```

If custom logic for creating the service discovery client is required, the client can be configured as follows:

```typescript
enableServiceDiscovery(
configurator,
async(builder: ServiceDiscoveryConfigurator) => {
builder.setServiceDiscoveryClient(
async(args: ConfigBuilderCallbackArgs) => {
const httpProvider = await requireInstance('http');
const httpClient = httpProvider.createClient('my_key');
return {
resolveServices() {
return httpClient.get('/services');
},
resolveService(key: string): Promise<ServiceEndpoint> {
return httpClient.get(`/services/${key}`);
}
};
}
);
});
```
3 changes: 2 additions & 1 deletion packages/modules/service-discovery/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"@equinor/fusion-framework-module": "workspace:^",
"@equinor/fusion-framework-module-http": "workspace:^",
"@equinor/fusion-query": "workspace:^",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"zod": "^3.23.8"
},
"devDependencies": {
"typescript": "^5.5.4"
Expand Down
42 changes: 42 additions & 0 deletions packages/modules/service-discovery/src/api-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import z from 'zod';

/**
* Represents a service from the service discovery API.
*/
const ApiService = z
.object({
key: z.string().describe('The key used to identify the service'),
uri: z.string().describe('The URI of the service'),
id: z.string().optional().describe('The ID of the service'),
environment: z.string().optional().describe('The environment of the service'),
name: z.string().optional().describe('The name of the service'),
scopes: z
.array(z.string())
.optional()
.default([])
.describe('Endpoint authentication scopes'),
tags: z.array(z.string()).optional().describe('Tags for the service'),
})
.describe('A service from the service discovery API')
.transform((value) => ({
...value,
/** @deprecated use `scopes` */
get defaultScopes() {
console.warn('The `defaultScopes` property is deprecated. Use `scopes` instead.');
return value.scopes ?? [];
},

Check warning on line 27 in packages/modules/service-discovery/src/api-schema.ts

View workflow job for this annotation

GitHub Actions / Check Code

[eslint] reported by reviewdog 🐶 Insert `,` Raw Output: {"ruleId":"prettier/prettier","severity":1,"message":"Insert `,`","line":27,"column":10,"nodeType":null,"messageId":"insert","endLine":27,"endColumn":10,"fix":{"range":[1044,1044],"text":","}}
}));

/**
* Represents a list of services from the service discovery API.
*
* This constant is an array of `ApiService` objects, which are defined
* elsewhere in the codebase. It uses the `z.array` method from the Zod
* library to enforce that the array contains only `ApiService` objects.
*
* @constant
* @description A list of services from the service discovery API.
*/
export const ApiServices = z
.array(ApiService)
.describe('A list of services from the service discovery API');
Loading
Loading