Skip to content

Commit

Permalink
[RHOAIENG-1105] Add Storage Class section in Admin Panel and Feature …
Browse files Browse the repository at this point in the history
…Flag
  • Loading branch information
jpuzz0 committed Aug 27, 2024
1 parent 36e69b9 commit 9ac48d4
Show file tree
Hide file tree
Showing 21 changed files with 475 additions and 9 deletions.
1 change: 1 addition & 0 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type DashboardConfig = K8sResourceCommon & {
disableDistributedWorkloads: boolean;
disableModelRegistry: boolean;
disableConnectionTypes: boolean;
disableStorageClasses: boolean;
};
groupsConfig?: {
adminGroups: string;
Expand Down
1 change: 1 addition & 0 deletions backend/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const blankDashboardCR: DashboardConfig = {
disableDistributedWorkloads: false,
disableModelRegistry: true,
disableConnectionTypes: true,
disableStorageClasses: true,
},
notebookController: {
enabled: true,
Expand Down
2 changes: 2 additions & 0 deletions docs/dashboard-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The following are a list of features that are supported, along with there defaul
| disableDistributedWorkloads | false | Disables Distributed Workload Metrics from the dashboard. |
| disableModelRegistry | true | Disables Model Registry from the dashboard. |
| disableConnectionTypes | true | Disables creating custom data connection types from the dashboard. |
| disableStorageClasses | true | Disables storage classes settings nav item from the dashboard. |

## Defaults

Expand Down Expand Up @@ -65,6 +66,7 @@ spec:
disablePipelineExperiments: true
disableDistributedWorkloads: false
disableConnectionTypes: false
disableStorageClasses: true
```
## Additional fields
Expand Down
1 change: 1 addition & 0 deletions frontend/src/__mocks__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export * from './mockModelArtifactList';
export * from './mockModelRegistryService';
export * from './mockServingRuntimeK8sResource';
export * from './mockInferenceServiceK8sResource';
export * from './mockStorageClasses';
3 changes: 3 additions & 0 deletions frontend/src/__mocks__/mockDashboardConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type MockDashboardConfigType = {
disableDistributedWorkloads?: boolean;
disableModelRegistry?: boolean;
disableConnectionTypes?: boolean;
disableStorageClasses?: boolean;
disableNotebookController?: boolean;
notebookSizes?: NotebookSize[];
};
Expand Down Expand Up @@ -55,6 +56,7 @@ export const mockDashboardConfig = ({
disableDistributedWorkloads = false,
disableModelRegistry = true,
disableConnectionTypes = true,
disableStorageClasses = false,
disableNotebookController = false,
notebookSizes = [
{
Expand Down Expand Up @@ -161,6 +163,7 @@ export const mockDashboardConfig = ({
disableDistributedWorkloads,
disableModelRegistry,
disableConnectionTypes,
disableStorageClasses,
},
notebookController: {
enabled: !disableNotebookController,
Expand Down
201 changes: 201 additions & 0 deletions frontend/src/__mocks__/mockStorageClasses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { K8sResourceListResult, StorageClassKind } from '~/k8sTypes';

type MockStorageClass = Omit<StorageClassKind, 'apiVersion' | 'kind'>;

export type MockStorageClassList = Omit<K8sResourceListResult<MockStorageClass>, 'metadata'> & {
metadata: {
resourceVersion: string;
};
};

export const mockStorageClassList = (
storageClasses: MockStorageClass[] = mockStorageClasses,
): MockStorageClassList => ({
kind: 'StorageClassList',
apiVersion: 'storage.k8s.io/v1',
metadata: {
resourceVersion: '55571379',
},
items: storageClasses,
});

const mockStorageClasses: MockStorageClass[] = [
{
metadata: {
name: 'csi-manila-ceph',
uid: 'c3c05a4a-c1b7-4358-a246-da6b6dfd12cd',
resourceVersion: '50902775',
creationTimestamp: '2024-07-04T09:21:40Z',
annotations: {
'opendatahub.io/sc-config':
'{"displayName":"csi-manila-ceph","isDefault":false,"isEnabled":false,"lastModified":"2024-08-22T15:42:53.100Z"}',
},
managedFields: [
{
manager: 'csi-driver-manila-operator',
operation: 'Update',
apiVersion: 'storage.k8s.io/v1',
time: '2024-07-04T09:21:40Z',
fieldsType: 'FieldsV1',
fieldsV1: {
'f:parameters': {
'.': {},
'f:csi.storage.k8s.io/node-publish-secret-name': {},
'f:csi.storage.k8s.io/node-publish-secret-namespace': {},
'f:csi.storage.k8s.io/node-stage-secret-name': {},
'f:csi.storage.k8s.io/node-stage-secret-namespace': {},
'f:csi.storage.k8s.io/provisioner-secret-name': {},
'f:csi.storage.k8s.io/provisioner-secret-namespace': {},
'f:type': {},
},
'f:provisioner': {},
'f:reclaimPolicy': {},
'f:volumeBindingMode': {},
},
},
{
manager: 'unknown',
operation: 'Update',
apiVersion: 'storage.k8s.io/v1',
time: '2024-08-22T15:42:53Z',
fieldsType: 'FieldsV1',
fieldsV1: {
'f:metadata': {
'f:annotations': {
'.': {},
'f:opendatahub.io/sc-config': {},
},
},
},
},
],
},
provisioner: 'manila.csi.openstack.org',
parameters: JSON.stringify({
'csi.storage.k8s.io/node-publish-secret-name': 'csi-manila-secrets',
'csi.storage.k8s.io/node-publish-secret-namespace': 'openshift-manila-csi-driver',
'csi.storage.k8s.io/node-stage-secret-name': 'csi-manila-secrets',
'csi.storage.k8s.io/node-stage-secret-namespace': 'openshift-manila-csi-driver',
'csi.storage.k8s.io/provisioner-secret-name': 'csi-manila-secrets',
'csi.storage.k8s.io/provisioner-secret-namespace': 'openshift-manila-csi-driver',
type: 'ceph',
}),
reclaimPolicy: 'Delete',
volumeBindingMode: 'Immediate',
},
{
metadata: {
name: 'csi-manila-netapp',
uid: 'f818edb6-3936-4ca0-90af-87f469c177d8',
resourceVersion: '50902773',
creationTimestamp: '2024-07-04T09:21:40Z',
annotations: {
'opendatahub.io/sc-config':
'{"displayName":"csi-manila-netapp","isDefault":false,"isEnabled":false,"lastModified":"2024-08-22T15:42:53.101Z"}',
},
managedFields: [
{
manager: 'csi-driver-manila-operator',
operation: 'Update',
apiVersion: 'storage.k8s.io/v1',
time: '2024-07-04T09:21:40Z',
fieldsType: 'FieldsV1',
fieldsV1: {
'f:parameters': {
'.': {},
'f:csi.storage.k8s.io/node-publish-secret-name': {},
'f:csi.storage.k8s.io/node-publish-secret-namespace': {},
'f:csi.storage.k8s.io/node-stage-secret-name': {},
'f:csi.storage.k8s.io/node-stage-secret-namespace': {},
'f:csi.storage.k8s.io/provisioner-secret-name': {},
'f:csi.storage.k8s.io/provisioner-secret-namespace': {},
'f:type': {},
},
'f:provisioner': {},
'f:reclaimPolicy': {},
'f:volumeBindingMode': {},
},
},
{
manager: 'unknown',
operation: 'Update',
apiVersion: 'storage.k8s.io/v1',
time: '2024-08-22T15:42:53Z',
fieldsType: 'FieldsV1',
fieldsV1: {
'f:metadata': {
'f:annotations': {
'.': {},
'f:opendatahub.io/sc-config': {},
},
},
},
},
],
},
provisioner: 'manila.csi.openstack.org',
parameters: JSON.stringify({
'csi.storage.k8s.io/node-publish-secret-name': 'csi-manila-secrets',
'csi.storage.k8s.io/node-publish-secret-namespace': 'openshift-manila-csi-driver',
'csi.storage.k8s.io/node-stage-secret-name': 'csi-manila-secrets',
'csi.storage.k8s.io/node-stage-secret-namespace': 'openshift-manila-csi-driver',
'csi.storage.k8s.io/provisioner-secret-name': 'csi-manila-secrets',
'csi.storage.k8s.io/provisioner-secret-namespace': 'openshift-manila-csi-driver',
type: 'netapp',
}),
reclaimPolicy: 'Delete',
volumeBindingMode: 'Immediate',
},
{
metadata: {
name: 'standard-csi',
uid: '5de188ae-aa8e-43d1-a714-4d60ecc5c6da',
resourceVersion: '50902774',
creationTimestamp: '2024-07-04T09:20:40Z',
annotations: {
'opendatahub.io/sc-config':
'{"displayName":"standard-csi","isDefault":true,"isEnabled":true,"lastModified":"2024-08-22T15:42:53.101Z"}',
'storageclass.kubernetes.io/is-default-class': 'true',
},
managedFields: [
{
manager: 'openstack-cinder-csi-driver-operator',
operation: 'Update',
apiVersion: 'storage.k8s.io/v1',
time: '2024-07-04T09:20:40Z',
fieldsType: 'FieldsV1',
fieldsV1: {
'f:allowVolumeExpansion': {},
'f:metadata': {
'f:annotations': {
'.': {},
'f:storageclass.kubernetes.io/is-default-class': {},
},
},
'f:provisioner': {},
'f:reclaimPolicy': {},
'f:volumeBindingMode': {},
},
},
{
manager: 'unknown',
operation: 'Update',
apiVersion: 'storage.k8s.io/v1',
time: '2024-08-22T15:42:53Z',
fieldsType: 'FieldsV1',
fieldsV1: {
'f:metadata': {
'f:annotations': {
'f:opendatahub.io/sc-config': {},
},
},
},
},
],
},
provisioner: 'cinder.csi.openstack.org',
reclaimPolicy: 'Delete',
allowVolumeExpansion: true,
volumeBindingMode: 'WaitForFirstConsumer',
},
];
23 changes: 23 additions & 0 deletions frontend/src/__tests__/cypress/cypress/pages/storageClasses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { appChrome } from '~/__tests__/cypress/cypress/pages/appChrome';

class StorageClassesPage {
visit() {
cy.visitWithLogin('/storageClasses');
this.wait();
}

private wait() {
cy.findByTestId('app-page-title').contains('Storage classes');
cy.testA11y();
}

findNavItem() {
return appChrome.findNavItem('Storage classes', 'Settings');
}

findEmptyState() {
return cy.findByTestId('storage-classes-empty-state');
}
}

export const storageClassesPage = new StorageClassesPage();
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import type {
PipelineVersionKFv2,
} from '~/concepts/pipelines/kfTypes';
import type { GrpcResponse } from '~/__mocks__/mlmd/utils';
import type { BuildMockPipelinveVersionsType } from '~/__mocks__';
import type { BuildMockPipelinveVersionsType, MockStorageClassList } from '~/__mocks__';
import type { ArtifactStorage } from '~/concepts/pipelines/types';
import type { ConnectionTypeConfigMap } from '~/concepts/connectionTypes/types';

Expand Down Expand Up @@ -188,6 +188,10 @@ declare global {
options: { query: { dryRun: 'All' } } | null,
response: OdhResponse<ServingRuntimeKind>,
) => Cypress.Chainable<null>) &
((
type: 'GET /api/k8s/apis/storage.k8s.io/v1/storageclasses',
response: OdhResponse<MockStorageClassList>,
) => Cypress.Chainable<null>) &
((
type: 'GET /api/images/byon',
response: OdhResponse<BYONImage[]>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { mockStorageClassList } from '~/__mocks__';
import { asProductAdminUser } from '~/__tests__/cypress/cypress/utils/mockUsers';
import { pageNotfound } from '~/__tests__/cypress/cypress/pages/pageNotFound';
import { storageClassesPage } from '~/__tests__/cypress/cypress/pages/storageClasses';

describe('Storage classes', () => {
it('shows "page not found" and does not show nav item as a non-admin user', () => {
cy.visitWithLogin('/storageClasses');
storageClassesPage.findNavItem().should('not.exist');
pageNotfound.findPage().should('be.visible');
});

describe('as an admin user', () => {
beforeEach(() => {
asProductAdminUser();
});

it('shows empty state when the returned storage class list is empty', () => {
cy.interceptOdh(
'GET /api/k8s/apis/storage.k8s.io/v1/storageclasses',
mockStorageClassList([]),
);
storageClassesPage.visit();
storageClassesPage.findNavItem().should('be.visible');
storageClassesPage.findEmptyState().should('be.visible');
});
});
});
6 changes: 6 additions & 0 deletions frontend/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,16 @@ const AcceleratorProfileRoutes = React.lazy(
() => import('../pages/acceleratorProfiles/AcceleratorProfilesRoutes'),
);

const StorageClassesPage = React.lazy(() => import('../pages/storageClasses/StorageClassesPage'));

const ModelRegistryRoutes = React.lazy(() => import('../pages/modelRegistry/ModelRegistryRoutes'));

const AppRoutes: React.FC = () => {
const { isAdmin, isAllowed } = useUser();
const isJupyterEnabled = useCheckJupyterEnabled();
const isHomeAvailable = useIsAreaAvailable(SupportedArea.HOME).status;
const isConnectionTypesAvailable = useIsAreaAvailable(SupportedArea.CONNECTION_TYPES).status;
const isStorageClassesAvailable = useIsAreaAvailable(SupportedArea.STORAGE_CLASSES).status;

if (!isAllowed) {
return (
Expand Down Expand Up @@ -128,6 +131,9 @@ const AppRoutes: React.FC = () => {
{isConnectionTypesAvailable ? (
<Route path="/connectionTypes/*" element={<ConnectionTypeRoutes />} />
) : null}
{isStorageClassesAvailable && (
<Route path="/storageClasses/*" element={<StorageClassesPage />} />
)}
<Route path="/modelRegistrySettings/*" element={<ModelRegistrySettingsRoutes />} />
<Route path="/groupSettings" element={<GroupSettingsPage />} />
</>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/concepts/areas/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const allFeatureFlags: string[] = Object.keys({
disableDistributedWorkloads: false,
disableModelRegistry: false,
disableConnectionTypes: false,
disableStorageClasses: false,
} satisfies DashboardCommonConfig);

export const SupportedAreasStateMap: SupportedAreasState = {
Expand All @@ -47,6 +48,9 @@ export const SupportedAreasStateMap: SupportedAreasState = {
[SupportedArea.CONNECTION_TYPES]: {
featureFlags: ['disableConnectionTypes'],
},
[SupportedArea.STORAGE_CLASSES]: {
featureFlags: ['disableStorageClasses'],
},
[SupportedArea.DS_PIPELINES]: {
featureFlags: ['disablePipelines'],
requiredComponents: [StackComponent.DS_PIPELINES],
Expand Down
1 change: 1 addition & 0 deletions frontend/src/concepts/areas/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export enum SupportedArea {
USER_MANAGEMENT = 'user-management',
ACCELERATOR_PROFILES = 'accelerator-profiles',
CONNECTION_TYPES = 'connections-types',
STORAGE_CLASSES = 'storage-classes',

/* DS Projects specific areas */
DS_PROJECTS_PERMISSIONS = 'ds-projects-permission',
Expand Down
Loading

0 comments on commit 9ac48d4

Please sign in to comment.