Skip to content

Commit

Permalink
Merge pull request #2312 from headlamp-k8s/apiproxy-refactor
Browse files Browse the repository at this point in the history
frontend: Refactor apiProxy
  • Loading branch information
illume authored Sep 10, 2024
2 parents 2266251 + b7d329c commit a40c319
Show file tree
Hide file tree
Showing 21 changed files with 2,241 additions and 2,009 deletions.
9 changes: 1 addition & 8 deletions frontend/src/components/common/Resource/PortForward.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
startPortForward,
stopOrDeletePortForward,
} from '../../../lib/k8s/apiProxy';
import { PortForward as PortForwardState } from '../../../lib/k8s/apiProxy/portForward';
import { KubeContainer, KubeObject } from '../../../lib/k8s/cluster';
import Pod from '../../../lib/k8s/pod';
import Service from '../../../lib/k8s/service';
Expand All @@ -22,14 +23,6 @@ interface PortForwardProps {
resource?: KubeObject;
}

export interface PortForwardState {
id: string;
namespace: string;
cluster: string;
port: string;
status: string;
}

export const PORT_FORWARDS_STORAGE_KEY = 'portforwards';
export const PORT_FORWARD_STOP_STATUS = 'Stopped';
export const PORT_FORWARD_RUNNING_STATUS = 'Running';
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/cronjob/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { getLastScheduleTime, getSchedule } from './List';

function SpawnJobDialog(props: {
cronJob: CronJob;
applyFunc: (newItem: KubeObjectInterface) => Promise<JSON>;
applyFunc: (newItem: KubeObjectInterface) => Promise<KubeObjectInterface>;
openJobDialog: boolean;
setOpenJobDialog: (open: boolean) => void;
}) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/k8s/apiProxy/apiProxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import WS from 'vitest-websocket-mock';
import exportFunctions from '../../../helpers';
import * as auth from '../../auth';
import * as cluster from '../../cluster';
import * as apiProxy from '.';
import * as apiProxy from '../apiProxy';

const baseApiUrl = exportFunctions.getAppUrl();
const wsUrl = baseApiUrl.replace('http', 'ws');
Expand Down
63 changes: 63 additions & 0 deletions frontend/src/lib/k8s/apiProxy/apply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import _ from 'lodash';
import { getCluster } from '../../cluster';
import { KubeObjectInterface } from '../cluster';
import { getClusterDefaultNamespace } from './clusterApi';
import { ApiError } from './clusterRequests';
import { resourceDefToApiFactory } from './factories';

/**
* Applies the provided body to the Kubernetes API.
*
* Tries to POST, and if there's a conflict it does a PUT to the api endpoint.
*
* @param body - The kubernetes object body to apply.
* @param clusterName - The cluster to apply the body to. By default uses the current cluster (URL defined).
*
* @returns The response from the kubernetes API server.
*/
export async function apply<T extends KubeObjectInterface>(
body: T,
clusterName?: string
): Promise<T> {
const bodyToApply = _.cloneDeep(body);

let apiEndpoint;
try {
apiEndpoint = await resourceDefToApiFactory(bodyToApply, clusterName);
} catch (err) {
console.error(`Error getting api endpoint when applying the resource ${bodyToApply}: ${err}`);
throw err;
}

const cluster = clusterName || getCluster();

// Check if the default namespace is needed. And we need to do this before
// getting the apiEndpoint because it will affect the endpoint itself.
const isNamespaced = apiEndpoint.isNamespaced;
const { namespace } = body.metadata;
if (!namespace && isNamespaced) {
let defaultNamespace = 'default';

if (!!cluster) {
defaultNamespace = getClusterDefaultNamespace(cluster) || 'default';
}

bodyToApply.metadata.namespace = defaultNamespace;
}

const resourceVersion = bodyToApply.metadata.resourceVersion;

try {
delete bodyToApply.metadata.resourceVersion;
return await apiEndpoint.post(bodyToApply, {}, cluster!);
} catch (err) {
// Check to see if failed because the record already exists.
// If the failure isn't a 409 (i.e. Confilct), just rethrow.
if ((err as ApiError).status !== 409) throw err;

// Preserve the resourceVersion if its an update request
bodyToApply.metadata.resourceVersion = resourceVersion;
// We had a conflict. Try a PUT
return apiEndpoint.put(bodyToApply, {}, cluster!) as Promise<T>;
}
}
177 changes: 177 additions & 0 deletions frontend/src/lib/k8s/apiProxy/clusterApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import helpers, { getHeadlampAPIHeaders } from '../../../helpers';
import { ConfigState } from '../../../redux/configSlice';
import store from '../../../redux/stores/store';
import {
deleteClusterKubeconfig,
findKubeconfigByClusterName,
storeStatelessClusterKubeconfig,
} from '../../../stateless';
import { getCluster } from '../../util';
import { ClusterRequest, clusterRequest, post, request } from './clusterRequests';
import { JSON_HEADERS } from './constants';

/**
* Test authentication for the given cluster.
* Will throw an error if the user is not authenticated.
*/
export async function testAuth(cluster = '', namespace = 'default') {
const spec = { namespace };
const clusterName = cluster || getCluster();

return post('/apis/authorization.k8s.io/v1/selfsubjectrulesreviews', { spec }, false, {
timeout: 5 * 1000,
cluster: clusterName,
});
}

/**
* Checks cluster health
* Will throw an error if the cluster is not healthy.
*/
export async function testClusterHealth(cluster?: string) {
const clusterName = cluster || getCluster() || '';
return clusterRequest('/healthz', { isJSON: false, cluster: clusterName });
}

export async function setCluster(clusterReq: ClusterRequest) {
const kubeconfig = clusterReq.kubeconfig;

if (kubeconfig) {
await storeStatelessClusterKubeconfig(kubeconfig);
// We just send parsed kubeconfig from the backend to the frontend.
return request(
'/parseKubeConfig',
{
method: 'POST',
body: JSON.stringify(clusterReq),
headers: {
...JSON_HEADERS,
},
},
false,
false
);
}

return request(
'/cluster',
{
method: 'POST',
body: JSON.stringify(clusterReq),
headers: {
...JSON_HEADERS,
...getHeadlampAPIHeaders(),
},
},
false,
false
);
}

// @todo: needs documenting.

export async function deleteCluster(
cluster: string
): Promise<{ clusters: ConfigState['clusters'] }> {
if (cluster) {
const kubeconfig = await findKubeconfigByClusterName(cluster);
if (kubeconfig !== null) {
await deleteClusterKubeconfig(cluster);
window.location.reload();
return { clusters: {} };
}
}

return request(
`/cluster/${cluster}`,
{ method: 'DELETE', headers: { ...getHeadlampAPIHeaders() } },
false,
false
);
}

/**
* getClusterDefaultNamespace gives the default namespace for the given cluster.
*
* If the checkSettings parameter is true (default), it will check the cluster settings first.
* Otherwise it will just check the cluster config. This means that if one needs the default
* namespace that may come from the kubeconfig, call this function with the checkSettings parameter as false.
*
* @param cluster The cluster name.
* @param checkSettings Whether to check the settings for the default namespace (otherwise it just checks the cluster config). Defaults to true.
*
* @returns The default namespace for the given cluster.
*/
export function getClusterDefaultNamespace(cluster: string, checkSettings?: boolean): string {
const includeSettings = checkSettings ?? true;
let defaultNamespace = '';

if (!!cluster) {
if (includeSettings) {
const clusterSettings = helpers.loadClusterSettings(cluster);
defaultNamespace = clusterSettings?.defaultNamespace || '';
}

if (!defaultNamespace) {
const state = store.getState();
const clusterDefaultNs: string =
state.config?.clusters?.[cluster]?.meta_data?.namespace || '';
defaultNamespace = clusterDefaultNs;
}
}

return defaultNamespace;
}

/**
* renameCluster sends call to backend to update a field in kubeconfig which
* is the custom name of the cluster used by the user.
* @param cluster
*/
export async function renameCluster(cluster: string, newClusterName: string, source: string) {
let stateless = false;
if (cluster) {
const kubeconfig = await findKubeconfigByClusterName(cluster);
if (kubeconfig !== null) {
stateless = true;
}
}

return request(
`/cluster/${cluster}`,
{
method: 'PUT',
headers: { ...getHeadlampAPIHeaders() },
body: JSON.stringify({ newClusterName, source, stateless }),
},
false,
false
);
}

/**
* parseKubeConfig sends call to backend to parse kubeconfig and send back
* the parsed clusters and contexts.
* @param clusterReq - The cluster request object.
*/
export async function parseKubeConfig(clusterReq: ClusterRequest) {
const kubeconfig = clusterReq.kubeconfig;

if (kubeconfig) {
return request(
'/parseKubeConfig',
{
method: 'POST',
body: JSON.stringify(clusterReq),
headers: {
...JSON_HEADERS,
...getHeadlampAPIHeaders(),
},
},
false,
false
);
}

return null;
}
Loading

0 comments on commit a40c319

Please sign in to comment.