Skip to content

Commit

Permalink
微修正
Browse files Browse the repository at this point in the history
  • Loading branch information
kahirokunn committed Nov 17, 2023
1 parent 8c3f226 commit 65ac8ec
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 128 deletions.
269 changes: 147 additions & 122 deletions examples/generate-all-k8s-client/k8s-client/client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import fetch, { FetchError } from 'node-fetch'
import * as https from 'node:https'
import * as k8s from '@kubernetes/client-node'

function removeNullableProperties<T extends Record<string, unknown>>(
obj: T
): T {
Object.keys(obj).forEach(
(key) => (obj[key] === undefined || obj[key] === null) && delete obj[key]
)
return obj
import * as k8s from "@kubernetes/client-node";
import NodeCache from "node-cache";
import fetch, { FetchError } from "node-fetch";
import fs from "node:fs";
import * as https from "node:https";

const fileCache = new NodeCache({ stdTTL: 3600, checkperiod: 120 });

async function readFile(filePath: string) {
if (!fileCache.has(filePath)) {
fileCache.set(filePath, await fs.promises.readFile(filePath, "utf8"));
}

return fileCache.get<string>(filePath);
}

type RemoveUndefined<T> = {
[K in keyof T]: Exclude<T[K], undefined | null>;
};

export function removeNullableProperties<
T extends Record<string, unknown | undefined> | undefined,
>(object: T): RemoveUndefined<T> {
if (!object) return object as RemoveUndefined<T>;
for (const key of Object.keys(object))
(object[key] === undefined || object[key] === null) && delete object[key];
return object as RemoveUndefined<T>;
}

/**
Expand All @@ -25,123 +41,129 @@ function removeNullableProperties<T extends Record<string, unknown>>(
* @param maxRetries - Maximum number of retries
*/
async function defaultBackoff(attempt: number, maxRetries: number) {
const attempts = Math.min(attempt, maxRetries)
const attempts = Math.min(attempt, maxRetries);

const timeout = ~~((Math.random() + 0.4) * (300 << attempts))
const timeout = Math.trunc((Math.random() + 0.4) * (300 << attempts));
await new Promise((resolve) =>
setTimeout((res: any) => resolve(res), timeout)
)
setTimeout((response: any) => resolve(response), timeout),
);
}

const isPlainObject = (value: any) => value?.constructor === Object
const isPlainObject = (value: any) => value?.constructor === Object;

type QueryArgsSpec = {
path: string
method?: 'GET' | 'DELETE' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS' | 'HEAD'
body?: any
contentType?: string
params?: any
}
type QueryArgumentsSpec = {
path: string | undefined;
method?:
| "GET"
| "DELETE"
| "PATCH"
| "POST"
| "PUT"
| "OPTIONS"
| "HEAD"
| undefined;
body?: any | undefined;
contentType?: string | undefined;
params?: any | undefined;
};

type MaybePromise<T> = T | Promise<T>
type MaybePromise<T> = T | Promise<T>;

type InterceptorArgs = {
args: QueryArgsSpec
opts: https.RequestOptions
}
type InterceptorArguments = {
args: QueryArgumentsSpec;
opts: https.RequestOptions;
};
type Interceptor = (
args: InterceptorArgs,
options: Options
) => MaybePromise<https.RequestOptions>
arguments_: InterceptorArguments,
options: Options,
) => MaybePromise<https.RequestOptions>;

const interceptors: Interceptor[] = [
async function injectKubernetesParameters({ opts }) {
const kc = new k8s.KubeConfig()
kc.loadFromDefault()
const nextOpts: https.RequestOptions = { ...opts }
// @kubernetes/[email protected]
// await kc.applytoHTTPSOptions(nextOpts)
// @kubernetes/[email protected]
await kc.applyToHTTPSOptions(nextOpts)
const kc = new k8s.KubeConfig();
kc.loadFromDefault();
const nextOptions: https.RequestOptions = { ...opts };
await kc.applyToHTTPSOptions(nextOptions)

const cluster = kc.getCurrentCluster()

if (cluster?.server) {
const url = new URL(cluster.server)
nextOpts.host = url.hostname
nextOpts.protocol = url.protocol
nextOpts.port = url.port
const url = new URL(cluster.server);
nextOptions.host = url.hostname;
nextOptions.protocol = url.protocol;
nextOptions.port = url.port;
}
return nextOpts
return nextOptions;
},
]
];

type RetryConditionFunction = (extraArgs: {
res?: Response
error: unknown
args: QueryArgsSpec
attempt: number
options: RetryOptions
}) => boolean | Promise<boolean>
type RetryConditionFunction = (extraArguments: {
res?: Response;
error: unknown;
args: QueryArgumentsSpec;
attempt: number;
options: RetryOptions;
}) => boolean | Promise<boolean>;

type RetryOptions = {
retryCondition?: RetryConditionFunction | undefined
maxRetries?: number | undefined
}
retryCondition?: RetryConditionFunction;
maxRetries?: number;
};

type HttpHeaderOptions = {
headers?: Record<string, string> | undefined
}
headers?: Record<string, string> | undefined;
};

export type Options = RetryOptions & HttpHeaderOptions
export type Options = RetryOptions & HttpHeaderOptions;

export async function apiClient<Response>(
args: QueryArgsSpec,
extraOptions?: Options
arguments_: QueryArgumentsSpec,
extraOptions?: Options,
): Promise<Response> {
const maxRetries = extraOptions?.maxRetries ?? 3
const maxRetries = extraOptions?.maxRetries ?? 3;

const defaultRetryCondition: RetryConditionFunction = ({ ...obj }) => {
const { res, attempt, error } = obj
const defaultRetryCondition: RetryConditionFunction = ({ ...object }) => {
const { res, attempt, error } = object;
if (attempt > maxRetries) {
return false
return false;
}

if (error instanceof FetchError) {
return true
return true;
}
if (res && res.status >= 500) {
return true
return true;
}
return false
}
return false;
};

const options = {
maxRetries,
backoff: defaultBackoff,
retryCondition: defaultRetryCondition,
...extraOptions,
}
...removeNullableProperties(extraOptions),
};

let { path, method, params, body, contentType } = { ...args }
let { path, method, params, body, contentType } = { ...arguments_ };

let httpsOptions: https.RequestOptions = {
path,
headers: {
...options.headers,
},
}
};
if (method) {
httpsOptions.method = method
httpsOptions.method = method;
}

for (const interceptor of interceptors) {
httpsOptions = await interceptor(
{
args,
args: arguments_,
opts: httpsOptions,
},
options
)
options,
);
}

if (
Expand All @@ -154,88 +176,91 @@ export async function apiClient<Response>(
cert: httpsOptions.cert,
key: httpsOptions.key,
port: httpsOptions.port ? Number(httpsOptions.port) : undefined,
})
)
httpsOptions.agent = agent
}),
);
httpsOptions.agent = agent;
}

if (!httpsOptions.protocol) {
httpsOptions.protocol = 'http:'
httpsOptions.protocol = "http:";
}
const host = httpsOptions.host || httpsOptions.hostname
let baseUrl = `${httpsOptions.protocol}//${host}`
const searchParams = toSearchParams(params)
if (searchParams.size) {
baseUrl += (baseUrl.includes('?') ? '&' : '?') + toSearchParams(params)
const host = httpsOptions.host || httpsOptions.hostname;
let baseUrl = `${httpsOptions.protocol}//${host}`;
const searchParameters = toSearchParameters(params);
if (searchParameters.size > 0) {
baseUrl += (baseUrl.includes("?") ? "&" : "?") + toSearchParameters(params);
}
const url = new URL(baseUrl)
const url = new URL(baseUrl);
if (httpsOptions.port) {
url.port = httpsOptions.port.toString()
url.port = httpsOptions.port.toString();
}
if (httpsOptions.path) {
url.pathname = httpsOptions.path
url.pathname = httpsOptions.path;
}
let isJson = false
let isJson = false;
if (isPlainObject(body) || Array.isArray(body)) {
isJson = true
body = JSON.stringify(body)
isJson = true;
body = JSON.stringify(body);
}
const headers: Record<string, string> = {
...(httpsOptions.headers as any),
}
};
if (contentType) {
headers['Content-Type'] = contentType
} else if (!httpsOptions.headers?.['Content-Type'] && isJson) {
headers['Content-Type'] = 'application/json'
headers["Content-Type"] = contentType;
} else if (!httpsOptions.headers?.["Content-Type"] && isJson) {
headers["Content-Type"] = "application/json";
}

let retry = 0
let retry = 0;
while (true) {
try {
const res = await fetch(url, {
headers,
protocol: httpsOptions.protocol || undefined,
method,
agent: httpsOptions.agent,
body,
})

const isSuccess = res.status >= 200 && res.status < 300
const contentType = res.headers.get('content-type')
const isJsonResponse = contentType?.includes('application/json') ?? false
const response = await fetch(
url,
removeNullableProperties({
headers,
protocol: httpsOptions.protocol || undefined,
method,
agent: httpsOptions.agent,
body,
}),
);

const isSuccess = response.status >= 200 && response.status < 300;
const contentType = response.headers.get("content-type");
const isJsonResponse = contentType?.includes("application/json") ?? false;

if (isSuccess && isJsonResponse) {
return (await res.json()) as Response
return (await response.json()) as Response;
}

// helpful message for debugging
const text = await res.text()
if (res.status === 404 && text.includes('404 page not found')) {
const text = await response.text();
if (response.status === 404 && text.includes("404 page not found")) {
console.info(
`Did you forget to install your Custom Resources Definitions? path: ${httpsOptions.path}`
)
`Did you forget to install your Custom Resources Definitions? path: ${httpsOptions.path}`,
);
}
throw new Error(text)
} catch (e: any) {
retry++
throw new Error(text);
} catch (error: any) {
retry++;

if (
!(await options.retryCondition({
res: e?.value?.res,
error: e,
args,
res: error?.value?.res,
error: error,
args: arguments_,
attempt: retry,
options: options,
}))
) {
throw e
throw error;
}

await options.backoff(retry, options.maxRetries)
await options.backoff(retry, options.maxRetries);
}
}
}

const toSearchParams = (params: Record<string, string>) => {
return new URLSearchParams(removeNullableProperties(params))
}
const toSearchParameters = (parameters: Record<string, string>) => {
return new URLSearchParams(removeNullableProperties(parameters));
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"packages/*"
],
"devDependencies": {
"@types/node-fetch": "^2.6.9",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-flowtype": "^5.7.2",
Expand Down
Loading

0 comments on commit 65ac8ec

Please sign in to comment.