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
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,97 @@ object PveManager {
queue.put(s"[user-package] $pkg")
}
}

/**
* Uninstalls a user-installed package from the PVE.
* 1. Prevents deletion of system packages
* 2. Updates user metadata upon success
* 3. Returns status messages
*/
def deletePackages(
cuid: Int,
packageName: String,
pveName: String,
isLocal: Boolean
): List[String] = {
val python = pythonBinPath(cuid, pveName).toAbsolutePath.toString
val metadataPath = cuidDir(cuid, pveName).resolve("user-packages.txt")

if (!Files.exists(Paths.get(python))) {
val msg = s"[PVE][ERR] Python executable not found for PVE: $python"
println(msg)
return List(msg)
}

val trimmedPackageName = packageName.trim
val normalizedPackageName = trimmedPackageName.split("==")(0).trim.toLowerCase

val systemPackages =
if (Files.exists(getSystemPath(isLocal))) {
Files
.readAllLines(getSystemPath(isLocal))
.asScala
.map(_.trim)
.filter(line => line.nonEmpty && !line.startsWith("#"))
.map(line => line.split("==")(0).trim.toLowerCase)
.toSet
} else {
Set[String]()
}

if (systemPackages.contains(normalizedPackageName)) {
return List(
s"[PVE][ERR] $trimmedPackageName is a system package and cannot be deleted."
)
}

try {
val command = Process(
Seq(
python,
"-u",
"-m",
"pip",
"uninstall",
"-y",
trimmedPackageName
),
None,
pipEnv.toSeq: _*
)

val output = scala.collection.mutable.ListBuffer[String]()

val exitCode = command.!(
ProcessLogger(
out => {
println(s"[pip] $out")
output += s"[pip] $out"
},
err => {
System.err.println(s"[pip][ERR] $err")
output += s"[pip][ERR] $err"
}
)
)

if (exitCode == 0) {
val updatedPackages = readPackageFile(metadataPath)
.filterNot(line => line.split("==")(0).trim.toLowerCase == normalizedPackageName)
.sorted

Files.write(metadataPath, updatedPackages.asJava)

output += s"[pip] uninstall($trimmedPackageName) finished with exit code $exitCode"
output += s"[PVE] Uninstalled $trimmedPackageName successfully"
} else {
output += s"[PVE][ERR] Failed to uninstall package: $trimmedPackageName"
}

output.toList
} catch {
case e: Exception =>
List(s"[PVE][ERR] Failed to delete package for cuid=$cuid: ${e.getMessage}")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import javax.ws.rs._
import javax.ws.rs.core.MediaType
import scala.jdk.CollectionConverters._
import java.util
import javax.ws.rs.DELETE
import javax.ws.rs.PathParam
import javax.ws.rs.core.Response

@Path("/pve")
@Consumes(Array(MediaType.APPLICATION_JSON))
Expand Down Expand Up @@ -85,4 +88,29 @@ class PveResource {
def deleteEnvironments(@PathParam("cuId") cuid: Int): Unit = {
PveManager.deleteEnvironments(cuid)
}

// --------------------------------------------------
// Delete User Installed Package
// --------------------------------------------------
@DELETE
@Path("/{cuid}/{pveName}/packages/{packageName}")
def deletePackage(
@PathParam("cuid") cuid: Int,
@PathParam("pveName") pveName: String,
@PathParam("packageName") packageName: String,
@QueryParam("isLocal") isLocal: Boolean
): Response = {
val messages = PveManager.deletePackages(
cuid,
packageName,
pveName,
isLocal
)

if (messages.exists(_.contains("[PVE][ERR]"))) {
Response.status(Response.Status.BAD_REQUEST).entity(messages.asJava).build()
} else {
Response.ok(messages.asJava).build()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,47 @@ class PveResourceSpec extends AnyFlatSpec with Matchers with BeforeAndAfterEach
pve.get.userPackages should contain(packageSpec)
}

"PveManager" should "delete a user package and remove it from the PVE package list" in {
PveManager.createNewPve(testCuid, queue, testPveName, isLocal = true)

val packageName = "colorama"
val packageVersion = "0.4.6"
val packageSpec = s"$packageName==$packageVersion"

queue.clear()

PveManager.installUserPackages(
List(packageSpec),
testCuid,
queue,
testPveName,
isLocal = true
)

PveManager
.getEnvironments(testCuid)
.find(_.pveName == testPveName)
.get
.userPackages should contain(packageSpec)

val deleteLogs = PveManager.deletePackages(
testCuid,
packageName,
testPveName,
isLocal = true
)

deleteLogs.mkString("\n") should not include "[PVE][ERR]"
deleteLogs.mkString("\n") should include(s"[PVE] Uninstalled $packageName successfully")

val pve = PveManager
.getEnvironments(testCuid)
.find(_.pveName == testPveName)

pve should not be empty
pve.get.userPackages should not contain packageSpec
}

"PveManager" should "delete all PVEs for a computing unit" in {
PveManager.createNewPve(testCuid, queue, testPveName, isLocal = true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,17 +554,31 @@
[ngModel]="pkg.version"
[disabled]="true" />
</div>
<button
nz-button
nzType="default"
nzShape="circle"
nzDanger
[ngClass]="{ 'highlighted-btn': pkg.deleteToggle }"
(click)="togglePackageDelete(envIndex, pkg)">
<i
nz-icon
nzType="delete"></i>
</button>
</div>
</div>

<!-- NEW PACKAGES -->
<div class="new-packages-section">
<div
class="user-package-header-row"
class="package-row user-package-header-row"
*ngIf="pve.newPackages.length > 0">
<div class="package-column-label">Package</div>
<label style="visibility: hidden">Op</label>
<div class="package-column-label">Version</div>
<div class="user-package-inputs">
<div class="package-column-label">Package</div>
<div></div>
<div class="package-column-label">Version</div>
<div></div>
</div>
</div>

<div
Expand Down Expand Up @@ -601,6 +615,17 @@
placeholder="Package Version"
[(ngModel)]="pve.newPackages[i].version" />
</div>
<button
nz-button
nzType="default"
nzShape="circle"
nzDanger
[ngClass]="{ 'highlighted-btn': pkg.deleteToggle }"
(click)="togglePackageDelete(envIndex, pkg)">
<i
nz-icon
nzType="delete"></i>
</button>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,14 @@ type PveUserPackageRow = {
name: string;
versionOp?: "==" | ">=" | "<=";
version?: string;
deleteToggle?: boolean;
};

type PveDraft = {
name: string;
userPackages: PveUserPackageRow[];
newPackages: PveUserPackageRow[];
deletingPackages: { name: string; version: string }[];
pipOutput: string;
prettyPipOutput: string;
expanded: boolean;
Expand Down Expand Up @@ -732,14 +734,29 @@ export class ComputingUnitSelectionComponent implements OnInit {

addPackage(index: number): void {
const env = this.pves[index];
env.newPackages.push({ name: "", version: "", versionOp: undefined });
env.newPackages.push({ name: "", version: "", versionOp: undefined, deleteToggle: false });
}

togglePackageDelete(index: number, pkg: PveUserPackageRow): void {
const env = this.pves[index];

pkg.deleteToggle = !pkg.deleteToggle;

const version = pkg.version ?? "";

env.deletingPackages = env.deletingPackages.filter(p => !(p.name === pkg.name && p.version === version));

if (pkg.deleteToggle) {
env.deletingPackages.push({ name: pkg.name, version });
}
Comment thread
SarahAsad23 marked this conversation as resolved.
}

addEnvironment(): void {
this.pves.push({
name: "",
userPackages: [],
newPackages: [],
deletingPackages: [],
pipOutput: "",
prettyPipOutput: "",
expanded: true,
Expand Down Expand Up @@ -776,6 +793,7 @@ export class ComputingUnitSelectionComponent implements OnInit {
name: pve.pveName,
userPackages: this.parsePackageRows(pve.userPackages),
newPackages: [],
deletingPackages: [],
expanded: false,
isInstalling: false,
pipOutput: "",
Expand Down Expand Up @@ -949,14 +967,17 @@ export class ComputingUnitSelectionComponent implements OnInit {
createVirtualEnvironment(index: number): void {
const env = this.pves[index];
const trimmedName = env.name.trim();
const isLocal = this.selectedComputingUnit?.computingUnit.type === "local";

if (!/^[a-zA-Z0-9]+$/.test(trimmedName)) {
this.notificationService.error("Environment name must contain only letters and numbers.");
return;
}

if (env.isLocked) {
this.installUserPackages(index);
this.deleteUserPackages(index, () => {
this.installUserPackages(index);
});
return;
}

Expand All @@ -968,7 +989,9 @@ export class ComputingUnitSelectionComponent implements OnInit {
}

this.runPveWebSocket(index, "create", "Creating virtual environment...\n", [], () => {
this.installUserPackages(index);
this.deleteUserPackages(index, () => {
this.installUserPackages(index);
});
});
}

Expand Down Expand Up @@ -1019,6 +1042,7 @@ export class ComputingUnitSelectionComponent implements OnInit {

if (packageArray.length === 0) {
this.pves[index].newPackages = [];
this.pves[index].isInstalling = false;
this.refreshUserPackages(index);
return;
}
Expand All @@ -1039,4 +1063,62 @@ export class ComputingUnitSelectionComponent implements OnInit {
};
});
}

private deleteUserPackages(index: number, onDone?: () => void): void {
const cuId = this.selectedComputingUnit!.computingUnit.cuid;
const isLocal = this.selectedComputingUnit?.computingUnit.type === "local";
const pveName = this.pves[index].name.trim();
const packagesToDelete = [...this.pves[index].deletingPackages];

if (packagesToDelete.length === 0) {
onDone?.();
return;
}

this.pves[index] = {
...this.pves[index],
pipOutput: `${this.pves[index].pipOutput ?? ""}Deleting user packages...\n`,
isInstalling: true,
};

let deleteIndex = 0;

const deleteNext = (): void => {
if (deleteIndex >= packagesToDelete.length) {
this.pves[index].deletingPackages = [];
this.refreshUserPackages(index);
onDone?.();
return;
}

const pkg = packagesToDelete[deleteIndex];

this.workflowPveService
.deletePackage(cuId, pveName, pkg.name, isLocal)
.pipe(untilDestroyed(this))
.subscribe({
next: messages => {
this.pves[index].pipOutput = `${this.pves[index].pipOutput ?? ""}${messages.join("\n")}\n`;

this.updatePrettyPipOutput(index);
this.scrollToBottomOfPipModal(index);

deleteIndex++;
deleteNext();
},
error: () => {
this.pves[index].pipOutput =
`${this.pves[index].pipOutput ?? ""}[PVE][ERR] Failed to delete package: ${pkg.name}\n`;

this.updatePrettyPipOutput(index);
this.scrollToBottomOfPipModal(index);

deleteIndex++;
deleteNext();
},
});
};

deleteNext();
}
}
Loading
Loading