Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authorization to kserve #2625

Merged
merged 1 commit into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend/src/routes/api/dsci/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getClusterInitialization } from '../../../utils/dsci';
import { KubeFastifyInstance } from '../../../types';
import { secureRoute } from '../../../utils/route-security';

module.exports = async (fastify: KubeFastifyInstance) => {
fastify.get(
'/status',
secureRoute(fastify)(async () => {
return getClusterInitialization(fastify);
}),
);
};
20 changes: 18 additions & 2 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type DashboardConfig = K8sResourceCommon & {
disableBiasMetrics: boolean;
disablePerformanceMetrics: boolean;
disableKServe: boolean;
disableKServeAuth: boolean;
disableModelMesh: boolean;
disableAcceleratorProfiles: boolean;
disablePipelineExperiments: boolean;
Expand Down Expand Up @@ -945,7 +946,7 @@ type ComponentNames =
| 'workbenches';

export type DataScienceClusterKindStatus = {
conditions: [];
conditions: K8sCondition[];
installedComponents: { [key in ComponentNames]?: boolean };
phase?: string;
};
Expand All @@ -960,6 +961,21 @@ export type DataScienceClusterList = {
items: DataScienceClusterKind[];
};

export type DataScienceClusterInitializationKindStatus = {
conditions: K8sCondition[];
phase?: string;
};

export type DataScienceClusterInitializationKind = K8sResourceCommon & {
spec: unknown; // we should never need to look into this
status: DataScienceClusterInitializationKindStatus;
};

export type DataScienceClusterInitializationList = {
kind: 'DataScienceClusterInitializationList';
items: DataScienceClusterInitializationKind[];
};

export type SubscriptionStatusData = {
installedCSV?: string;
installPlanRefNamespace?: string;
Expand Down Expand Up @@ -997,4 +1013,4 @@ export type DSPipelineKind = K8sResourceCommon & {
status?: {
conditions?: K8sCondition[];
};
};
};
1 change: 1 addition & 0 deletions backend/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const blankDashboardCR: DashboardConfig = {
disablePerformanceMetrics: false,
disablePipelines: false,
disableKServe: false,
disableKServeAuth: false,
disableModelMesh: false,
disableAcceleratorProfiles: false,
disablePipelineExperiments: true,
Expand Down
25 changes: 25 additions & 0 deletions backend/src/utils/dsci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
DataScienceClusterInitializationKind,
DataScienceClusterInitializationKindStatus,
DataScienceClusterInitializationList,
KubeFastifyInstance,
} from '../types';
import { createCustomError } from './requestUtils';

export const getClusterInitialization = async (
fastify: KubeFastifyInstance,
): Promise<DataScienceClusterInitializationKindStatus> => {
const result: DataScienceClusterInitializationKind | null = await fastify.kube.customObjectsApi
.listClusterCustomObject('dscinitialization.opendatahub.io', 'v1', 'dscinitializations')
.then((res) => (res.body as DataScienceClusterInitializationList).items[0])
.catch((e) => {
fastify.log.error(`Failure to fetch dsci: ${e.response.body}`);
return null;
});

if (!result) {
throw createCustomError('DSCI Unavailable', 'Unable to get status', 404);
}

return result.status;
};
1 change: 1 addition & 0 deletions docs/dashboard-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The following are a list of features that are supported, along with there defaul
| disableProjectSharing | false | Disables Project Sharing from Data Science Projects. |
| disableCustomServingRuntimes | false | Disables Custom Serving Runtimes from the Admin Panel. |
| disableKServe | false | Disables the ability to select KServe as a Serving Platform. |
| disableKServeAuth | false | Disables the ability to use auth in KServe. |
| disableModelMesh | false | Disables the ability to select ModelMesh as a Serving Platform. |
| disableAcceleratorProfiles | false | Disables Accelerator profiles from the Admin Panel. |
| modelMetricsNamespace | false | Enables the namespace in which the Model Serving Metrics' Prometheus Operator is installed. |
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/__mocks__/mockDashboardConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type MockDashboardConfigType = {
disableModelServing?: boolean;
disableCustomServingRuntimes?: boolean;
disableKServe?: boolean;
disableKServeAuth?: boolean;
disableModelMesh?: boolean;
disableAcceleratorProfiles?: boolean;
disablePerformanceMetrics?: boolean;
Expand All @@ -37,6 +38,7 @@ export const mockDashboardConfig = ({
disableCustomServingRuntimes = false,
disablePipelines = false,
disableKServe = false,
disableKServeAuth = false,
disableModelMesh = false,
disableAcceleratorProfiles = false,
disablePerformanceMetrics = false,
Expand Down Expand Up @@ -73,6 +75,7 @@ export const mockDashboardConfig = ({
disableBiasMetrics,
disablePerformanceMetrics,
disableKServe,
disableKServeAuth,
disableModelMesh,
disableAcceleratorProfiles,
disablePipelineExperiments,
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/__mocks__/mockDsciStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { StackCapability } from '~/concepts/areas/types';
import { DataScienceClusterInitializationKindStatus, K8sCondition } from '~/k8sTypes';

export type MockDsciStatus = {
conditions?: K8sCondition[];
requiredCapabilities?: StackCapability[];
phase?: string;
};

export const mockDsciStatus = ({
conditions = [],
requiredCapabilities = [],
phase = 'Ready',
}: MockDsciStatus): DataScienceClusterInitializationKindStatus => ({
conditions: [
...[
{
lastHeartbeatTime: '2023-10-20T11:45:04Z',
lastTransitionTime: '2023-10-20T11:45:04Z',
message: 'Reconcile completed successfully',
reason: 'ReconcileCompleted',
status: 'True',
type: 'ReconcileComplete',
},
...requiredCapabilities.map((capability) => ({
lastHeartbeatTime: '2023-10-20T11:45:04Z',
lastTransitionTime: '2023-10-20T11:45:04Z',
message: `Capability ${capability} installed`,
reason: 'ReconcileCompleted',
status: 'True',
type: capability,
})),
],
...conditions,
],
phase,
});
6 changes: 6 additions & 0 deletions frontend/src/__mocks__/mockInferenceServiceModalData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const mockInferenceServiceModalData = ({
},
minReplicas = 1,
maxReplicas = 1,
externalRoute = false,
tokenAuth = false,
tokens = [],
}: MockResourceConfigType): CreatingInferenceServiceObject => ({
name,
project,
Expand All @@ -29,4 +32,7 @@ export const mockInferenceServiceModalData = ({
format,
minReplicas,
maxReplicas,
externalRoute,
tokenAuth,
tokens,
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ import { be } from '~/__tests__/cypress/cypress/utils/should';
import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes';
import { ServingRuntimePlatform } from '~/types';
import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal';
import { StackCapability } from '~/concepts/areas/types';
import { mockDsciStatus } from '~/__mocks__/mockDsciStatus';

type HandlersProps = {
disableKServeConfig?: boolean;
disableKServeAuthConfig?: boolean;
disableModelMeshConfig?: boolean;
disableAccelerator?: boolean;
projectEnableModelMesh?: boolean;
Expand All @@ -50,10 +53,12 @@ type HandlersProps = {
rejectInferenceService?: boolean;
rejectServingRuntime?: boolean;
rejectDataConnection?: boolean;
requiredCapabilities?: StackCapability[];
};

const initIntercepts = ({
disableKServeConfig,
disableKServeAuthConfig,
disableModelMeshConfig,
disableAccelerator,
projectEnableModelMesh,
Expand Down Expand Up @@ -86,19 +91,27 @@ const initIntercepts = ({
rejectInferenceService = false,
rejectServingRuntime = false,
rejectDataConnection = false,
requiredCapabilities = [],
}: HandlersProps) => {
cy.intercept(
'/api/dsc/status',
mockDscStatus({
installedComponents: { kserve: true, 'model-mesh': true },
}),
);
cy.intercept(
'/api/dsci/status',
mockDsciStatus({
requiredCapabilities,
}),
);
cy.intercept('/api/status', mockStatus());
cy.intercept(
'/api/config',
mockDashboardConfig({
disableKServe: disableKServeConfig,
disableModelMesh: disableModelMeshConfig,
disableKServeAuth: disableKServeAuthConfig,
}),
);
cy.intercept(
Expand Down Expand Up @@ -545,6 +558,7 @@ describe('Serving Runtime List', () => {
disableModelMeshConfig: false,
disableKServeConfig: false,
servingRuntimes: [],
requiredCapabilities: [StackCapability.SERVICE_MESH, StackCapability.SERVICE_MESH_AUTHZ],
});

projectDetails.visitSection('test-project', 'model-server');
Expand All @@ -561,6 +575,10 @@ describe('Serving Runtime List', () => {
kserveModal.findServingRuntimeTemplateDropdown().findDropdownItem('Caikit').click();
kserveModal.findModelFrameworkSelect().findSelectOption('onnx - 1').click();
kserveModal.findSubmitButton().should('be.disabled');
// check external route, token should be checked and no alert
kserveModal.findAuthenticationCheckbox().check();
kserveModal.findExternalRouteError().should('not.exist');
kserveModal.findServiceAccountNameInput().should('have.value', 'default-name');
kserveModal.findExistingConnectionSelect().findSelectOption('Test Secret').click();
kserveModal.findLocationPathInput().type('test-model/');
kserveModal.findSubmitButton().should('be.enabled');
Expand Down Expand Up @@ -615,6 +633,31 @@ describe('Serving Runtime List', () => {
});
});

it('Kserve auth should be hidden when auth is disabled', () => {
initIntercepts({
disableModelMeshConfig: false,
disableKServeConfig: false,
servingRuntimes: [],
});

projectDetails.visitSection('test-project', 'model-server');

modelServingSection.getServingPlatformCard('single-serving').findDeployModelButton().click();

kserveModal.shouldBeOpen();

// test that you can not submit on empty
kserveModal.findSubmitButton().should('be.disabled');

// test filling in minimum required fields
kserveModal.findModelNameInput().type('Test Name');
kserveModal.findServingRuntimeTemplateDropdown().findDropdownItem('Caikit').click();
kserveModal.findModelFrameworkSelect().findSelectOption('onnx - 1').click();
kserveModal.findSubmitButton().should('be.disabled');
// check external route, token should be checked and no alert
kserveModal.findAuthenticationCheckbox().should('not.exist');
});

it('Do not deploy KServe model when user cannot edit namespace', () => {
initIntercepts({
disableModelMeshConfig: false,
Expand Down Expand Up @@ -1495,6 +1538,37 @@ describe('Serving Runtime List', () => {
});
});

describe('Check token section in serving runtime details', () => {
it('Check token section is enabled if capability is enabled', () => {
initIntercepts({
projectEnableModelMesh: false,
disableKServeConfig: false,
disableModelMeshConfig: true,
disableAccelerator: true,
requiredCapabilities: [StackCapability.SERVICE_MESH, StackCapability.SERVICE_MESH_AUTHZ],
});
projectDetails.visitSection('test-project', 'model-server');
const kserveRow = modelServingSection.getKServeRow('Llama Caikit');
kserveRow.findExpansion().should(be.collapsed);
kserveRow.findToggleButton().click();
kserveRow.findDescriptionListItem('Token authorization').should('exist');
});

it('Check token section is disabled if capability is disabled', () => {
initIntercepts({
projectEnableModelMesh: false,
disableKServeConfig: false,
disableModelMeshConfig: true,
disableAccelerator: true,
});
projectDetails.visitSection('test-project', 'model-server');
const kserveRow = modelServingSection.getKServeRow('Llama Caikit');
kserveRow.findExpansion().should(be.collapsed);
kserveRow.findToggleButton().click();
kserveRow.findDescriptionListItem('Token authorization').should('not.exist');
});
});

describe('Dry run check', () => {
it('Check when inference service dryRun fails', () => {
initIntercepts({
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/__tests__/cypress/cypress/pages/modelServing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ class InferenceServiceModal extends Modal {
return this.find().findByTestId('existing-data-connection-radio');
}

findExternalRouteError() {
return this.find().findByTestId('external-route-no-token-alert');
}

findServiceAccountNameInput() {
return this.find().findByTestId('service-account-form-name');
}

findNewDataConnectionOption() {
return this.find().findByTestId('new-data-connection-radio');
}
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/api/k8s/__tests__/inferenceServices.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,30 @@ describe('assembleInferenceService', () => {
expect(inferenceService.metadata.annotations?.['serving.kserve.io/deploymentMode']).toBe(
undefined,
);
expect(inferenceService.metadata.annotations?.['security.opendatahub.io/enable-auth']).toBe(
undefined,
);
expect(
inferenceService.metadata.annotations?.['serving.knative.openshift.io/enablePassthrough'],
).toBe('true');
expect(inferenceService.metadata.annotations?.['sidecar.istio.io/inject']).toBe('true');
expect(inferenceService.metadata.annotations?.['sidecar.istio.io/rewriteAppHTTPProbers']).toBe(
'true',
);
});

it('should have the right annotations when creating for Kserve with auth', async () => {
const inferenceService = assembleInferenceService(
mockInferenceServiceModalData({ tokenAuth: true }),
);

expect(inferenceService.metadata.annotations).toBeDefined();
expect(inferenceService.metadata.annotations?.['serving.kserve.io/deploymentMode']).toBe(
undefined,
);
expect(inferenceService.metadata.annotations?.['security.opendatahub.io/enable-auth']).toBe(
'true',
);
expect(
inferenceService.metadata.annotations?.['serving.knative.openshift.io/enablePassthrough'],
).toBe('true');
Expand Down
Loading
Loading