fix(aws): keep the access point id when a removal keeps the saved files
remove_aws_resources cleared access_point_id even without discard_data, though the access point stays; the project could then move hosts and orphan it. The id is now kept, so a later start reuses it, a later removal with "Also remove the access point" deletes it, and moving the project is refused until then (the message and the Host section say so). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
be2825d344
commit
c467308513
3 files changed
+65
-5
No files matched your search
@@ -1230,8 +1230,10 @@ fn validate_aws_update(
|
||||
})
|
||||
{
|
||||
return Err(
|
||||
"This project still has AWS resources (a task, its data's access point or task \
|
||||
definitions) where it runs now; remove the workspace's AWS resources first"
|
||||
"This project still has AWS resources (a task, its saved files' access point or task \
|
||||
definitions) where it runs now; remove the workspace's AWS resources first — \
|
||||
including the access point (\"Also remove the workspace's file-system access \
|
||||
point\"), which stays when the saved files are kept"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
@@ -1691,13 +1693,26 @@ pub async fn remove_aws_resources(
|
||||
ProjectHost::for_project(&project)
|
||||
.remove_aws_resources(&project, &settings, discard_data)
|
||||
.await?;
|
||||
state.projects_store.set_aws_runtime(&project_id, None, None, None)?;
|
||||
state
|
||||
.projects_store
|
||||
.set_aws_runtime(&project_id, None, access_point_after_removal(&project, discard_data), None)?;
|
||||
state
|
||||
.projects_store
|
||||
.get(&project_id)
|
||||
.ok_or_else(|| format!("Project {} not found", project_id))
|
||||
}
|
||||
|
||||
/// The access point a removal leaves stored: kept when the saved files were
|
||||
/// kept (it is still there, a later start reuses it and a later removal
|
||||
/// with `discard_data` deletes it; and while it is stored the project can't
|
||||
/// move away from it — [`validate_aws_update`]), gone when they were not.
|
||||
fn access_point_after_removal(project: &Project, discard_data: bool) -> Option<String> {
|
||||
if discard_data {
|
||||
return None;
|
||||
}
|
||||
project.aws.as_ref().and_then(|a| a.access_point_id.clone())
|
||||
}
|
||||
|
||||
fn check_aws_resources_removable(project: &Project) -> Result<(), String> {
|
||||
if !project.is_aws() {
|
||||
return Err("This project does not run on AWS".into());
|
||||
@@ -2671,6 +2686,30 @@ mod tests {
|
||||
assert!(validate_aws_update(&running, &edited, &hosts).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_removal_that_keeps_the_saved_files_keeps_their_access_point_and_the_project_in_place() {
|
||||
let hosts = vec![host("h1")];
|
||||
let mut stored = aws_project();
|
||||
stored.aws.as_mut().unwrap().access_point_id = Some("fsap-1".into());
|
||||
assert_eq!(access_point_after_removal(&stored, false), Some("fsap-1".into()));
|
||||
assert_eq!(access_point_after_removal(&stored, true), None);
|
||||
// What the store holds after a keep-the-files removal still blocks a move.
|
||||
let mut after = stored.clone();
|
||||
let a = after.aws.as_mut().unwrap();
|
||||
a.task_arn = None;
|
||||
a.task_definition_arn = None;
|
||||
a.access_point_id = access_point_after_removal(&stored, false);
|
||||
let mut local = after.clone();
|
||||
local.host_id = None;
|
||||
local.aws_placement = None;
|
||||
local.aws = None;
|
||||
let e = validate_aws_update(&after, &local, &hosts).unwrap_err();
|
||||
assert!(e.contains("access point"), "{e}");
|
||||
// After a discarding removal it can move.
|
||||
after.aws.as_mut().unwrap().access_point_id = access_point_after_removal(&stored, true);
|
||||
assert!(validate_aws_update(&after, &local, &hosts).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_project_with_aws_resources_cannot_be_moved_off_its_host() {
|
||||
let hosts = vec![host("h1"), host("h2")];
|
||||
|
||||
@@ -165,6 +165,21 @@ describe("HostSection", () => {
|
||||
await waitFor(() => expect(removeAwsResources).toHaveBeenCalledWith(p.id, true));
|
||||
});
|
||||
|
||||
it("keeps a project whose kept files' access point remains on AWS until it is removed", () => {
|
||||
const p = awsProject();
|
||||
p.aws = { ...p.aws!, access_point_id: "fsap-1", task_arn: null, task_definition_arn: null };
|
||||
renderHost(p);
|
||||
const move = screen.getByRole("button", { name: "Run on this machine instead" });
|
||||
expect(move).toHaveAttribute("aria-disabled", "true");
|
||||
expect(move).toHaveAccessibleDescription(/access point is still on AWS/);
|
||||
expect(screen.getByRole("button", { name: "Remove AWS resources…" })).not.toHaveAttribute(
|
||||
"aria-disabled",
|
||||
"true",
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Remove AWS resources…" }));
|
||||
expect(within(screen.getByRole("dialog")).getByText(/access point\s+stays too/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the AWS resource buttons locked while the project is in Error (its task may live)", () => {
|
||||
const p = awsProject({ status: "error" });
|
||||
p.aws = { ...p.aws!, task_arn: "arn:task" };
|
||||
|
||||
@@ -58,6 +58,8 @@ export default function HostSection({ project, save, disabled }: Props) {
|
||||
const saveAws = (patch: Partial<AwsWorkspaceConfig>) => save({ aws: { ...aws, ...patch } });
|
||||
|
||||
const hasResources = !!(aws.task_arn || aws.access_point_id || aws.task_definition_arn);
|
||||
// A removal that kept the saved files keeps their access point stored.
|
||||
const onlyAccessPoint = !!aws.access_point_id && !aws.task_arn && !aws.task_definition_arn;
|
||||
// Not `disabled` (which also allows Error): on AWS, Error can mean a live task.
|
||||
const stopped = project.status === "stopped";
|
||||
|
||||
@@ -343,7 +345,9 @@ export default function HostSection({ project, save, disabled }: Props) {
|
||||
unavailableReason={
|
||||
!stopped
|
||||
? STILL_RUNNING
|
||||
: "Remove the workspace's AWS resources first, so nothing is left running or billed."
|
||||
: onlyAccessPoint
|
||||
? "The saved files' access point is still on AWS. Remove the AWS resources with \u201cAlso remove the workspace\u2019s file-system access point\u201d first."
|
||||
: "Remove the workspace's AWS resources first, so nothing is left running or billed."
|
||||
}
|
||||
onClick={() => void save({ host_id: null, aws_placement: null, aws: null })}
|
||||
>
|
||||
@@ -421,7 +425,9 @@ function RemoveAwsResourcesModal({ project, onClose }: { project: Project; onClo
|
||||
Also remove the workspace’s file-system access point
|
||||
<span className="block text-xs text-[var(--text-secondary)]">
|
||||
The files stay on your own EFS file system (only your workspace role can read
|
||||
them) until an administrator offboards you.
|
||||
them) until an administrator offboards you. Left unchecked, the access point
|
||||
stays too: the next start here reuses it, and the project can’t move to
|
||||
another host until it is removed.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
Reference in new issue
Block a user