Skip to content

Commit

Permalink
StorageClassKind array of cluster storage classes. get logic in App /…
Browse files Browse the repository at this point in the history
… AppContext and additional property. Storage classes info only gets loaded anew on app starts or hard browser page refresh.

StorageClassKind array of cluster storage classes. get logic in App / AppContext and additional property. Storage classes info only gets loaded anew on app starts or hard browser page refresh.

prettify

console log entry minor change
  • Loading branch information
shalberd committed Sep 11, 2023
1 parent dfb659f commit ae7c2fb
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 4 deletions.
1 change: 1 addition & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './k8s/routes';
export * from './k8s/secrets';
export * from './k8s/serviceAccounts';
export * from './k8s/servingRuntimes';
export * from './k8s/storageClasses';
export * from './k8s/users';
export * from './k8s/groups';
export * from './k8s/templates';
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/k8s/pvcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const assemblePvc = (
projectName: string,
description: string,
pvcSize: number,
storageClassName?: string,
): PersistentVolumeClaimKind => ({
apiVersion: 'v1',
kind: 'PersistentVolumeClaim',
Expand All @@ -36,6 +37,7 @@ export const assemblePvc = (
storage: `${pvcSize}Gi`,
},
},
storageClassName: storageClassName || undefined,
volumeMode: 'Filesystem',
},
status: {
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/api/k8s/storageClasses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { k8sListResource } from '@openshift/dynamic-plugin-sdk-utils';
import { StorageClassKind } from '~/k8sTypes';
import { StorageClassModel } from '~/api/models';
export const getStorageClasses = (): Promise<StorageClassKind[]> =>
k8sListResource<StorageClassKind>({
model: StorageClassModel,
queryOptions: {},
}).then((listResource) => listResource.items);
7 changes: 7 additions & 0 deletions frontend/src/api/models/k8s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export const PVCModel: K8sModelCommon = {
plural: 'persistentvolumeclaims',
};

export const StorageClassModel: K8sModelCommon = {
apiVersion: 'v1',
apiGroup: 'storage.k8s.io',
kind: 'StorageClass',
plural: 'storageclasses',
};

export const NamespaceModel: K8sModelCommon = {
apiVersion: 'v1',
kind: 'Namespace',
Expand Down
30 changes: 28 additions & 2 deletions frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import AppRoutes from './AppRoutes';
import NavSidebar from './NavSidebar';
import AppNotificationDrawer from './AppNotificationDrawer';
import { AppContext } from './AppContext';
import { useApplicationSettings } from './useApplicationSettings';
import { useApplicationSettings, useClusterStorageClasses } from './useApplicationSettings';
import TelemetrySetup from './TelemetrySetup';
import { logout } from './appUtils';

Expand All @@ -40,9 +40,11 @@ const App: React.FC = () => {
loadError: fetchConfigError,
} = useApplicationSettings();

const { clusterStorageClasses: appContextStorageClasses } = useClusterStorageClasses();

useDetectUser();

if (!username || !configLoaded || !dashboardConfig) {
if (!username || !configLoaded || !dashboardConfig || appContextStorageClasses.length === 0) {
// We lack the critical data to startup the app
if (userError || fetchConfigError) {
// There was an error fetching critical data
Expand Down Expand Up @@ -72,6 +74,29 @@ const App: React.FC = () => {
</Page>
);
}
if (appContextStorageClasses.length === 0) {
// There was an error fetching cluster storage classes via openshift/dynamic-plugin-sdk-utils
return (
<Page>
<PageSection>
<Stack hasGutter>
<StackItem>
<Alert variant="danger" isInline title="General loading error">
<p>
There was an error fetching the list of cluster StorageClasses via
openshift/dynamic-plugin-sdk-utils
</p>
<p>
Check with your Openshift cluster admins that at least one StorageClass is set
up in the cluster
</p>
</Alert>
</StackItem>
</Stack>
</PageSection>
</Page>
);
}

// Assume we are still waiting on the API to finish
return (
Expand All @@ -86,6 +111,7 @@ const App: React.FC = () => {
value={{
buildStatuses,
dashboardConfig,
appContextStorageClasses,
}}
>
<Page
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/app/AppContext.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import * as React from 'react';
import { BuildStatus, DashboardConfig } from '~/types';
import { StorageClassKind } from '~/k8sTypes';

type AppContextProps = {
buildStatuses: BuildStatus[];
dashboardConfig: DashboardConfig;
appContextStorageClasses: StorageClassKind[];
};

const defaultAppContext: AppContextProps = {
buildStatuses: [],
// At runtime dashboardConfig is never null -- DO NOT DO THIS usually
dashboardConfig: null as unknown as DashboardConfig,
appContextStorageClasses: [] as StorageClassKind[],
};

export const AppContext = React.createContext(defaultAppContext);
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/app/useApplicationSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react';
import { DashboardConfig } from '~/types';
import { StorageClassKind } from '~/k8sTypes';
import { getStorageClasses } from '~/api';
import { POLL_INTERVAL } from '~/utilities/const';
import { useDeepCompareMemoize } from '~/utilities/useDeepCompareMemoize';
import { fetchDashboardConfig } from '~/services/dashboardConfigService';
Expand Down Expand Up @@ -59,3 +61,27 @@ export const useApplicationSettings = (): {

return { dashboardConfig: retConfig, loaded, loadError };
};

export const useClusterStorageClasses = (): {
clusterStorageClasses: StorageClassKind[];
} => {
const defaultStorageClass: StorageClassKind[] = [];
const [clusterStorageClasses, setClusterStorageClasses] = React.useState(defaultStorageClass);
React.useEffect(() => {
const myClusterStorageClasses = async () => {
const clusterStorageClasses = await getStorageClasses()
.then((res: StorageClassKind[]) => res)
.catch(() => {
const scEmptyArray: StorageClassKind[] = [];
return scEmptyArray;
});
setClusterStorageClasses(clusterStorageClasses);
return myClusterStorageClasses;
};

// fetch data inside useEffect
myClusterStorageClasses();
}, []);

return { clusterStorageClasses };
};
21 changes: 21 additions & 0 deletions frontend/src/k8sTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ type DisplayNameAnnotations = Partial<{
'openshift.io/display-name': string; // the name provided by the user
}>;

type StorageClassAnnotations = Partial<{
// if true, enables any persistent volume claim (PVC) that does not specify a specific storage class to automatically be provisioned.
// Only one, if any, StorageClass per cluster can be set as default.
'storageclass.kubernetes.io/is-default-class': 'true' | 'false';
// the description provided by the cluster admin or Container Storage Interface (CSI) provider
'kubernetes.io/description': string;
}>;

export type K8sDSGResource = K8sResourceCommon & {
metadata: {
annotations?: DisplayNameAnnotations;
Expand Down Expand Up @@ -250,6 +258,18 @@ export type PersistentVolumeClaimKind = K8sResourceCommon & {
} & Record<string, unknown>;
};

export type StorageClassKind = K8sResourceCommon & {
metadata: {
annotations?: StorageClassAnnotations;
name: string;
};
provisioner: string;
parameters?: string;
reclaimPolicy: string;
volumeBindingMode: string;
allowVolumeExpansion?: boolean;
};

export type NotebookKind = K8sResourceCommon & {
metadata: {
annotations: DisplayNameAnnotations & NotebookAnnotations;
Expand Down Expand Up @@ -721,6 +741,7 @@ export type DashboardConfigKind = K8sResourceCommon & {
notebookController?: {
enabled: boolean;
pvcSize?: string;
storageClassName?: string;
notebookNamespace?: string;
gpuSetting?: GpuSettingString;
notebookTolerationSettings?: TolerationSettings;
Expand Down
32 changes: 31 additions & 1 deletion frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,38 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
canEnablePipelines,
}) => {
const [errorMessage, setErrorMessage] = React.useState('');

const {
dashboardConfig: {
spec: { notebookController },
},
appContextStorageClasses,
} = React.useContext(AppContext);
const tolerationSettings = notebookController?.notebookTolerationSettings;

const defaultClusterStorageClasses = appContextStorageClasses.filter((storageclass) =>
storageclass.metadata.annotations?.['storageclass.kubernetes.io/is-default-class']?.includes(
'true',
),
);

const configStorageClassName = notebookController?.storageClassName ?? '';

let storageClassNameForPVCAssembly = '';
if (defaultClusterStorageClasses.length === 0 && configStorageClassName != '') {
storageClassNameForPVCAssembly = appContextStorageClasses.filter((storageclass) =>
storageclass.metadata.name.includes(configStorageClassName),
)[0]?.metadata?.name;
if (storageClassNameForPVCAssembly === undefined) {
// eslint-disable-next-line no-console
console.error(
'no cluster default storageclass set and notebooks.storageClassName entry is not in list of cluster StorageClasses',
);
}
} else if (defaultClusterStorageClasses.length != 0) {
storageClassNameForPVCAssembly = defaultClusterStorageClasses[0]?.metadata?.name;
}

const {
notebooks: { data },
dataConnections: { data: existingDataConnections },
Expand Down Expand Up @@ -187,7 +213,11 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
? [dataConnection.existing]
: [];

const pvcDetails = await createPvcDataForNotebook(projectName, storageData).catch(handleError);
const pvcDetails = await createPvcDataForNotebook(
projectName,
storageData,
storageClassNameForPVCAssembly,
).catch(handleError);
const envFrom = await createConfigMapsAndSecretsForNotebook(projectName, [
...envVariables,
...newDataConnection,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/projects/screens/spawner/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { fetchNotebookEnvVariables } from './environmentVariables/useNotebookEnv
export const createPvcDataForNotebook = async (
projectName: string,
storageData: StorageData,
storageClassName?: string,
): Promise<{ volumes: Volume[]; volumeMounts: VolumeMount[] }> => {
const {
storageType,
Expand All @@ -42,7 +43,7 @@ export const createPvcDataForNotebook = async (
const { volumes, volumeMounts } = getVolumesByStorageData(storageData);

if (storageType === StorageType.NEW_PVC) {
const pvcData = assemblePvc(pvcName, projectName, pvcDescription, size);
const pvcData = assemblePvc(pvcName, projectName, pvcDescription, size, storageClassName);
const pvc = await createPvc(pvcData);
const newPvcName = pvc.metadata.name;
volumes.push({ name: newPvcName, persistentVolumeClaim: { claimName: newPvcName } });
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type DashboardConfig = K8sResourceCommon & {
notebookController?: {
enabled: boolean;
pvcSize?: string;
storageClassName?: string;
notebookNamespace?: string;
gpuSetting?: GpuSettingString;
notebookTolerationSettings?: TolerationSettings;
Expand Down

0 comments on commit ae7c2fb

Please sign in to comment.