Skip to content

Move /global-images/ to live under /system/ as per RFD-288 #1678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 20, 2022
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
6 changes: 3 additions & 3 deletions docs/debugging-authz.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ In this example, we found _no_ roles for this user on the resource we were autho
"pid": 12898,
"actor": "001de000-05e4-4000-8000-000000004007",
"authenticated": true,
"uri": "/images/alpine-edge",
"uri": "/system/images/alpine-edge",
"method": "GET",
"req_id": "c37ebae6-2252-4cd1-a9d5-73462522d56a",
"remote_addr": "127.0.0.1:36233",
Expand Down Expand Up @@ -634,7 +634,7 @@ $ grep 'full Oso configuration' log | bunyan
#
# The resources here do not correspond to anything that appears explicitly in
# the API or is stored in the database. These are used either at the top level
# of the API path (e.g., "/images") or as an implementation detail of the system
# of the API path (e.g., "/system/images") or as an implementation detail of the system
# (in the case of console sessions and "Database"). The policies are
# either statically-defined in this file or driven by role assignments on the
# Fleet. None of these resources defines their own roles.
Expand All @@ -659,7 +659,7 @@ $ grep 'full Oso configuration' log | bunyan
has_relation(fleet: Fleet, "parent_fleet", ip_pool_list: IpPoolList)
if ip_pool_list.fleet = fleet;

# Describes the policy for accessing "/images" (in the API)
# Describes the policy for accessing "/system/images" (in the API)
resource GlobalImageList {
permissions = [
"list_children",
Expand Down
6 changes: 3 additions & 3 deletions docs/how-to-run.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ the exact addresses that are available depends on the environment.

a. This can be the alpine.iso image that ships with propolis:

oxide api /images --method POST --input - <<EOF
oxide api /system/images --method POST --input - <<EOF
{
"name": "alpine",
"description": "boot from propolis zone blob!",
Expand All @@ -241,7 +241,7 @@ the exact addresses that are available depends on the environment.

b. Or an ISO / raw disk image / etc hosted at a URL:

oxide api /images --method POST --input - <<EOF
oxide api /system/images --method POST --input - <<EOF
{
"name": "crucible-tester-sparse",
"description": "boot from a url!",
Expand All @@ -267,7 +267,7 @@ the exact addresses that are available depends on the environment.
"size": 1073741824,
"disk_source": {
"type": "global_image",
"image_id": "$(oxide api /images/alpine | jq -r .id)"
"image_id": "$(oxide api /system/images/alpine | jq -r .id)"
}
}
EOF
Expand Down
4 changes: 2 additions & 2 deletions nexus/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,11 +469,11 @@ impl Nexus {
/// For unimplemented endpoints, indicates whether the resource identified
/// by this endpoint will always be publicly visible or not
///
/// For example, the resource "/images" is well-known (it's part of the
/// For example, the resource "/system/images" is well-known (it's part of the
/// API). Being unauthorized to list images will result in a "403
/// Forbidden". It's `UnimplResourceVisibility::Public'.
///
/// By contrast, the resource "/images/some-image" is not publicly-known.
/// By contrast, the resource "/system/images/some-image" is not publicly-known.
/// If you're not authorized to view it, you'll get a "404 Not Found". It's
/// `Unimpl::ProtectedLookup(LookupType::ByName("some-image"))`.
pub enum Unimpl {
Expand Down
4 changes: 2 additions & 2 deletions nexus/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ has_relation(silo: Silo, "parent_silo", saml_identity_provider: SamlIdentityProv
#
# The resources here do not correspond to anything that appears explicitly in
# the API or is stored in the database. These are used either at the top level
# of the API path (e.g., "/images") or as an implementation detail of the system
# of the API path (e.g., "/system/images") or as an implementation detail of the system
# (in the case of console sessions and "Database"). The policies are
# either statically-defined in this file or driven by role assignments on the
# Fleet. None of these resources defines their own roles.
Expand All @@ -347,7 +347,7 @@ resource IpPoolList {
has_relation(fleet: Fleet, "parent_fleet", ip_pool_list: IpPoolList)
if ip_pool_list.fleet = fleet;

# Describes the policy for accessing "/images" (in the API)
# Describes the policy for accessing "/system/images" (in the API)
resource GlobalImageList {
permissions = [
"list_children",
Expand Down
60 changes: 30 additions & 30 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,11 @@ pub fn external_api() -> NexusApiDescription {
api.register(silo_identity_provider_create)?;
api.register(silo_identity_provider_view)?;

api.register(image_global_list)?;
api.register(image_global_create)?;
api.register(image_global_view)?;
api.register(image_global_view_by_id)?;
api.register(image_global_delete)?;
api.register(system_image_list)?;
api.register(system_image_create)?;
api.register(system_image_view)?;
api.register(system_image_view_by_id)?;
api.register(system_image_delete)?;
Comment on lines +237 to +241
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need some way to differentiate this, but I do wonder if this prefix is correct given that we're not using prefixes for other things.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Punt. I think it's tolerable.


api.register(updates_refresh)?;
api.register(user_list)?;
Expand Down Expand Up @@ -2243,16 +2243,16 @@ async fn instance_disk_detach(

// Images

/// List global images
/// List system-wide images
///
/// Returns a list of all the global images. Global images are returned sorted
/// Returns a list of all the system-wide images. System-wide images are returned sorted
/// by creation date, with the most recent images appearing first.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this comment still make sense? I'm wary of saying something like List system images given @kc8apf's comment on system images being a distinct thing that would be confusing to be conflated with global images. I could update it to say List system-wide images which does a better job of capturing the intent.

#[endpoint {
method = GET,
path = "/images",
tags = ["images:global"],
path = "/system/images",
tags = ["system"],
}]
async fn image_global_list(
async fn system_image_list(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
query_params: Query<PaginatedByName>,
) -> Result<HttpResponseOk<ResultsPage<GlobalImage>>, HttpError> {
Expand Down Expand Up @@ -2280,16 +2280,16 @@ async fn image_global_list(
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

/// Create a global image
/// Create a system-wide image
///
/// Create a new global image. This image can then be used by any user as a
/// Create a new system-wide image. This image can then be used by any user in any silo as a
/// base for instances.
#[endpoint {
method = POST,
path = "/images",
tags = ["images:global"]
path = "/system/images",
tags = ["system"]
}]
async fn image_global_create(
async fn system_image_create(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
new_image: TypedBody<params::GlobalImageCreate>,
) -> Result<HttpResponseCreated<GlobalImage>, HttpError> {
Expand All @@ -2310,15 +2310,15 @@ struct GlobalImagePathParam {
image_name: Name,
}

/// Fetch a global image
/// Fetch a system-wide image
///
/// Returns the details of a specific global image.
/// Returns the details of a specific system-wide image.
#[endpoint {
method = GET,
path = "/images/{image_name}",
tags = ["images:global"],
path = "/system/images/{image_name}",
tags = ["system"],
}]
async fn image_global_view(
async fn system_image_view(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
path_params: Path<GlobalImagePathParam>,
) -> Result<HttpResponseOk<GlobalImage>, HttpError> {
Expand All @@ -2334,13 +2334,13 @@ async fn image_global_view(
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

/// Fetch a global image by id
/// Fetch a system-wide image by id
#[endpoint {
method = GET,
path = "/by-id/global-images/{id}",
tags = ["images:global"],
path = "/system/by-id/images/{id}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel too strongly either way but I kind of thought this would be /by-id/system/images. Is that bizarre?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not bizarre, that certainly is a plausible approach. The reason I went with /system being first is because it just treats /by-id/ in this context just like any other route that's being executed outside the context of the silo. /by-id/system/ on the other hand is sort of a special case. The docs would be something like: "All non-silo scoped endpoints grouped under /system unless you want to do an ID lookup then it's /by-id/system" vs "All non-silo scoped endpoints are grouped under system".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning is that it simplifies the algorithm for constructing a by-id path. If it's /by-id/system/images, you can simply say "take whatever the path is and prefix it with /by-id", and that would be true for all (I think) resources. It looks a bit weird, but to me both versions look equally weird, so it being one trivial universal rule sells it for me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yeah I see the other side too. "Why is this system route not prefixed with /system?" Ugh!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly about this but /system/by-id makes a little more sense to me because I think of / and /system as basically two different namespaces, each with a top-level by-id resource. (Yeah, it's a little weird to think of it that way because one is contained in the other. But that's my mental model.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I’m fine with it. We’ll try to make it easy to see in the API docs.

tags = ["system"],
}]
async fn image_global_view_by_id(
async fn system_image_view_by_id(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
path_params: Path<ByIdPathParams>,
) -> Result<HttpResponseOk<GlobalImage>, HttpError> {
Expand All @@ -2356,17 +2356,17 @@ async fn image_global_view_by_id(
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

/// Delete a global image
/// Delete a system-wide image
///
/// Permanently delete a global image. This operation cannot be undone. Any
/// instances using the global image will continue to run, however new instances
/// Permanently delete a system-wide image. This operation cannot be undone. Any
/// instances using the system-wide image will continue to run, however new instances
/// can not be created with this image.
#[endpoint {
method = DELETE,
path = "/images/{image_name}",
tags = ["images:global"],
path = "/system/images/{image_name}",
tags = ["system"],
}]
async fn image_global_delete(
async fn system_image_delete(
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
path_params: Path<GlobalImagePathParam>,
) -> Result<HttpResponseDeleted, HttpError> {
Expand Down
6 changes: 3 additions & 3 deletions nexus/tests/integration_tests/authz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ async fn test_global_image_read_for_unpriv(
block_size: params::BlockSize::try_from(512).unwrap(),
};

NexusRequest::objects_post(client, "/images", &image_create_params)
NexusRequest::objects_post(client, "/system/images", &image_create_params)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
Expand All @@ -180,7 +180,7 @@ async fn test_global_image_read_for_unpriv(

// - can list global images
let _images: ResultsPage<views::GlobalImage> =
NexusRequest::object_get(client, &"/images")
NexusRequest::object_get(client, &"/system/images")
.authn_as(AuthnMode::SiloUser(new_silo_user_id))
.execute()
.await
Expand All @@ -190,7 +190,7 @@ async fn test_global_image_read_for_unpriv(

// - can read a global image
let _image: views::GlobalImage =
NexusRequest::object_get(client, &"/images/alpine-edge")
NexusRequest::object_get(client, &"/system/images/alpine-edge")
.authn_as(AuthnMode::SiloUser(new_silo_user_id))
.execute()
.await
Expand Down
6 changes: 3 additions & 3 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ lazy_static! {
// Global Images
pub static ref DEMO_GLOBAL_IMAGE_NAME: Name = "alpine-edge".parse().unwrap();
pub static ref DEMO_GLOBAL_IMAGE_URL: String =
format!("/images/{}", *DEMO_GLOBAL_IMAGE_NAME);
format!("/system/images/{}", *DEMO_GLOBAL_IMAGE_NAME);
pub static ref DEMO_GLOBAL_IMAGE_CREATE: params::GlobalImageCreate =
params::GlobalImageCreate {
identity: IdentityMetadataCreateParams {
Expand Down Expand Up @@ -1378,7 +1378,7 @@ lazy_static! {
/* Global Images */

VerifyEndpoint {
url: "/images",
url: "/system/images",
visibility: Visibility::Public,
unprivileged_access: UnprivilegedAccess::ReadOnly,
allowed_methods: vec![
Expand All @@ -1390,7 +1390,7 @@ lazy_static! {
},

VerifyEndpoint {
url: "/by-id/global-images/{id}",
url: "/system/by-id/images/{id}",
visibility: Visibility::Protected,
unprivileged_access: UnprivilegedAccess::ReadOnly,
allowed_methods: vec![
Expand Down
52 changes: 29 additions & 23 deletions nexus/tests/integration_tests/images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async fn test_global_image_create(cptestctx: &ControlPlaneTestContext) {

// No global images yet
let global_images: Vec<GlobalImage> =
NexusRequest::iter_collection_authn(client, "/images", "", None)
NexusRequest::iter_collection_authn(client, "/system/images", "", None)
.await
.expect("failed to list images")
.all_items;
Expand All @@ -65,15 +65,15 @@ async fn test_global_image_create(cptestctx: &ControlPlaneTestContext) {
block_size: params::BlockSize::try_from(512).unwrap(),
};

NexusRequest::objects_post(client, "/images", &image_create_params)
NexusRequest::objects_post(client, "/system/images", &image_create_params)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap();

// Verify one global image
let global_images: Vec<GlobalImage> =
NexusRequest::iter_collection_authn(client, "/images", "", None)
NexusRequest::iter_collection_authn(client, "/system/images", "", None)
.await
.expect("failed to list images")
.all_items;
Expand Down Expand Up @@ -112,7 +112,7 @@ async fn test_global_image_create_url_404(cptestctx: &ControlPlaneTestContext) {
};

let error = NexusRequest::new(
RequestBuilder::new(client, Method::POST, &"/images")
RequestBuilder::new(client, Method::POST, &"/system/images")
.body(Some(&image_create_params))
.expect_status(Some(StatusCode::BAD_REQUEST)),
)
Expand Down Expand Up @@ -149,7 +149,7 @@ async fn test_global_image_create_bad_url(cptestctx: &ControlPlaneTestContext) {
};

let error = NexusRequest::new(
RequestBuilder::new(client, Method::POST, &"/images")
RequestBuilder::new(client, Method::POST, &"/system/images")
.body(Some(&image_create_params))
.expect_status(Some(StatusCode::BAD_REQUEST)),
)
Expand Down Expand Up @@ -199,7 +199,7 @@ async fn test_global_image_create_bad_content_length(
};

let error = NexusRequest::new(
RequestBuilder::new(client, Method::POST, &"/images")
RequestBuilder::new(client, Method::POST, &"/system/images")
.body(Some(&image_create_params))
.expect_status(Some(StatusCode::BAD_REQUEST)),
)
Expand Down Expand Up @@ -250,7 +250,7 @@ async fn test_global_image_create_bad_image_size(
};

let error = NexusRequest::new(
RequestBuilder::new(client, Method::POST, &"/images")
RequestBuilder::new(client, Method::POST, &"/system/images")
.body(Some(&image_create_params))
.expect_status(Some(StatusCode::BAD_REQUEST)),
)
Expand Down Expand Up @@ -300,14 +300,17 @@ async fn test_make_disk_from_global_image(cptestctx: &ControlPlaneTestContext) {
block_size: params::BlockSize::try_from(512).unwrap(),
};

let alpine_image: GlobalImage =
NexusRequest::objects_post(client, "/images", &image_create_params)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap()
.parsed_body()
.unwrap();
let alpine_image: GlobalImage = NexusRequest::objects_post(
client,
"/system/images",
&image_create_params,
)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap()
.parsed_body()
.unwrap();

create_organization(&client, "myorg").await;
create_project(client, "myorg", "myproj").await;
Expand Down Expand Up @@ -370,14 +373,17 @@ async fn test_make_disk_from_global_image_too_small(
block_size: params::BlockSize::try_from(512).unwrap(),
};

let alpine_image: GlobalImage =
NexusRequest::objects_post(client, "/images", &image_create_params)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap()
.parsed_body()
.unwrap();
let alpine_image: GlobalImage = NexusRequest::objects_post(
client,
"/system/images",
&image_create_params,
)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap()
.parsed_body()
.unwrap();

create_organization(&client, "myorg").await;
create_project(client, "myorg", "myproj").await;
Expand Down
Loading