Skip to content

Commit

Permalink
feat: session class
Browse files Browse the repository at this point in the history
BREAKING CHANGE: changed API of Session
  • Loading branch information
Ivan Zuev committed Dec 24, 2021
1 parent 1e2dcc2 commit 826e6de
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 43 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"unicorn/no-null": "off",
"import/no-extraneous-dependencies": ["error", {
"devDependencies": true
}]
}],
"import/no-cycle": "off"
}
}
110 changes: 109 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"lodash": "^4.17.21",
"log4js": "^6.3.0",
"luxon": "^2.2.0",
"nice-grpc": "^1.0.4",
"nice-grpc-client-middleware-deadline": "^1.0.4",
"node-fetch": "^3.1.0",
"protobufjs": "^6.8.8"
},
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * as serviceClients from './generated/yandex/cloud/service_clients';
export * as cloudApi from './generated/yandex/cloud';
export * from './session';
11 changes: 7 additions & 4 deletions src/service-endpoints.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { ServiceClientConstructor } from '@grpc/grpc-js';
import { getServiceClientEndpoint } from './service-endpoints';
import { serviceClients } from '.';
import { GeneratedServiceClientCtor } from './types';

// eslint-disable-next-line @typescript-eslint/ban-types
type MockServiceClientCtor = GeneratedServiceClientCtor<{}>;

describe('service endpoints', () => {
it('each service in generated service_clients module should have endpoint declared in service-endpoints', () => {
for (const [, ServiceClient] of Object.entries(serviceClients)) {
// eslint-disable-next-line @typescript-eslint/no-loop-func
expect(() => {
const endpoint = getServiceClientEndpoint(ServiceClient);
const endpoint = getServiceClientEndpoint(ServiceClient as MockServiceClientCtor);

expect(endpoint).toBeTruthy();
}).not.toThrow();
Expand All @@ -18,13 +21,13 @@ describe('service endpoints', () => {
const serviceName = 'myCustomService';

expect(() => {
getServiceClientEndpoint({ options: { serviceName } } as unknown as ServiceClientConstructor);
getServiceClientEndpoint({ options: { serviceName } } as unknown as MockServiceClientCtor);
}).toThrow(`Endpoint for service ${serviceName} is no defined`);
});

it('should throw exception if client class has no serviceName option', () => {
expect(() => {
getServiceClientEndpoint({ options: {} } as unknown as ServiceClientConstructor);
getServiceClientEndpoint({ options: {} } as unknown as MockServiceClientCtor);
}).toThrow('Unable to retrieve serviceName of provided service client class');
});
});
4 changes: 2 additions & 2 deletions src/service-endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ServiceClientConstructor } from '@grpc/grpc-js';
import { ServiceClientConstructor, ServiceDefinition } from '@grpc/grpc-js';
import { GeneratedServiceClientCtor } from './types';

interface ServiceEndpoint {
Expand Down Expand Up @@ -305,7 +305,7 @@ const SERVICE_ENDPOINTS_LIST: ServiceEndpointsList = [
},
];

export const getServiceClientEndpoint = (generatedClientCtor: GeneratedServiceClientCtor): string => {
export const getServiceClientEndpoint = <T extends ServiceDefinition>(generatedClientCtor: GeneratedServiceClientCtor<T>): string => {
const clientCtor = generatedClientCtor as unknown as ServiceClientConstructor;
// eslint-disable-next-line prefer-destructuring
const serviceName: string = clientCtor.options.serviceName as string;
Expand Down
102 changes: 102 additions & 0 deletions src/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
ChannelCredentials, credentials, Metadata, ServiceDefinition,
} from '@grpc/grpc-js';
import { createChannel } from 'nice-grpc';
import {
GeneratedServiceClientCtor,
IamTokenCredentialsConfig,
OAuthCredentialsConfig,
ServiceAccountCredentialsConfig,
SessionConfig,
} from './types';
import { IamTokenService } from './token-service/iam-token-service';
import { MetadataTokenService } from './token-service/metadata-token-service';
import { clientFactory } from './utils/client-factory';
import { serviceClients, cloudApi } from '.';
import { getServiceClientEndpoint } from './service-endpoints';

const isOAuth = (config: SessionConfig): config is OAuthCredentialsConfig => 'oauthToken' in config;

const isIamToken = (config: SessionConfig): config is IamTokenCredentialsConfig => 'iamToken' in config;

const isServiceAccount = (config: SessionConfig): config is ServiceAccountCredentialsConfig => 'serviceAccountJson' in config;

const createIamToken = async (iamEndpoint: string, req: Partial<cloudApi.iam.iam_token.CreateIamTokenRequest>) => {
const channel = createChannel(iamEndpoint, credentials.createSsl());
const client = clientFactory.create(serviceClients.IamTokenServiceClient.service, channel);
const resp = await client.create(cloudApi.iam.iam_token.CreateIamTokenRequest.fromPartial(req));

return resp.iamToken;
};

const newTokenCreator = (config: SessionConfig): () => Promise<string> => {
if (isOAuth(config)) {
return () => {
const iamEndpoint = getServiceClientEndpoint(serviceClients.IamTokenServiceClient);

return createIamToken(iamEndpoint, {
yandexPassportOauthToken: config.oauthToken,
});
};
} if (isIamToken(config)) {
const { iamToken } = config;

return async () => iamToken;
}

const tokenService = isServiceAccount(config) ? new IamTokenService(config.serviceAccountJson) : new MetadataTokenService();

return async () => tokenService.getToken();
};

const newChannelCredentials = (tokenCreator: TokenCreator) => credentials.combineChannelCredentials(
credentials.createSsl(),
credentials.createFromMetadataGenerator(
(
params: { service_url: string },
callback: (error: any, result?: any) => void,
) => {
tokenCreator()
.then((token) => {
const md = new Metadata();

md.set('authorization', `Bearer ${token}`);

return callback(null, md);
})
.catch((error) => callback(error));
},
),
);

type TokenCreator = () => Promise<string>;

export class Session {
private readonly config: SessionConfig;
private readonly channelCredentials: ChannelCredentials;
private readonly tokenCreator: TokenCreator;

private static readonly DEFAULT_CONFIG: SessionConfig = {
pollInterval: 1000,
};

constructor(config?: SessionConfig) {
this.config = {
...Session.DEFAULT_CONFIG,
...config,
};
this.tokenCreator = newTokenCreator(this.config);
this.channelCredentials = newChannelCredentials(this.tokenCreator);
}

get pollInterval() {
return this.config.pollInterval;
}

client<S extends ServiceDefinition>(clientClass: GeneratedServiceClientCtor<S>) {
const endpoint = getServiceClientEndpoint(clientClass);
const channel = createChannel(endpoint, this.channelCredentials);

return clientFactory.create(clientClass.service, channel);
}
}
44 changes: 11 additions & 33 deletions src/token-service/iam-token-service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import { credentials, Metadata } from '@grpc/grpc-js';
import * as jwt from 'jsonwebtoken';
import { DateTime } from 'luxon';
import { createChannel } from 'nice-grpc';
import { cloudApi, serviceClients } from '..';
import { getServiceClientEndpoint } from '../service-endpoints';
import { TokenService } from '../types';
import { IIAmCredentials, ISslCredentials, TokenService } from '../types';
import { clientFactory } from '../utils/client-factory';

const { IamTokenServiceClient } = serviceClients;

export interface IIAmCredentials {
serviceAccountId: string;
accessKeyId: string;
privateKey: Buffer | string;
}

export interface ISslCredentials {
rootCertificates?: Buffer;
clientPrivateKey?: Buffer;
clientCertChain?: Buffer;
}

export class IamTokenService implements TokenService {
public readonly sslCredentials?: ISslCredentials;

Expand Down Expand Up @@ -53,11 +43,9 @@ export class IamTokenService implements TokenService {

private client() {
const endpoint = getServiceClientEndpoint(IamTokenServiceClient);
const channel = createChannel(endpoint, credentials.createSsl());

return new serviceClients.IamTokenServiceClient(
endpoint,
credentials.createSsl(),
);
return clientFactory.create(IamTokenServiceClient.service, channel);
}

private getJwtRequest() {
Expand Down Expand Up @@ -91,21 +79,11 @@ export class IamTokenService implements TokenService {
private async requestToken(): Promise<cloudApi.iam.iam_token.CreateIamTokenResponse> {
const deadline = DateTime.now().plus({ millisecond: this.tokenRequestTimeout }).toJSDate();

return new Promise((resolve, reject) => {
this.client().create(
cloudApi.iam.iam_token.CreateIamTokenRequest.fromPartial({
jwt: this.getJwtRequest(),
}),
new Metadata(),
{ deadline },
(err, response) => {
if (err) {
reject(err);
} else {
resolve(response);
}
},
);
});
return this.client().create(
cloudApi.iam.iam_token.CreateIamTokenRequest.fromPartial({
jwt: this.getJwtRequest(),
}),
{ deadline },
);
}
}
Loading

0 comments on commit 826e6de

Please sign in to comment.