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

Pull code from unmaintained prisma-yml package into prisma-loader package. #1841

Merged
merged 1 commit into from Jul 29, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 23 additions & 4 deletions packages/loaders/prisma/package.json
Expand Up @@ -16,14 +16,33 @@
"graphql": "^14.0.0 || ^15.0.0"
},
"dependencies": {
"@graphql-tools/utils": "6.0.15",
"@graphql-tools/url-loader": "6.0.15",
"@graphql-tools/utils": "6.0.15",
"@types/http-proxy-agent": "^2.0.2",
"@types/js-yaml": "^3.12.5",
"@types/json-stable-stringify": "^1.0.32",
"@types/jsonwebtoken": "^8.5.0",
"ajv": "^6.12.3",
"bluebird": "^3.7.2",
"chalk": "^4.1.0",
"debug": "^4.1.1",
"dotenv": "^8.2.0",
"fs-extra": "9.0.1",
"prisma-yml": "1.34.10",
"tslib": "~2.0.0"
"graphql-request": "^2.0.0",
"http-proxy-agent": "^4.0.1",
"https-proxy-agent": "^5.0.0",
"isomorphic-fetch": "^2.2.1",
"js-yaml": "^3.14.0",
"json-stable-stringify": "^1.0.1",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.19",
"replaceall": "^0.1.6",
"scuid": "^1.1.0",
"tslib": "~2.0.0",
"yaml-ast-parser": "^0.0.43"
},
"publishConfig": {
"access": "public",
"directory": "dist"
}
}
}
2 changes: 1 addition & 1 deletion packages/loaders/prisma/src/index.ts
@@ -1,5 +1,5 @@
import { UrlLoader, LoadFromUrlOptions } from '@graphql-tools/url-loader';
import { PrismaDefinitionClass, Environment } from 'prisma-yml';
import { PrismaDefinitionClass, Environment } from './prisma-yml';
import { join } from 'path';
import { pathExists } from 'fs-extra';
import { homedir } from 'os';
Expand Down
31 changes: 31 additions & 0 deletions packages/loaders/prisma/src/prisma-yml/Cluster.test.ts
@@ -0,0 +1,31 @@
import { Cluster, Output } from '.';

describe('cluster endpoint generation', () => {
test('local cluster', () => {
const cluster = new Cluster(new Output(), 'local', 'http://localhost:4466', undefined, true);
expect(cluster.getApiEndpoint('default', 'default')).toMatchSnapshot();
expect(cluster.getApiEndpoint('dev', 'default')).toMatchSnapshot();
expect(cluster.getApiEndpoint('default', 'dev')).toMatchSnapshot();
expect(cluster.getApiEndpoint('default', 'dev', 'ignore-me')).toMatchSnapshot();
});
test('private cluster', () => {
const cluster = new Cluster(
new Output(),
'test01',
'https://test01_workspace.prisma.sh',
undefined,
false,
false,
true
);
expect(cluster.getApiEndpoint('default', 'default', 'workspace')).toMatchSnapshot();
expect(cluster.getApiEndpoint('dev', 'default', 'workspace')).toMatchSnapshot();
expect(cluster.getApiEndpoint('default', 'dev', 'workspace')).toMatchSnapshot();
});
test('sandbox cluster', () => {
const cluster = new Cluster(new Output(), 'prisma-eu1', 'https://eu1.prisma.sh', undefined, false, true, false);
expect(cluster.getApiEndpoint('default', 'default', 'workspace')).toMatchSnapshot();
expect(cluster.getApiEndpoint('dev', 'default', 'workspace')).toMatchSnapshot();
expect(cluster.getApiEndpoint('default', 'dev', 'workspace')).toMatchSnapshot();
});
});
282 changes: 282 additions & 0 deletions packages/loaders/prisma/src/prisma-yml/Cluster.ts
@@ -0,0 +1,282 @@
import 'isomorphic-fetch';
import * as jwt from 'jsonwebtoken';
import { cloudApiEndpoint } from './constants';
import { GraphQLClient } from 'graphql-request';
import chalk from 'chalk';
import { IOutput } from './Output';
import { getProxyAgent } from './utils/getProxyAgent';
const debug = require('debug')('environment');

export class Cluster {
name: string;
baseUrl: string;
local: boolean;
shared: boolean;
clusterSecret?: string;
requiresAuth: boolean;
out: IOutput;
isPrivate: boolean;
workspaceSlug?: string;
private cachedToken?: string;
hasOldDeployEndpoint: boolean;
custom?: boolean;
constructor(
out: IOutput,
name: string,
baseUrl: string,
clusterSecret?: string,
local = true,
shared = false,
isPrivate = false,
workspaceSlug?: string
) {
this.out = out;
this.name = name;

// All `baseUrl` extension points in this class
// adds a trailing slash. Here we remove it from
// the passed `baseUrl` in order to avoid double
// slashes.
this.baseUrl = baseUrl.replace(/\/$/, '');
this.clusterSecret = clusterSecret;
this.local = local;
this.shared = shared;
this.isPrivate = isPrivate;
this.workspaceSlug = workspaceSlug;
this.hasOldDeployEndpoint = false;
}

async getToken(serviceName: string, workspaceSlug?: string, stageName?: string): Promise<string | null> {
// public clusters just take the token

const needsAuth = await this.needsAuth();
debug({ needsAuth });
if (!needsAuth) {
return null;
}

if (this.name === 'shared-public-demo') {
return '';
}
if (this.isPrivate && process.env.PRISMA_MANAGEMENT_API_SECRET) {
return this.getLocalToken();
}
if (this.shared || (this.isPrivate && !process.env.PRISMA_MANAGEMENT_API_SECRET)) {
return this.generateClusterToken(serviceName, workspaceSlug, stageName);
} else {
return this.getLocalToken();
}
}

getLocalToken(): string | null {
if (!this.clusterSecret && !process.env.PRISMA_MANAGEMENT_API_SECRET) {
return null;
}
if (!this.cachedToken) {
const grants = [{ target: `*/*`, action: '*' }];
const secret = process.env.PRISMA_MANAGEMENT_API_SECRET || this.clusterSecret;

try {
const algorithm = process.env.PRISMA_MANAGEMENT_API_SECRET ? 'HS256' : 'RS256';
this.cachedToken = jwt.sign({ grants }, secret, {
expiresIn: '5y',
algorithm,
});
} catch (e) {
throw new Error(
`Could not generate token for cluster ${chalk.bold(
this.getDeployEndpoint()
)}. Did you provide the env var PRISMA_MANAGEMENT_API_SECRET?
Original error: ${e.message}`
);
}
}

return this.cachedToken!;
}

get cloudClient() {
return new GraphQLClient(cloudApiEndpoint, {
headers: {
Authorization: `Bearer ${this.clusterSecret}`,
},
agent: getProxyAgent(cloudApiEndpoint),
} as any);
}

async generateClusterToken(
serviceName: string,
workspaceSlug: string = this.workspaceSlug || '*',
stageName?: string
): Promise<string> {
const query = `
mutation ($input: GenerateClusterTokenRequest!) {
generateClusterToken(input: $input) {
clusterToken
}
}
`;

const {
generateClusterToken: { clusterToken },
} = await this.cloudClient.request<{
generateClusterToken: {
clusterToken: string;
};
}>(query, {
input: {
workspaceSlug,
clusterName: this.name,
serviceName,
stageName,
},
});

return clusterToken;
}

async addServiceToCloudDBIfMissing(
serviceName: string,
workspaceSlug: string = this.workspaceSlug!,
stageName?: string
): Promise<boolean> {
const query = `
mutation ($input: GenerateClusterTokenRequest!) {
addServiceToCloudDBIfMissing(input: $input)
}
`;

const serviceCreated = await this.cloudClient.request<{
addServiceToCloudDBIfMissing: boolean;
}>(query, {
input: {
workspaceSlug,
clusterName: this.name,
serviceName,
stageName,
},
});

return serviceCreated.addServiceToCloudDBIfMissing;
}

getApiEndpoint(service: string, stage: string, workspaceSlug?: string | null) {
if (!this.shared && service === 'default' && stage === 'default') {
return this.baseUrl;
}
if (!this.shared && stage === 'default') {
return `${this.baseUrl}/${service}`;
}
if (this.isPrivate || this.local) {
return `${this.baseUrl}/${service}/${stage}`;
}
const workspaceString = workspaceSlug ? `${workspaceSlug}/` : '';
return `${this.baseUrl}/${workspaceString}${service}/${stage}`;
}

getWSEndpoint(service: string, stage: string, workspaceSlug?: string | null) {
return this.getApiEndpoint(service, stage, workspaceSlug).replace(/^http/, 'ws');
}

getImportEndpoint(service: string, stage: string, workspaceSlug?: string | null) {
return this.getApiEndpoint(service, stage, workspaceSlug) + `/import`;
}

getExportEndpoint(service: string, stage: string, workspaceSlug?: string | null) {
return this.getApiEndpoint(service, stage, workspaceSlug) + `/export`;
}

getDeployEndpoint() {
return `${this.baseUrl}/${this.hasOldDeployEndpoint ? 'cluster' : 'management'}`;
}

async isOnline(): Promise<boolean> {
const version = await this.getVersion();
return typeof version === 'string';
}

async getVersion(): Promise<string | null> {
// first try new api
try {
const result = await this.request(`{
serverInfo {
version
}
}`);

const res = await result.json();
const { data, errors } = res;
if (errors && errors[0].code === 3016 && errors[0].message.includes('management@default')) {
this.hasOldDeployEndpoint = true;
return await this.getVersion();
}
if (data && data.serverInfo) {
return data.serverInfo.version;
}
} catch (e) {
debug(e);
}

// if that doesn't work, try the old one
try {
const result = await this.request(`{
serverInfo {
version
}
}`);

const res = await result.json();
const { data } = res;
return data.serverInfo.version;
} catch (e) {
debug(e);
}

return null;
}

request(query: string, variables?: any) {
return fetch(this.getDeployEndpoint(), {
method: 'post',
headers: {
'Content-Type': 'application/json',
} as any,
body: JSON.stringify({
query,
variables,
}),
agent: getProxyAgent(this.getDeployEndpoint()),
} as any);
}

async needsAuth(): Promise<boolean> {
try {
const result = await this.request(`{
listProjects {
name
}
}`);
const data = await result.json();
if (data.errors && data.errors.length > 0) {
return true;
}
return false;
} catch (e) {
debug('Assuming that the server needs authentication');
debug(e.toString());
return true;
}
}

toJSON() {
return {
name: this.name,
baseUrl: this.baseUrl,
local: this.local,
clusterSecret: this.clusterSecret,
shared: this.shared,
isPrivate: this.isPrivate,
workspaceSlug: this.workspaceSlug,
};
}
}