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:
shadowdaoandClaude Opus 5.5 committed 2026-11-15 13:08:05 -08:00
1 parent be2825d344
commit c467308513
3 files changed
+65 -5

No files matched your search

+42 -3
View File
@@ -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&rsquo;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&rsquo;t move to
another host until it is removed.
</span>
</span>
</label>