use node-fetch for k8s resources

The main advantage this has over using the list*CustomObject methods is that it
won't reject non-OK statuses, so we won't have to be so careful about
catching and rethrowing.

Personally I find it a bit clearer to see the actual requests rather than the
client methods with all those empty arguments, and a side benefit is that this
implementation uses the fetch API -- under the covers, @kubernetes/client-node
is using the deprecated request library.

Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
Jamie Klassen
2022-11-29 13:52:09 -05:00
parent 99ca12978d
commit f54362ad57
@@ -14,7 +14,13 @@
* limitations under the License.
*/
import { topPods } from '@kubernetes/client-node';
import {
Cluster,
KubeConfig,
User,
bufferFromFileOrString,
topPods,
} from '@kubernetes/client-node';
import lodash, { Dictionary } from 'lodash';
import { Logger } from 'winston';
import {
@@ -32,6 +38,9 @@ import {
PodStatusFetchResponse,
} from '@backstage/plugin-kubernetes-common';
import { KubernetesClientProvider } from './KubernetesClientProvider';
import fetch, { Headers, RequestInit } from 'node-fetch';
import * as https from 'https';
import fs from 'fs-extra';
export interface KubernetesClientBasedFetcherOptions {
kubernetesClientProvider: KubernetesClientProvider;
@@ -149,50 +158,82 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
labelSelector: string,
objectType: KubernetesObjectTypes,
namespace?: string,
): Promise<FetchResponse> {
const customObjects =
this.kubernetesClientProvider.getCustomObjectsClient(clusterDetails);
customObjects.addInterceptor((requestOptions: any) => {
requestOptions.uri = requestOptions.uri.replace('/apis//v1/', '/api/v1/');
});
): Promise<FetchResult> {
const { group, apiVersion, plural } = resource;
const encode = (s: string) => encodeURIComponent(s);
let resourcePath = group
? `/apis/${encode(group)}/${encode(apiVersion)}`
: `/api/${encode(apiVersion)}`;
if (namespace) {
return customObjects
.listNamespacedCustomObject(
resource.group,
resource.apiVersion,
namespace,
resource.plural,
'',
false,
'',
'',
labelSelector,
)
.then(r => {
resourcePath += `/namespaces/${encode(namespace)}`;
}
resourcePath += `/${encode(plural)}`;
const headers: Headers = new Headers({
Accept: 'application/json',
'Content-Type': 'application/json',
});
const fetchOptions: RequestInit = {
method: 'GET',
};
let token: Buffer | string;
let url: URL;
if (clusterDetails.serviceAccountToken) {
url = new URL(`${clusterDetails.url}${resourcePath}`);
if (url.protocol === 'https:') {
fetchOptions.agent = new https.Agent({
ca:
bufferFromFileOrString(
clusterDetails.caFile,
clusterDetails.caData,
) ?? undefined,
rejectUnauthorized: !clusterDetails.skipTLSVerify,
});
}
token = clusterDetails.serviceAccountToken;
} else {
const kc = new KubeConfig();
kc.loadFromCluster();
// loadFromCluster never fails (unless an exception is thrown) and is
// guaranteed to populate the cluster/user/context correctly
const cluster = kc.getCurrentCluster() as Cluster;
const user = kc.getCurrentUser() as User;
url = new URL(`${cluster.server}${resourcePath}`);
if (url.protocol === 'https:') {
fetchOptions.agent = new https.Agent({
ca: fs.readFileSync(cluster.caFile as string),
});
}
token = fs.readFileSync(user.authProvider.config.tokenFile);
}
headers.set('Authorization', `Bearer ${token}`);
fetchOptions.headers = headers;
url.search = `labelSelector=${labelSelector}`;
return fetch(url.toString(), fetchOptions).then(r => {
return r.json().then(j => {
if (r.ok) {
return {
type: objectType,
resources: (r.body as any).items,
resources: j.items,
};
});
}
return customObjects
.listClusterCustomObject(
resource.group,
resource.apiVersion,
resource.plural,
'',
false,
'',
'',
labelSelector,
)
.then(r => {
}
this.logger.warn(
`statusCode=${
r.status
} for resource ${resourcePath} body=[${JSON.stringify(j)}]`,
);
return {
type: objectType,
resources: (r.body as any).items,
errorType: statusCodeToErrorType(r.status),
statusCode: r.status,
resourcePath,
};
});
});
}
}