Skip to content

Commit 6ff7062

Browse files
authored
Wire up external API for attaching external subnets (#9781)
- Plumb the existing app layer code to the new attach / detach sagas - Add a bunch of integration tests confirming the new API behavior - Closes #9453
1 parent 558f89e commit 6ff7062

File tree

2 files changed

+579
-78
lines changed

2 files changed

+579
-78
lines changed

nexus/src/app/external_subnet.rs

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,21 @@
77
//! External subnets are similar to floating IPs but allocate entire subnets
88
//! rather than individual IP addresses. They can be attached to instances
99
//! to provide external connectivity.
10-
//!
11-
//! TODO(#9453): This module contains stub implementations that return
12-
//! "not implemented" errors. Full implementation requires:
13-
//! - Database schema and models (see nexus/db-model/)
14-
//! - Datastore methods (see nexus/db-queries/src/db/datastore/)
15-
//! - Authorization resources (see nexus/auth/src/authz/)
16-
//! - Sagas for attach/detach operations
17-
//! - Replacing these stubs with real implementations
1810
19-
use crate::app::Unimpl;
11+
use crate::app::sagas::subnet_attach;
12+
use crate::app::sagas::subnet_detach;
13+
use nexus_auth::authn;
2014
use nexus_db_lookup::LookupPath;
2115
use nexus_db_lookup::lookup;
2216
use nexus_db_queries::authz;
2317
use nexus_db_queries::context::OpContext;
2418
use nexus_types::external_api::{params, views};
2519
use omicron_common::api::external::DeleteResult;
2620
use omicron_common::api::external::Error;
21+
use omicron_common::api::external::InternalContext as _;
2722
use omicron_common::api::external::ListResultVec;
2823
use omicron_common::api::external::LookupResult;
29-
use omicron_common::api::external::LookupType;
3024
use omicron_common::api::external::NameOrId;
31-
use omicron_common::api::external::ResourceType;
3225
use omicron_common::api::external::UpdateResult;
3326
use omicron_common::api::external::http_pagination::PaginatedBy;
3427
use omicron_uuid_kinds::ExternalSubnetUuid;
@@ -140,63 +133,86 @@ impl super::Nexus {
140133
self.datastore().delete_external_subnet(opctx, &authz_subnet).await
141134
}
142135

143-
// TODO-remove: This is a temporary method to ensure we continue to fail
144-
// reliably for the methods below that remain unimplemented.
145-
fn external_subnet_lookup_not_found(
146-
&self,
147-
selector: params::ExternalSubnetSelector,
148-
) -> LookupResult<()> {
149-
let lookup_type = match selector {
150-
params::ExternalSubnetSelector {
151-
external_subnet: NameOrId::Id(id),
152-
project: None,
153-
} => LookupType::ById(id),
154-
params::ExternalSubnetSelector {
155-
external_subnet: NameOrId::Name(name),
156-
project: Some(_),
157-
} => LookupType::ByName(name.to_string()),
158-
params::ExternalSubnetSelector {
159-
external_subnet: NameOrId::Id(_),
160-
..
161-
} => {
162-
return Err(Error::invalid_request(
163-
"when providing external subnet as an ID \
164-
project should not be specified",
165-
));
166-
}
167-
_ => {
168-
return Err(Error::invalid_request(
169-
"external subnet should either be a UUID or \
170-
project should be specified",
171-
));
172-
}
173-
};
174-
Err(lookup_type.into_not_found(ResourceType::ExternalSubnet))
175-
}
176-
177136
pub(crate) async fn external_subnet_attach(
178137
&self,
179138
opctx: &OpContext,
180139
selector: params::ExternalSubnetSelector,
181-
_attach: params::ExternalSubnetAttach,
140+
attach: params::ExternalSubnetAttach,
182141
) -> UpdateResult<views::ExternalSubnet> {
183-
let not_found =
184-
self.external_subnet_lookup_not_found(selector).unwrap_err();
185-
Err(self
186-
.unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found))
187-
.await)
142+
let (.., authz_project, authz_subnet, db_subnet) = self
143+
.external_subnet_lookup(opctx, selector)?
144+
.fetch_for(authz::Action::Modify)
145+
.await?;
146+
let instance_selector = match &attach.instance {
147+
NameOrId::Id(id) => params::InstanceSelector {
148+
project: None,
149+
instance: NameOrId::Id(*id),
150+
},
151+
NameOrId::Name(name) => params::InstanceSelector {
152+
project: Some(NameOrId::Id(authz_project.id())),
153+
instance: NameOrId::Name(name.clone()),
154+
},
155+
};
156+
let (.., authz_instance, db_instance) = self
157+
.instance_lookup(opctx, instance_selector)?
158+
.fetch_for(authz::Action::Modify)
159+
.await?;
160+
if db_instance.project_id != db_subnet.project_id {
161+
return Err(Error::invalid_request(
162+
"External subnet and instance must be in the same project",
163+
));
164+
}
165+
let params = subnet_attach::Params {
166+
authz_subnet,
167+
ip_version: db_subnet.subnet.ip_version(),
168+
authz_instance,
169+
serialized_authn: authn::saga::Serialized::for_opctx(opctx),
170+
};
171+
let output = self
172+
.sagas
173+
.saga_execute::<subnet_attach::SagaSubnetAttach>(params)
174+
.await?;
175+
output
176+
.lookup_node_output::<views::ExternalSubnet>("output")
177+
.map_err(|e| Error::internal_error(&format!("{e:#}")))
178+
.internal_context("looking up output from subnet attach saga")
188179
}
189180

190181
pub(crate) async fn external_subnet_detach(
191182
&self,
192183
opctx: &OpContext,
193184
selector: params::ExternalSubnetSelector,
194185
) -> UpdateResult<views::ExternalSubnet> {
195-
let not_found =
196-
self.external_subnet_lookup_not_found(selector).unwrap_err();
197-
Err(self
198-
.unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found))
199-
.await)
186+
let (.., authz_subnet, db_subnet) = self
187+
.external_subnet_lookup(opctx, selector)?
188+
.fetch_for(authz::Action::Modify)
189+
.await?;
190+
let Some(instance_id) = &db_subnet.instance_id else {
191+
return Err(Error::invalid_request(
192+
"External subnet is not attached to an instance",
193+
));
194+
};
195+
let instance_selector = params::InstanceSelector {
196+
project: None,
197+
instance: NameOrId::Id(instance_id.into_untyped_uuid()),
198+
};
199+
let (.., authz_instance, _db_instance) = self
200+
.instance_lookup(opctx, instance_selector)?
201+
.fetch_for(authz::Action::Modify)
202+
.await?;
203+
let params = subnet_detach::Params {
204+
authz_instance,
205+
authz_subnet,
206+
serialized_authn: authn::saga::Serialized::for_opctx(opctx),
207+
};
208+
let output = self
209+
.sagas
210+
.saga_execute::<subnet_detach::SagaSubnetDetach>(params)
211+
.await?;
212+
output
213+
.lookup_node_output::<views::ExternalSubnet>("output")
214+
.map_err(|e| Error::internal_error(&format!("{e:#}")))
215+
.internal_context("looking up output from subnet detach saga")
200216
}
201217

202218
pub(crate) async fn instance_list_external_subnets(

0 commit comments

Comments
 (0)