@@ -14,7 +14,7 @@ use dropshot::{
1414 Query , RequestContext , ResultsPage , StreamingBody , TypedBody ,
1515 WebsocketChannelResult , WebsocketConnection ,
1616} ;
17- use dropshot_api_manager_types:: ValidationContext ;
17+ use dropshot_api_manager_types:: { ValidationContext , api_versions } ;
1818use http:: Response ;
1919use ipnetwork:: IpNetwork ;
2020use nexus_types:: {
@@ -33,7 +33,48 @@ use omicron_common::api::external::{
3333} ;
3434use openapiv3:: OpenAPI ;
3535
36- pub const API_VERSION : & str = "20251208.0.0" ;
36+ api_versions ! ( [
37+ // API versions are in the format YYYYMMDDNN.0.0, defined below as
38+ // YYYYMMDDNN. Here, NN is a two-digit number starting at 00 for a
39+ // particular date.
40+ //
41+ // WHEN CHANGING THE API (part 1 of 2):
42+ //
43+ // +- First, determine the next API version number to use.
44+ // |
45+ // | * On the main branch: Take today's date in YYYYMMDD format, e.g. 20251112.
46+ // | Find the smallest NN that isn't already defined in the list below. In
47+ // | most cases, that is 00, but if 00 is already taken, use 01, 02, etc.
48+ // |
49+ // | * On a release branch, don't alter the date. Instead, always bump the NN.
50+ // |
51+ // | Duplicate this line, uncomment the *second* copy, update that copy for
52+ // | your new API version, and leave the first copy commented out as an
53+ // | example for the next person.
54+ // |
55+ // | If there's a merge conflict, update the version number to the current
56+ // | date. Otherwise, it is okay to leave the version number unchanged even
57+ // | if you land your change on a different day from the one you make it on.
58+ // |
59+ // | Ensure that version numbers are sorted in descending order. (This macro
60+ // | will panic at runtime if they're not in descending order.) The newest
61+ // | date-based version should be at the top of the list.
62+ // v
63+ // (next_yyyymmddnn, IDENT),
64+ ( 2025112000 , INITIAL ) ,
65+ ] ) ;
66+
67+ // WHEN CHANGING THE API (part 2 of 2):
68+ //
69+ // The call to `api_versions!` above defines constants of type
70+ // `semver::Version` that you can use in your Dropshot API definition to specify
71+ // the version when a particular endpoint was added or removed. For example, if
72+ // you used:
73+ //
74+ // (2025120100, ADD_FOOBAR)
75+ //
76+ // Then you could use `VERSION_ADD_FOOBAR` as the version in which endpoints
77+ // were added or removed.
3778
3879const MIB : usize = 1024 * 1024 ;
3980const GIB : usize = 1024 * MIB ;
@@ -4364,8 +4405,37 @@ pub trait NexusExternalApi {
43644405 ) -> Result < HttpResponseDeleted , HttpError > ;
43654406}
43664407
4367- /// Perform extra validations on the OpenAPI spec.
4408+ /// Perform extra validations on the OpenAPI document, and generate the
4409+ /// nexus_tags.txt file.
43684410pub fn validate_api ( spec : & OpenAPI , mut cx : ValidationContext < ' _ > ) {
4411+ let blessed = cx
4412+ . is_blessed ( )
4413+ . expect ( "this is a versioned API so is_blessed should always be Some" ) ;
4414+
4415+ // There are two parts to this function:
4416+ //
4417+ // 1. Perform validation on the OpenAPI document.
4418+ // 2. Generate the nexus_tags.txt file.
4419+ //
4420+ // Step 1 should only be performed on non-blessed versions. That's because
4421+ // blessed versions are immutable, and if new checks are added in the
4422+ // future, we don't want old API versions to be affected.
4423+ //
4424+ // nexus_tags.txt is unversioned, so step 2 should only be performed on the
4425+ // latest version, whether or not it's blessed.
4426+
4427+ if !blessed {
4428+ validate_api_doc ( spec, & mut cx) ;
4429+ }
4430+
4431+ // nexus_tags.txt is unversioned, so only write it out for the latest
4432+ // version (whether it's blessed or not).
4433+ if cx. is_latest ( ) {
4434+ generate_tags_file ( spec, & mut cx) ;
4435+ }
4436+ }
4437+
4438+ fn validate_api_doc ( spec : & OpenAPI , cx : & mut ValidationContext < ' _ > ) {
43694439 if spec. openapi != "3.0.3" {
43704440 cx. report_error ( anyhow ! (
43714441 "Expected OpenAPI version to be 3.0.3, found {}" ,
@@ -4378,13 +4448,6 @@ pub fn validate_api(spec: &OpenAPI, mut cx: ValidationContext<'_>) {
43784448 spec. info. title,
43794449 ) ) ;
43804450 }
4381- if spec. info . version != API_VERSION {
4382- cx. report_error ( anyhow ! (
4383- "Expected OpenAPI version to be '{}', found '{}'" ,
4384- API_VERSION ,
4385- spec. info. version,
4386- ) ) ;
4387- }
43884451
43894452 // Spot check a couple of items.
43904453 if spec. paths . paths . is_empty ( ) {
@@ -4394,13 +4457,7 @@ pub fn validate_api(spec: &OpenAPI, mut cx: ValidationContext<'_>) {
43944457 cx. report_error ( anyhow ! ( "Expected a path for /v1/projects" ) ) ;
43954458 }
43964459
4397- // Construct a string that helps us identify the organization of tags and
4398- // operations.
4399- let mut ops_by_tag =
4400- BTreeMap :: < String , Vec < ( String , String , String ) > > :: new ( ) ;
4401-
4402- let mut ops_by_tag_valid = true ;
4403- for ( path, method, op) in spec. operations ( ) {
4460+ for ( _path, _method, op) in spec. operations ( ) {
44044461 // Make sure each operation has exactly one tag. Note, we intentionally
44054462 // do this before validating the OpenAPI output as fixing an error here
44064463 // would necessitate refreshing the spec file again.
@@ -4410,8 +4467,6 @@ pub fn validate_api(spec: &OpenAPI, mut cx: ValidationContext<'_>) {
44104467 op. operation_id. as_ref( ) . unwrap( ) ,
44114468 op. tags. len( )
44124469 ) ) ;
4413- ops_by_tag_valid = false ;
4414- continue ;
44154470 }
44164471
44174472 // Every non-hidden endpoint must have a summary
@@ -4421,8 +4476,21 @@ pub fn validate_api(spec: &OpenAPI, mut cx: ValidationContext<'_>) {
44214476 "operation '{}' is missing a summary doc comment" ,
44224477 op. operation_id. as_ref( ) . unwrap( )
44234478 ) ) ;
4424- // This error does not prevent `ops_by_tag` from being populated
4425- // correctly, so we can continue.
4479+ }
4480+ }
4481+ }
4482+
4483+ fn generate_tags_file ( spec : & OpenAPI , cx : & mut ValidationContext < ' _ > ) {
4484+ // Construct a string that helps us identify the organization of tags and
4485+ // operations.
4486+ let mut ops_by_tag =
4487+ BTreeMap :: < String , Vec < ( String , String , String ) > > :: new ( ) ;
4488+
4489+ for ( path, method, op) in spec. operations ( ) {
4490+ // If an operation doesn't have exactly one tag, skip generating the
4491+ // tags file entirely. (Validation above catches this case).
4492+ if op. tags . len ( ) != 1 {
4493+ return ;
44264494 }
44274495
44284496 ops_by_tag
@@ -4435,34 +4503,29 @@ pub fn validate_api(spec: &OpenAPI, mut cx: ValidationContext<'_>) {
44354503 ) ) ;
44364504 }
44374505
4438- if ops_by_tag_valid {
4439- let mut tags = String :: new ( ) ;
4440- for ( tag, mut ops) in ops_by_tag {
4441- ops. sort ( ) ;
4442- tags. push_str ( & format ! (
4443- r#"API operations found with tag "{}""# ,
4444- tag
4445- ) ) ;
4506+ let mut tags = String :: new ( ) ;
4507+ for ( tag, mut ops) in ops_by_tag {
4508+ ops. sort ( ) ;
4509+ tags. push_str ( & format ! ( r#"API operations found with tag "{}""# , tag) ) ;
4510+ tags. push_str ( & format ! (
4511+ "\n {:40} {:8} {}\n " ,
4512+ "OPERATION ID" , "METHOD" , "URL PATH"
4513+ ) ) ;
4514+ for ( operation_id, method, path) in ops {
44464515 tags. push_str ( & format ! (
4447- "\n {:40} {:8} {}\n " ,
4448- "OPERATION ID" , "METHOD" , "URL PATH"
4516+ "{:40} {:8} {}\n " ,
4517+ operation_id , method , path
44494518 ) ) ;
4450- for ( operation_id, method, path) in ops {
4451- tags. push_str ( & format ! (
4452- "{:40} {:8} {}\n " ,
4453- operation_id, method, path
4454- ) ) ;
4455- }
4456- tags. push ( '\n' ) ;
44574519 }
4458-
4459- // When this fails, verify that operations on which you're adding,
4460- // renaming, or changing the tags are what you intend.
4461- cx. record_file_contents (
4462- "nexus/external-api/output/nexus_tags.txt" ,
4463- tags. into_bytes ( ) ,
4464- ) ;
4520+ tags. push ( '\n' ) ;
44654521 }
4522+
4523+ // When this fails, verify that operations on which you're adding,
4524+ // renaming, or changing the tags are what you intend.
4525+ cx. record_file_contents (
4526+ "nexus/external-api/output/nexus_tags.txt" ,
4527+ tags. into_bytes ( ) ,
4528+ ) ;
44664529}
44674530
44684531pub type IpPoolRangePaginationParams =
0 commit comments