Skip to content
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
75 changes: 75 additions & 0 deletions app/app/api/private-cloud/products/_operations/create.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from '@jest/globals';
import { GlobalRole } from '@/constants';
import { createSamplePrivateCloudProductData } from '@/helpers/mock-resources';
import { resourceRequests1 } from '@/helpers/mock-resources/private-cloud-product';
import { findOtherMockUsers } from '@/helpers/mock-users';
import { pickProductData } from '@/helpers/product';
import { Cluster } from '@/prisma/client';
Expand Down Expand Up @@ -203,4 +204,78 @@ describe('Create Private Cloud Request - Validations', () => {
const response = await createPrivateCloudProduct(requestData);
expect(response.status).toBe(200);
});

it('should reset GPU quota to 0 for non-admin users on Emerald', async () => {
const product = createSamplePrivateCloudProductData({
data: {
cluster: Cluster.EMERALD,
resourceRequests: {
...resourceRequests1,
development: {
...resourceRequests1.development,
gpu: 4,
},
},
},
});

await mockSessionByIdirGuid(product.projectOwner.idirGuid);

const response = await createPrivateCloudProduct(product);

expect(response.status).toBe(200);

const responseData = await response.json();

expect(responseData.decisionData.resourceRequests.development.gpu).toBe(0);
});
it('should reset GPU quota to 0 for unsupported clusters', async () => {
const product = createSamplePrivateCloudProductData({
data: {
cluster: Cluster.SILVER,
resourceRequests: {
...resourceRequests1,
development: {
...resourceRequests1.development,
gpu: 4,
},
},
},
});

await mockSessionByRole(GlobalRole.Admin);

const response = await createPrivateCloudProduct(product);

expect(response.status).toBe(200);

const responseData = await response.json();

expect(responseData.decisionData.resourceRequests.development.gpu).toBe(0);
});

it('should preserve GPU quota for private admin on Emerald', async () => {
const product = createSamplePrivateCloudProductData({
data: {
cluster: Cluster.EMERALD,
resourceRequests: {
...resourceRequests1,
development: {
...resourceRequests1.development,
gpu: 4,
},
},
},
});

await mockSessionByRole(GlobalRole.PrivateAdmin);

const response = await createPrivateCloudProduct(product);

expect(response.status).toBe(200);

const responseData = await response.json();

expect(responseData.decisionData.resourceRequests.development.gpu).toBe(4);
});
});
3 changes: 3 additions & 0 deletions app/app/api/private-cloud/products/_operations/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import prisma from '@/core/prisma';
import { OkResponse, UnauthorizedResponse } from '@/core/responses';
import generateLicencePlate from '@/helpers/licence-plate';
import { sendRequestNatsMessage } from '@/helpers/nats-message';
import { canManageGpuQuota, sanitizeGpuResourceRequests } from '@/helpers/quota-change';
import { DecisionStatus, ProjectStatus, RequestType, EventType, TaskType, Cluster } from '@/prisma/client';
import { sendCreateRequestEmails, sendRequestApprovalEmails } from '@/services/ches/private-cloud';
import { createEvent, models, privateCloudRequestDetailInclude, tasks } from '@/services/db';
Expand Down Expand Up @@ -44,6 +45,8 @@ export default async function createOp({ session, body }: { session: Session; bo
} = body;

if (rest.cluster === Cluster.GOLDDR) rest.cluster = Cluster.GOLD;
const canManageGpu = canManageGpuQuota(session);
rest.resourceRequests = sanitizeGpuResourceRequests(rest.resourceRequests, rest.cluster, canManageGpu);

const productData = {
...rest,
Expand Down
133 changes: 133 additions & 0 deletions app/app/api/private-cloud/products/_operations/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,38 @@ async function makeBasicProductChange(extra = {}) {
return response;
}

async function createAndProvisionProduct(cluster: Cluster) {
const product = createSamplePrivateCloudProductData({
data: {
cluster,
resourceRequests: resourceRequests1,
},
});

await mockSessionByIdirGuid(product.projectOwner.idirGuid);

const createResponse = await createPrivateCloudProduct(product);
expect(createResponse.status).toBe(200);

const request = await createResponse.json();

await mockSessionByRole(GlobalRole.PrivateReviewer);

const approvalResponse = await makePrivateCloudRequestDecision(request.id, {
...request.decisionData,
type: RequestType.CREATE,
decision: DecisionStatus.APPROVED,
});
expect(approvalResponse.status).toBe(200);

await mockTeamServiceAccount(['private-admin']);

const provisionResponse = await provisionPrivateCloudProduct(request.licencePlate);
expect(provisionResponse.status).toBe(200);

return { product, request };
}

describe('Update Private Cloud Product - Permissions', () => {
it('should successfully submit a create request for PO', async () => {
await mockSessionByIdirGuid(productData.main.projectOwner.idirGuid);
Expand Down Expand Up @@ -254,4 +286,105 @@ describe('Update Private Cloud Product - Validations', () => {
const response = await makeBasicProductChange({ secondaryTechnicalLeadId: null });
expect(response.status).toBe(200);
});

it('should reset GPU quota to 0 for unsupported clusters on update', async () => {
const { request } = await createAndProvisionProduct(Cluster.SILVER);

await mockSessionByRole(GlobalRole.Admin);

const response = await editPrivateCloudProduct(request.licencePlate, {
...request.decisionData,
isAgMinistry: request.project?.organization.isAgMinistry || false,
resourceRequests: {
...request.decisionData.resourceRequests,
development: {
...request.decisionData.resourceRequests.development,
gpu: 4,
},
},
});

expect(response.status).toBe(200);

const responseData = await response.json();

expect(responseData.decisionData.resourceRequests.development.gpu).toBe(0);
});

it('should preserve existing GPU quota when regular user updates other fields on Emerald', async () => {
const product = createSamplePrivateCloudProductData({
data: {
cluster: Cluster.EMERALD,
resourceRequests: {
...resourceRequests1,
development: {
...resourceRequests1.development,
gpu: 4,
},
},
},
});

await mockSessionByRole(GlobalRole.Admin);

const createResponse = await createPrivateCloudProduct(product);
expect(createResponse.status).toBe(200);

const request = await createResponse.json();

await mockSessionByRole(GlobalRole.PrivateReviewer);

const approvalResponse = await makePrivateCloudRequestDecision(request.id, {
...request.decisionData,
type: RequestType.CREATE,
decision: DecisionStatus.APPROVED,
});

expect(approvalResponse.status).toBe(200);

await mockTeamServiceAccount(['private-admin']);

const provisionResponse = await provisionPrivateCloudProduct(request.licencePlate);
expect(provisionResponse.status).toBe(200);

await mockSessionByIdirGuid(product.primaryTechnicalLead.idirGuid);

const response = await editPrivateCloudProduct(request.licencePlate, {
...request.decisionData,
description: `${request.decisionData.description} updated`,
isAgMinistry: request.project?.organization.isAgMinistry || false,
resourceRequests: request.decisionData.resourceRequests,
});

expect(response.status).toBe(200);

const responseData = await response.json();

expect(responseData.decisionData.resourceRequests.development.gpu).toBe(4);
});

it('should preserve GPU quota for admin on Emerald', async () => {
const product = createSamplePrivateCloudProductData({
data: {
cluster: Cluster.EMERALD,
resourceRequests: {
...resourceRequests1,
development: {
...resourceRequests1.development,
gpu: 4,
},
},
},
});

await mockSessionByRole(GlobalRole.Admin);

const response = await createPrivateCloudProduct(product);

expect(response.status).toBe(200);

const responseData = await response.json();

expect(responseData.decisionData.resourceRequests.development.gpu).toBe(4);
});
});
10 changes: 9 additions & 1 deletion app/app/api/private-cloud/products/_operations/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { OkResponse, UnauthorizedResponse } from '@/core/responses';
import { getQuotaChangeStatus } from '@/helpers/auto-approval-check';
import { sendRequestNatsMessage } from '@/helpers/nats-message';
import { comparePrivateProductData } from '@/helpers/product-change';
import { canManageGpuQuota, sanitizeGpuResourceRequests } from '@/helpers/quota-change';
import { DecisionStatus, Cluster, RequestType, EventType, TaskType } from '@/prisma/client';
import { sendEditRequestEmails, sendRequestApprovalEmails } from '@/services/ches/private-cloud';
import {
Expand Down Expand Up @@ -52,6 +53,13 @@ export default async function updateOp({
if (!product._permissions.manageMembers) {
rest.members = product.members.map(({ userId, roles }) => ({ userId, roles }));
}
const canManageGpu = canManageGpuQuota(session);
rest.resourceRequests = sanitizeGpuResourceRequests(
rest.resourceRequests,
product.cluster,
canManageGpu,
product.resourceRequests,
);

const productData = {
...rest,
Expand All @@ -73,7 +81,7 @@ export default async function updateOp({
licencePlate: product.licencePlate,
cluster: product.cluster,
currentResourceRequests: product.resourceRequests,
requestedResourceRequests: body.resourceRequests,
requestedResourceRequests: rest.resourceRequests,
});

// If there is no quota change or no quota upgrade and no golddr flag changes, the request is automatically approved
Expand Down
Loading
Loading