Skip to content

Commit

Permalink
Merge pull request #2650 from mturley/dw-localqueues
Browse files Browse the repository at this point in the history
DW: Fetch LocalQueues, pass them down in context, use them for "quota is not set" empty state
  • Loading branch information
openshift-merge-bot[bot] authored Apr 4, 2024
2 parents cb94bc1 + 4f8d167 commit 987730d
Show file tree
Hide file tree
Showing 14 changed files with 466 additions and 133 deletions.
61 changes: 19 additions & 42 deletions frontend/src/__mocks__/mockClusterQueueK8sResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { genUID } from '~/__mocks__/mockUtils';

type MockResourceConfigType = {
name?: string;
hasResourceGroups?: boolean;
};

export const mockClusterQueueK8sResource = ({
name = 'test-cluster-queue',
hasResourceGroups = true,
}: MockResourceConfigType): ClusterQueueKind => ({
apiVersion: 'kueue.x-k8s.io/v1beta1',
kind: 'ClusterQueue',
Expand All @@ -18,39 +20,30 @@ export const mockClusterQueueK8sResource = ({
uid: genUID('clusterqueue'),
},
spec: {
flavorFungibility: {
whenCanBorrow: 'Borrow',
whenCanPreempt: 'TryNextFlavor',
},
flavorFungibility: { whenCanBorrow: 'Borrow', whenCanPreempt: 'TryNextFlavor' },
namespaceSelector: {},
preemption: {
borrowWithinCohort: {
policy: 'Never',
},
borrowWithinCohort: { policy: 'Never' },
reclaimWithinCohort: 'Never',
withinClusterQueue: 'Never',
},
queueingStrategy: 'BestEffortFIFO',
resourceGroups: [
{
coveredResources: ['cpu', 'memory'],
flavors: [
resourceGroups: hasResourceGroups
? [
{
name: 'test-flavor',
resources: [
{
name: 'cpu',
nominalQuota: '20',
},
coveredResources: ['cpu', 'memory'],
flavors: [
{
name: 'memory',
nominalQuota: '36Gi',
name: 'test-flavor',
resources: [
{ name: 'cpu', nominalQuota: '20' },
{ name: 'memory', nominalQuota: '36Gi' },
],
},
],
},
],
},
],
]
: [],
stopPolicy: 'None',
},
status: {
Expand All @@ -68,33 +61,17 @@ export const mockClusterQueueK8sResource = ({
{
name: 'test-flavor',
resources: [
{
borrowed: '0',
name: 'cpu',
total: '0',
},
{
borrowed: '0',
name: 'memory',
total: '0',
},
{ borrowed: '0', name: 'cpu', total: '0' },
{ borrowed: '0', name: 'memory', total: '0' },
],
},
],
flavorsUsage: [
{
name: 'test-flavor',
resources: [
{
borrowed: '0',
name: 'cpu',
total: '0',
},
{
borrowed: '0',
name: 'memory',
total: '0',
},
{ borrowed: '0', name: 'cpu', total: '0' },
{ borrowed: '0', name: 'memory', total: '0' },
],
},
],
Expand Down
59 changes: 59 additions & 0 deletions frontend/src/__mocks__/mockLocalQueueK8sResource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { LocalQueueKind } from '~/k8sTypes';
import { genUID } from '~/__mocks__/mockUtils';

type MockResourceConfigType = {
name?: string;
namespace?: string;
};

export const mockLocalQueueK8sResource = ({
name = 'test-local-queue',
namespace = 'test-project',
}: MockResourceConfigType): LocalQueueKind => ({
apiVersion: 'kueue.x-k8s.io/v1beta1',
kind: 'LocalQueue',
metadata: {
creationTimestamp: '2024-03-26T14:12:10Z',
generation: 1,
name,
namespace,
uid: genUID('localqueue'),
},
spec: {
clusterQueue: 'test-cluster-queue',
},
status: {
pendingWorkloads: 0,
reservingWorkloads: 0,
admittedWorkloads: 0,
conditions: [
{
lastTransitionTime: '2024-03-26T15:49:12Z',
message: 'Can submit new workloads to clusterQueue',
reason: 'Ready',
status: 'True',
type: 'Active',
},
],
flavorUsage: [
{
name: 'test-flavor',
resources: [
{ name: 'cpu', total: '0' },
{ name: 'memory', total: '0' },
{ name: 'nvidia.com/gpu', total: '0' },
],
},
],
flavorsReservation: [
{
name: 'test-flavor',
resources: [
{ name: 'cpu', total: '0' },
{ name: 'memory', total: '0' },
{ name: 'nvidia.com/gpu', total: '0' },
],
},
],
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,28 @@ import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList';
import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource';
import { mockPrometheusQueryVectorResponse } from '~/__mocks__/mockPrometheusQueryVectorResponse';
import { mockWorkloadK8sResource } from '~/__mocks__/mockWorkloadK8sResource';
import { WorkloadKind } from '~/k8sTypes';
import { ClusterQueueKind, LocalQueueKind, WorkloadKind } from '~/k8sTypes';
import { WorkloadStatusType } from '~/concepts/distributedWorkloads/utils';
import { mockClusterQueueK8sResource } from '~/__mocks__/mockClusterQueueK8sResource';
import { mockLocalQueueK8sResource } from '~/__mocks__/mockLocalQueueK8sResource';

type HandlersProps = {
isKueueInstalled?: boolean;
disableDistributedWorkloads?: boolean;
hasProjects?: boolean;
clusterQueues?: ClusterQueueKind[];
localQueues?: LocalQueueKind[];
workloads?: WorkloadKind[];
};

const initIntercepts = ({
isKueueInstalled = true,
disableDistributedWorkloads = false,
hasProjects = true,
clusterQueues = [mockClusterQueueK8sResource({ name: 'test-cluster-queue' })],
localQueues = [
mockLocalQueueK8sResource({ name: 'test-local-queue', namespace: 'test-project' }),
],
workloads = [
mockWorkloadK8sResource({ k8sName: 'test-workload', mockStatus: WorkloadStatusType.Succeeded }),
mockWorkloadK8sResource({
Expand Down Expand Up @@ -58,6 +66,20 @@ const initIntercepts = ({
: [],
),
);
cy.intercept(
{
method: 'GET',
pathname: '/api/k8s/apis/kueue.x-k8s.io/v1beta1/clusterqueues',
},
mockK8sResourceList(clusterQueues),
);
cy.intercept(
{
method: 'GET',
pathname: '/api/k8s/apis/kueue.x-k8s.io/v1beta1/namespaces/*/localqueues',
},
mockK8sResourceList(localQueues),
);
cy.intercept(
{
method: 'GET',
Expand All @@ -77,8 +99,8 @@ const initIntercepts = ({
);
};

describe('Distributed Workload Metrics', () => {
it('Distributed Workload Metrics page does not exist if kueue is not installed', () => {
describe('Distributed Workload Metrics root page', () => {
it('Does not exist if kueue is not installed', () => {
initIntercepts({
isKueueInstalled: false,
disableDistributedWorkloads: false,
Expand All @@ -91,7 +113,7 @@ describe('Distributed Workload Metrics', () => {
globalDistributedWorkloads.shouldNotFoundPage();
});

it('Distributed Workload Metrics page does not exist if feature is disabled', () => {
it('Does not exist if feature is disabled', () => {
initIntercepts({
isKueueInstalled: true,
disableDistributedWorkloads: true,
Expand All @@ -104,7 +126,7 @@ describe('Distributed Workload Metrics', () => {
globalDistributedWorkloads.shouldNotFoundPage();
});

it('Distributed Workload Metrics page exists if kueue is installed and feature is enabled', () => {
it('Exists if kueue is installed and feature is enabled', () => {
initIntercepts({
isKueueInstalled: true,
disableDistributedWorkloads: false,
Expand Down Expand Up @@ -161,7 +183,62 @@ describe('Distributed Workload Metrics', () => {

cy.findByText('No data science projects').should('exist');
});
});

describe('Project Metrics tab', () => {
it('Should render with no quota state when there is no clusterqueue', () => {
initIntercepts({ clusterQueues: [] });
globalDistributedWorkloads.visit();
cy.findByLabelText('Project metrics tab').click();
cy.findByText('Quota is not set').should('exist');
});

it('Should render with no quota state when the clusterqueue has no resourceGroups', () => {
initIntercepts({
clusterQueues: [
mockClusterQueueK8sResource({ name: 'test-cluster-queue', hasResourceGroups: false }),
],
});
globalDistributedWorkloads.visit();
cy.findByLabelText('Project metrics tab').click();
cy.findByText('Quota is not set').should('exist');
});

it('Should render with no quota state when there are no localqueues', () => {
initIntercepts({ localQueues: [] });
globalDistributedWorkloads.visit();
cy.findByLabelText('Project metrics tab').click();
cy.findByText('Quota is not set').should('exist');
});

it('Should render with no workloads empty state', () => {
initIntercepts({ workloads: [] });
globalDistributedWorkloads.visit();

cy.findByLabelText('Project metrics tab').click();

cy.findByText('Resource Usage').should('exist');

cy.findByText('Top resource-consuming distributed workloads')
.closest('.dw-section-card')
.within(() => {
cy.findByText('No distributed workloads');
});
cy.findByText('Distributed workload resource metrics')
.closest('.dw-section-card')
.within(() => {
cy.findByText('No distributed workloads');
});
cy.findByText('Resource Usage')
.closest('.dw-section-card')
.within(() => {
//Resource Usage shows chart even if empty workload\
cy.findByText('Charts Placeholder');
});
});
});

describe('Workload Status tab', () => {
it('Should render the status overview chart', () => {
initIntercepts({});
globalDistributedWorkloads.visit();
Expand Down Expand Up @@ -202,30 +279,4 @@ describe('Distributed Workload Metrics', () => {
cy.findByLabelText('Distributed workload status tab').click();
cy.findByText('No distributed workloads match your filters').should('exist');
});

it('Should render the projects metrics with empty workload state', () => {
initIntercepts({ workloads: [] });
globalDistributedWorkloads.visit();

cy.findByLabelText('Project metrics tab').click();

cy.findByText('Resource Usage').should('exist');

cy.findByText('Top resource-consuming distributed workloads')
.closest('.dw-section-card')
.within(() => {
cy.findByText('No distributed workloads');
});
cy.findByText('Distributed workload resource metrics')
.closest('.dw-section-card')
.within(() => {
cy.findByText('No distributed workloads');
});
cy.findByText('Resource Usage')
.closest('.dw-section-card')
.within(() => {
//Resource Usage shows chart even if empty workload\
cy.findByText('Charts Placeholder');
});
});
});
1 change: 1 addition & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './k8s/templates';
export * from './k8s/dashboardConfig';
export * from './k8s/acceleratorProfiles';
export * from './k8s/clusterQueues';
export * from './k8s/localQueues';
export * from './k8s/workloads';

// Model registry
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/api/k8s/__tests__/localQueues.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { k8sListResourceItems } from '@openshift/dynamic-plugin-sdk-utils';
import { mockLocalQueueK8sResource } from '~/__mocks__/mockLocalQueueK8sResource';
import { LocalQueueKind } from '~/k8sTypes';
import { listLocalQueues } from '~/api/k8s/localQueues';

jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({
k8sListResourceItems: jest.fn(),
}));

const k8sListResourceItemsMock = jest.mocked(k8sListResourceItems<LocalQueueKind>);

const mockedLocalQueue = mockLocalQueueK8sResource({
name: 'test-local-queue',
namespace: 'test-project',
});

describe('listLocalQueues', () => {
it('should fetch and return localqueues', async () => {
k8sListResourceItemsMock.mockResolvedValue([mockedLocalQueue]);
const result = await listLocalQueues('test-project');
expect(k8sListResourceItemsMock).toHaveBeenCalledWith({
model: {
apiGroup: 'kueue.x-k8s.io',
apiVersion: 'v1beta1',
kind: 'LocalQueue',
plural: 'localqueues',
},
queryOptions: { ns: 'test-project' },
});
expect(k8sListResourceItemsMock).toHaveBeenCalledTimes(1);
expect(result).toStrictEqual([mockedLocalQueue]);
});

it('should handle errors and rethrow', async () => {
k8sListResourceItemsMock.mockRejectedValue(new Error('error1'));
await expect(listLocalQueues('test-project')).rejects.toThrow('error1');
expect(k8sListResourceItemsMock).toHaveBeenCalledTimes(1);
expect(k8sListResourceItemsMock).toHaveBeenCalledWith({
model: {
apiGroup: 'kueue.x-k8s.io',
apiVersion: 'v1beta1',
kind: 'LocalQueue',
plural: 'localqueues',
},
queryOptions: { ns: 'test-project' },
});
});
});
Loading

0 comments on commit 987730d

Please sign in to comment.