-
Notifications
You must be signed in to change notification settings - Fork 62
RFD-322: Add /v1/ endpoints for organizations and projects
#2050
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
Conversation
73e0b67 to
599e6eb
Compare
| pub struct ProjectPath { | ||
| pub struct OldProjectPath { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An unfortunate naming conflict, but this'll go away completely once we do #2055
| organization_delete (delete "/organizations/{organization_name}") | ||
| project_delete (delete "/organizations/{organization_name}/projects/{project_name}") | ||
| instance_delete (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") | ||
| instance_view_by_id (get "/by-id/instances/{id}") | ||
| organization_view_by_id (get "/by-id/organizations/{id}") | ||
| project_view_by_id (get "/by-id/projects/{id}") | ||
| login_saml_begin (get "/login/{silo_name}/saml/{provider_name}") | ||
| organization_list (get "/organizations") | ||
| organization_view (get "/organizations/{organization_name}") | ||
| organization_policy_view (get "/organizations/{organization_name}/policy") | ||
| project_list (get "/organizations/{organization_name}/projects") | ||
| project_view (get "/organizations/{organization_name}/projects/{project_name}") | ||
| instance_list (get "/organizations/{organization_name}/projects/{project_name}/instances") | ||
| instance_view (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}") | ||
| instance_serial_console (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console") | ||
| instance_serial_console_stream (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/serial-console/stream") | ||
| project_policy_view (get "/organizations/{organization_name}/projects/{project_name}/policy") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's work I could do here to get this test passing for all the old endpoints but I'm opting not to do that in favor of making process. The authz test is non-trivial and I'm trying to update it as little as possible until I can get a critical mass of APIs converted. I am going through and ensuring the new API endpoints are correctly tested though.
On that note, there is a slight issue in that you can only test and endpoint from the spec once. If you try to verify it more than once it freaks out on you. I don't have the exact error handy but it does this because it "consumes" routes from the spec and if it encounters something its already consumed then that's considered a failure. We actually need to be able to test a route at least twice (name path, id). Anyway, I'll add that to the top level issue to resolve.
| impl From<Name> for NameOrId { | ||
| fn from(name: Name) -> Self { | ||
| NameOrId::Name(name) | ||
| } | ||
| } | ||
|
|
||
| impl From<Uuid> for NameOrId { | ||
| fn from(id: Uuid) -> Self { | ||
| NameOrId::Id(id) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These greatly reduce a lot of conversion noise. We might not need them post v1 migration.
| impl From<Name> for external::NameOrId { | ||
| fn from(name: Name) -> Self { | ||
| Self::Name(name.0) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above
| pub async fn organization_fetch( | ||
| &self, | ||
| opctx: &OpContext, | ||
| organization_name: &Name, | ||
| ) -> LookupResult<db::model::Organization> { | ||
| let (.., db_organization) = LookupPath::new(opctx, &self.db_datastore) | ||
| .organization_name(organization_name) | ||
| .fetch() | ||
| .await?; | ||
| Ok(db_organization) | ||
| } | ||
|
|
||
| pub async fn organization_fetch_by_id( | ||
| &self, | ||
| opctx: &OpContext, | ||
| organization_id: &Uuid, | ||
| ) -> LookupResult<db::model::Organization> { | ||
| let (.., db_organization) = LookupPath::new(opctx, &self.db_datastore) | ||
| .organization_id(*organization_id) | ||
| .fetch() | ||
| .await?; | ||
| Ok(db_organization) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davepacheco you mentioned in #1957 that we could probably just ditch the fetch endpoints now that we're passing around lookups. I've been doing that.
| opctx: &'a OpContext, | ||
| instance_selector: &'a params::InstanceSelector, | ||
| ) -> LookupResult<lookup::Instance<'a>> { | ||
| match instance_selector { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'll notice this selector logic is greatly reduced. It checks the name and ID case of the resource being selected against and delegates selection of sub resources to that resource's lookup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That looks a lot cleaner. The error cases will likely need to be expanded depending on how that gets spec'd. Specifically the over-specification case will want a different error message.
For example:
InstanceSelector { project_selector: Some(project_selector), instance: NameOrId::Id(id) }There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, yeah. @david-crespo mentioned in the RFD how he thought that case shouldn't be an error and I'm still undecided. Should we just ignore that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ideal would be to allow valid over-specifications, but blanket allowing it would allow parameter combinations that contained mismatched parent identifiers. i.e. /{instance}?project={p_id}&organization={o_id} where p_id is not a child of o_id
Solving that would require also solving the id -> id lookup path.
It certainly could be defined via documentation that we ignore excess parameters, though changing that decision would be a breaking change across the API.
I think allowing valid over-specification and disallowing invalid would be ideal, but I do not know what the effort in that is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point. I think disallowing invalid combinations is the lowest common denominator. We could loosen that later and it would be non-breaking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated this in 69ac008 to have expanded error checks. Essentially I just added an extra case for instance and project.
| #[derive(Deserialize, JsonSchema)] | ||
| pub struct OptionalOrganizationSelector { | ||
| #[serde(flatten)] | ||
| pub organization_selector: Option<OrganizationSelector>, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only reason this optional selector has to exist is because dropshot chokes on Query<Option<OrganizationalSelector>>. If that worked we could delete this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The types actually work out fine, but the EXPECTORATE update fails.
Example error
thread 'integration_tests::commands::test_nexus_openapi' panicked at 'expected normal process exit with code 0, got Signaled(6)
process stderr:thread 'main' panicked at 'invalid type Object(
SchemaObject {
metadata: Some(
Metadata {
id: None,
title: Some(
"Nullable_ProjectSelector",
),
description: None,
default: None,
deprecated: false,
read_only: false,
write_only: false,
examples: [],
},
),
instance_type: None,
format: None,
enum_values: None,
const_value: None,
subschemas: Some(
SubschemaValidation {
all_of: Some(
[
Object(
SchemaObject {
metadata: None,
instance_type: None,
format: None,
enum_values: None,
const_value: None,
subschemas: None,
number: None,
string: None,
array: None,
object: None,
reference: Some(
"#/components/schemas/ProjectSelector",
),
extensions: {},
},
),
],
),
any_of: None,
one_of: None,
not: None,
if_schema: None,
then_schema: None,
else_schema: None,
},
),
number: None,
string: None,
array: None,
object: None,
reference: None,
extensions: {
"nullable": Bool(true),
},
},
)', /Users/just-be/.cargo/git/checkouts/dropshot-a4a923d29dccc492/2529126/dropshot/src/handler.rs:932:20
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
', nexus/tests/integration_tests/commands.rs:106:38
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tried converting all of it but you might be able to represent this as:
#[derive(Deserialize, JsonSchema)]
pub struct OrganizationSelector {
pub organization: Option<NameOrId>,
}
#[derive(Deserialize, JsonSchema)]
pub struct ProjectSelector {
#[serde(flatten)]
pub organization_selector: Option<OrganizationSelector>,
pub project: Option<NameOrId>,
}It would mean that the error message in _ => Err(Error::invalid_request(...) would need updating.
I'm also not positive on the open api spec that this will generate, it would need testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true. I made it more explicit because there are cases when we know the user must provide a path param (like the parent resource of a list endpoint) vs when it could be represented by the path. I think the extra code (which can be generated) is worth the trade off of a more correct spec.
| #[derive(Deserialize, JsonSchema)] | ||
| pub struct OrganizationPath { | ||
| pub organization: NameOrId, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
karencfv
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll leave the review of the internals for people who are more acquainted with the code base, but I played around with the progenitor generated client and it works beautifully!!!
Thanks for taking this on, the API is looking great! 🙇♀️
| pub pagination: PaginatedByNameOrId, | ||
| #[serde(flatten)] | ||
| pub organization: OrganizationSelector, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd almost rather define this next to the endpoint handler since that's the only place it's called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I just grouped them together because I'm hoping to macroize it at some point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@david-crespo I thought you were the one who split these out by type rather than by use?
david-crespo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great. A bit noisy, but the noise is extremely straightforward so it's not a big deal. We'll have to see whether macros or openapi-lint is the better approach to avoiding mistakes.
|
All right folks, I'm going to go ahead and merge. Your reviews are still welcome/greatly desired. I'll address any issues in a followup. |
ahl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
adding these as new endpoints rather than modifying existing ones make errors much harder to spot. When are we going to get rid of the non-v1 operations? presumably we'll be changing the operation names to remove v1?
| // let expected_uncovered_endpoints = | ||
| // std::fs::read_to_string("tests/output/uncovered-authz-endpoints.txt") | ||
| // .expect("failed to load file of allowed uncovered endpoints"); | ||
|
|
||
| // TODO: Update this to remove overwrite capabilities | ||
| // See https://github.com/oxidecomputer/expectorate/pull/12 | ||
| assert_contents( | ||
| "tests/output/uncovered-authz-endpoints.txt", | ||
| uncovered_endpoints.as_str(), | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the plan?
| #[serde(flatten)] | ||
| pub organization_selector: Option<OrganizationSelector>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not
pub organization: Option<NameOrId>,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consistency mostly. I'll flatten it down.
| #[derive(Deserialize, JsonSchema)] | ||
| pub struct ProjectSelector { | ||
| #[serde(flatten)] | ||
| pub organization_selector: Option<OrganizationSelector>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just organization: Option<NameOrId>
| pub pagination: PaginatedByNameOrId, | ||
| #[serde(flatten)] | ||
| pub organization: OrganizationSelector, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@david-crespo I thought you were the one who split these out by type rather than by use?
That's true, but it's generally also less disruptive. My goal is to not block folks working on other things while the transition is in progress. The loose order of operations is
You can see (or add) more specifics in the parent issue: #2028
Tentatively, yes. That seems to be the popular opinion. I think that that because we can have |

Changes
v1endpoints added for orgsv1endpoints added for projectsparamsmoduleNameandUuidtoNameOrIdconverters to reduce some of the translation noise