pipelines/frontend/server/utils.ts

185 lines
5.2 KiB
TypeScript

// Copyright 2018-2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { readFileSync } from 'fs';
import { Transform, TransformOptions } from 'stream';
/** get the server address from host, port, and schema (defaults to 'http'). */
export function getAddress({
host,
port,
namespace,
schema = 'http',
}: {
host: string;
port?: string | number;
namespace?: string;
schema?: string;
}) {
namespace = namespace ? `.${namespace}` : '';
port = port ? `:${port}` : '';
return `${schema}://${host}${namespace}${port}`;
}
export function equalArrays(a1: any[], a2: any[]): boolean {
if (!Array.isArray(a1) || !Array.isArray(a2) || a1.length !== a2.length) {
return false;
}
return JSON.stringify(a1) === JSON.stringify(a2);
}
export function generateRandomString(length: number): string {
let d = new Date().getTime();
function randomChar(): string {
const r = Math.trunc((d + Math.random() * 16) % 16);
d = Math.floor(d / 16);
return r.toString(16);
}
let str = '';
for (let i = 0; i < length; ++i) {
str += randomChar();
}
return str;
}
export function loadJSON<T>(filepath?: string, defaultValue?: T): T | undefined {
if (!filepath) {
return defaultValue;
}
try {
return JSON.parse(readFileSync(filepath, 'utf-8'));
} catch (error) {
console.error(`Failed reading json data from '${filepath}':`);
console.error(error);
return defaultValue;
}
}
export interface PreviewStreamOptions extends TransformOptions {
peek: number;
}
/**
* Transform stream that only stream the first X number of bytes.
*/
export class PreviewStream extends Transform {
constructor({ peek, ...opts }: PreviewStreamOptions) {
// acts like passthrough
let transform: TransformOptions['transform'] = (chunk, _encoding, callback) =>
callback(undefined, chunk);
// implements preview - peek must be positive number
if (peek && peek > 0) {
let size = 0;
transform = (chunk, _encoding, callback) => {
const delta = peek - size;
size += chunk.length;
if (size >= peek) {
callback(undefined, chunk.slice(0, delta));
this.resume(); // do not handle any subsequent data
return;
}
callback(undefined, chunk);
};
}
super({ ...opts, transform });
}
}
export interface ErrorDetails {
message: string;
additionalInfo: any;
}
const UNKOWN_ERROR = 'Unknown error';
export async function parseError(error: any): Promise<ErrorDetails> {
return (
parseK8sError(error) ||
(await parseKfpApiError(error)) ||
parseGenericError(error) || { message: UNKOWN_ERROR, additionalInfo: error }
);
}
function parseGenericError(error: any): ErrorDetails | undefined {
if (!error) {
return undefined;
} else if (typeof error === 'string') {
return {
message: error,
additionalInfo: error,
};
} else if (error instanceof Error) {
return { message: error.message, additionalInfo: error };
} else if (error.message && typeof error.message === 'string') {
return { message: error.message, additionalInfo: error };
} else if (
error.url &&
typeof error.url === 'string' &&
error.status &&
typeof error.status === 'number' &&
error.statusText &&
typeof error.statusText === 'string'
) {
const { url, status, statusText } = error;
return {
message: `Fetching ${url} failed with status code ${status} and message: ${statusText}`,
additionalInfo: { url, status, statusText },
};
}
// Cannot understand error type
return undefined;
}
async function parseKfpApiError(error: any): Promise<ErrorDetails | undefined> {
if (!error || !error.json || typeof error.json !== 'function') {
return undefined;
}
try {
const json = await error.json();
const { error: message, details } = json;
if (message && details && typeof message === 'string' && typeof details === 'object') {
return {
message,
additionalInfo: details,
};
} else {
return undefined;
}
} catch (err) {
return undefined;
}
}
function parseK8sError(error: any): ErrorDetails | undefined {
if (!error || !error.body || typeof error.body !== 'object') {
return undefined;
}
if (typeof error.body.message !== 'string') {
return undefined;
}
// Kubernetes client http error has body with all the info.
// Example error.body
// {
// kind: 'Status',
// apiVersion: 'v1',
// metadata: {},
// status: 'Failure',
// message: 'pods "test-pod" not found',
// reason: 'NotFound',
// details: { name: 'test-pod', kind: 'pods' },
// code: 404
// }
return {
message: error.body.message,
additionalInfo: error.body,
};
}