Skip to content

Commit 7e8eca7

Browse files
david-crespocharliepark
authored andcommitted
InstanceUpdate body: change Option fields to Nullable so they're required to be present (#9046)
Optional fields on a request body can be left out completely from the JSON. In the case of instance update (and probably many other endpoints) this is a problem because passing `null` and passing nothing have the same effect, namely unsetting that field if it is already set in the DB. And because the fields are optional, the generated client types do not enforce that they are present (at least when creating the JSON by hand and also in TypeScript, which distinguishes between optional and nullable at the type level). It is easy to do this by accident: while working on [this](oxidecomputer/console#2900), I [discovered a bug](oxidecomputer/console#2900 (comment)) in the console where we accidentally unset the auto restart policy on an instance when you resize an instance. This PR changes `Option` to `Nullable`, which I added in #8214 to fix this problem: `null` remains a valid value, but the client is no longer allowed to leave that field out of the body. If we don't want to do this, I can live with it because in the console, I have [made a helper](oxidecomputer/console@60ca75e) that enforces that all fields are explicitly present. But I do think the status quo is very error-prone. https://github.com/oxidecomputer/omicron/blob/c6989117c72ecf88d4e3dc8a597d9fe703152a93/common/src/api/external/mod.rs#L3564-L3570
1 parent 9dd995f commit 7e8eca7

File tree

5 files changed

+62
-66
lines changed

5 files changed

+62
-66
lines changed

nexus/src/app/instance.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ impl super::Nexus {
367367

368368
check_instance_cpu_memory_sizes(*ncpus, *memory)?;
369369

370-
let boot_disk_id = match boot_disk {
370+
let boot_disk_id = match boot_disk.as_ref() {
371371
Some(disk) => {
372372
let selector = params::DiskSelector {
373373
project: match &disk {

nexus/tests/integration_tests/endpoints.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -688,9 +688,9 @@ pub static DEMO_STOPPED_INSTANCE_CREATE: LazyLock<params::InstanceCreate> =
688688
});
689689
pub static DEMO_INSTANCE_UPDATE: LazyLock<params::InstanceUpdate> =
690690
LazyLock::new(|| params::InstanceUpdate {
691-
boot_disk: None,
692-
cpu_platform: None,
693-
auto_restart_policy: None,
691+
boot_disk: Nullable(None),
692+
cpu_platform: Nullable(None),
693+
auto_restart_policy: Nullable(None),
694694
ncpus: InstanceCpuCount(1),
695695
memory: ByteCount::from_gibibytes_u32(16),
696696
});

nexus/tests/integration_tests/instances.rs

Lines changed: 50 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ use omicron_common::api::external::InstanceNetworkInterface;
6868
use omicron_common::api::external::InstanceState;
6969
use omicron_common::api::external::Name;
7070
use omicron_common::api::external::NameOrId;
71+
use omicron_common::api::external::Nullable;
7172
use omicron_common::api::external::Vni;
7273
use omicron_common::api::internal::shared::ResolvedVpcRoute;
7374
use omicron_common::api::internal::shared::RouterId;
@@ -4920,9 +4921,9 @@ async fn test_cannot_detach_boot_disk(cptestctx: &ControlPlaneTestContext) {
49204921
&client,
49214922
&instance.identity.id,
49224923
params::InstanceUpdate {
4923-
boot_disk: None,
4924-
auto_restart_policy: None,
4925-
cpu_platform: None,
4924+
boot_disk: Nullable(None),
4925+
auto_restart_policy: Nullable(None),
4926+
cpu_platform: Nullable(None),
49264927
ncpus: InstanceCpuCount::try_from(2).unwrap(),
49274928
memory: ByteCount::from_gibibytes_u32(4),
49284929
},
@@ -5025,9 +5026,9 @@ async fn test_updating_running_instance_boot_disk_is_conflict(
50255026
&client,
50265027
&instance_id.into_untyped_uuid(),
50275028
params::InstanceUpdate {
5028-
boot_disk: Some(alsodata.clone().into()),
5029-
auto_restart_policy: None,
5030-
cpu_platform: None,
5029+
boot_disk: Nullable(Some(alsodata.clone().into())),
5030+
auto_restart_policy: Nullable(None),
5031+
cpu_platform: Nullable(None),
50315032
ncpus: InstanceCpuCount::try_from(2).unwrap(),
50325033
memory: ByteCount::from_gibibytes_u32(4),
50335034
},
@@ -5044,9 +5045,11 @@ async fn test_updating_running_instance_boot_disk_is_conflict(
50445045
params::InstanceUpdate {
50455046
// Leave the boot disk the same as the one with which the instance
50465047
// was created.
5047-
boot_disk: Some(probablydata.clone().into()),
5048-
auto_restart_policy: Some(InstanceAutoRestartPolicy::BestEffort),
5049-
cpu_platform: None,
5048+
boot_disk: Nullable(Some(probablydata.clone().into())),
5049+
auto_restart_policy: Nullable(Some(
5050+
InstanceAutoRestartPolicy::BestEffort,
5051+
)),
5052+
cpu_platform: Nullable(None),
50505053
ncpus: InstanceCpuCount::try_from(2).unwrap(),
50515054
memory: ByteCount::from_gibibytes_u32(4),
50525055
},
@@ -5067,9 +5070,9 @@ async fn test_updating_missing_instance_is_not_found(
50675070
&client,
50685071
&UUID_THAT_DOESNT_EXIST,
50695072
params::InstanceUpdate {
5070-
boot_disk: None,
5071-
auto_restart_policy: None,
5072-
cpu_platform: None,
5073+
boot_disk: Nullable(None),
5074+
auto_restart_policy: Nullable(None),
5075+
cpu_platform: Nullable(None),
50735076
ncpus: InstanceCpuCount::try_from(0).unwrap(),
50745077
memory: ByteCount::from_gibibytes_u32(0),
50755078
},
@@ -5185,16 +5188,22 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) {
51855188
let new_ncpus = InstanceCpuCount::try_from(4).unwrap();
51865189
let new_memory = ByteCount::from_gibibytes_u32(8);
51875190

5191+
let base_update = params::InstanceUpdate {
5192+
auto_restart_policy: Nullable(auto_restart_policy),
5193+
boot_disk: Nullable(boot_disk_nameorid.clone()),
5194+
cpu_platform: Nullable(None),
5195+
ncpus: initial_ncpus,
5196+
memory: initial_memory,
5197+
};
5198+
51885199
// Resizing the instance immediately will error; the instance is running.
51895200
let err = expect_instance_reconfigure_err(
51905201
client,
51915202
&instance.identity.id,
51925203
params::InstanceUpdate {
5193-
auto_restart_policy,
5194-
boot_disk: boot_disk_nameorid.clone(),
5195-
cpu_platform: None,
51965204
ncpus: new_ncpus,
51975205
memory: new_memory,
5206+
..base_update.clone()
51985207
},
51995208
StatusCode::CONFLICT,
52005209
)
@@ -5213,11 +5222,9 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) {
52135222
client,
52145223
&instance.identity.id,
52155224
params::InstanceUpdate {
5216-
auto_restart_policy,
5217-
boot_disk: boot_disk_nameorid.clone(),
5218-
cpu_platform: None,
52195225
ncpus: new_ncpus,
52205226
memory: new_memory,
5227+
..base_update.clone()
52215228
},
52225229
)
52235230
.await;
@@ -5229,11 +5236,9 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) {
52295236
client,
52305237
&instance.identity.id,
52315238
params::InstanceUpdate {
5232-
auto_restart_policy,
5233-
boot_disk: boot_disk_nameorid.clone(),
5234-
cpu_platform: None,
52355239
ncpus: initial_ncpus,
52365240
memory: new_memory,
5241+
..base_update.clone()
52375242
},
52385243
)
52395244
.await;
@@ -5244,11 +5249,9 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) {
52445249
client,
52455250
&instance.identity.id,
52465251
params::InstanceUpdate {
5247-
auto_restart_policy,
5248-
boot_disk: boot_disk_nameorid.clone(),
5249-
cpu_platform: None,
52505252
ncpus: initial_ncpus,
52515253
memory: initial_memory,
5254+
..base_update.clone()
52525255
},
52535256
)
52545257
.await;
@@ -5263,11 +5266,9 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) {
52635266
client,
52645267
&instance.identity.id,
52655268
params::InstanceUpdate {
5266-
auto_restart_policy,
5267-
boot_disk: boot_disk_nameorid.clone(),
5268-
cpu_platform: None,
52695269
ncpus: InstanceCpuCount(MAX_VCPU_PER_INSTANCE + 1),
52705270
memory: instance.memory,
5271+
..base_update.clone()
52715272
},
52725273
StatusCode::BAD_REQUEST,
52735274
)
@@ -5285,11 +5286,9 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) {
52855286
client,
52865287
&instance.identity.id,
52875288
params::InstanceUpdate {
5288-
auto_restart_policy,
5289-
boot_disk: boot_disk_nameorid.clone(),
5290-
cpu_platform: None,
52915289
ncpus: instance.ncpus,
52925290
memory: ByteCount::from_mebibytes_u32(0),
5291+
..base_update.clone()
52935292
},
52945293
StatusCode::BAD_REQUEST,
52955294
)
@@ -5301,12 +5300,10 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) {
53015300
client,
53025301
&instance.identity.id,
53035302
params::InstanceUpdate {
5304-
auto_restart_policy,
5305-
boot_disk: boot_disk_nameorid.clone(),
5306-
cpu_platform: None,
53075303
ncpus: instance.ncpus,
53085304
memory: ByteCount::try_from(MAX_MEMORY_BYTES_PER_INSTANCE - 1)
53095305
.unwrap(),
5306+
..base_update.clone()
53105307
},
53115308
StatusCode::BAD_REQUEST,
53125309
)
@@ -5319,13 +5316,11 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) {
53195316
client,
53205317
&instance.identity.id,
53215318
params::InstanceUpdate {
5322-
auto_restart_policy,
5323-
boot_disk: boot_disk_nameorid.clone(),
5324-
cpu_platform: None,
53255319
ncpus: instance.ncpus,
53265320
memory: ByteCount::from_mebibytes_u32(
53275321
(max_mib + 1024).try_into().unwrap(),
53285322
),
5323+
..base_update.clone()
53295324
},
53305325
StatusCode::BAD_REQUEST,
53315326
)
@@ -5342,11 +5337,9 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) {
53425337
client,
53435338
&instance.identity.id,
53445339
params::InstanceUpdate {
5345-
auto_restart_policy,
5346-
boot_disk: boot_disk_nameorid.clone(),
5347-
cpu_platform: None,
53485340
ncpus: new_ncpus,
53495341
memory: new_memory,
5342+
..base_update.clone()
53505343
},
53515344
StatusCode::NOT_FOUND,
53525345
)
@@ -5404,9 +5397,9 @@ async fn test_auto_restart_policy_can_be_changed(
54045397
client,
54055398
&instance.identity.id,
54065399
dbg!(params::InstanceUpdate {
5407-
auto_restart_policy,
5408-
boot_disk: None,
5409-
cpu_platform: None,
5400+
auto_restart_policy: Nullable(auto_restart_policy),
5401+
boot_disk: Nullable(None),
5402+
cpu_platform: Nullable(None),
54105403
ncpus: InstanceCpuCount::try_from(2).unwrap(),
54115404
memory: ByteCount::from_gibibytes_u32(4),
54125405
}),
@@ -5477,9 +5470,9 @@ async fn test_cpu_platform_can_be_changed(cptestctx: &ControlPlaneTestContext) {
54775470
client,
54785471
&instance.identity.id,
54795472
dbg!(params::InstanceUpdate {
5480-
auto_restart_policy: None,
5481-
boot_disk: None,
5482-
cpu_platform,
5473+
auto_restart_policy: Nullable(None),
5474+
boot_disk: Nullable(None),
5475+
cpu_platform: Nullable(cpu_platform),
54835476
ncpus: InstanceCpuCount::try_from(2).unwrap(),
54845477
memory: ByteCount::from_gibibytes_u32(4),
54855478
}),
@@ -5572,9 +5565,9 @@ async fn test_boot_disk_can_be_changed(cptestctx: &ControlPlaneTestContext) {
55725565
&client,
55735566
&instance.identity.id,
55745567
params::InstanceUpdate {
5575-
boot_disk: Some(disks[1].identity.id.into()),
5576-
auto_restart_policy: None,
5577-
cpu_platform: None,
5568+
boot_disk: Nullable(Some(disks[1].identity.id.into())),
5569+
auto_restart_policy: Nullable(None),
5570+
cpu_platform: Nullable(None),
55785571
ncpus: InstanceCpuCount::try_from(2).unwrap(),
55795572
memory: ByteCount::from_gibibytes_u32(4),
55805573
},
@@ -5641,9 +5634,9 @@ async fn test_boot_disk_must_be_attached(cptestctx: &ControlPlaneTestContext) {
56415634
&client,
56425635
&instance.identity.id,
56435636
params::InstanceUpdate {
5644-
boot_disk: Some(disks[0].identity.id.into()),
5645-
auto_restart_policy: None,
5646-
cpu_platform: None,
5637+
boot_disk: Nullable(Some(disks[0].identity.id.into())),
5638+
auto_restart_policy: Nullable(None),
5639+
cpu_platform: Nullable(None),
56475640
ncpus: InstanceCpuCount::try_from(2).unwrap(),
56485641
memory: ByteCount::from_gibibytes_u32(4),
56495642
},
@@ -5675,9 +5668,9 @@ async fn test_boot_disk_must_be_attached(cptestctx: &ControlPlaneTestContext) {
56755668
&client,
56765669
&instance.identity.id,
56775670
params::InstanceUpdate {
5678-
boot_disk: Some(disks[0].identity.id.into()),
5679-
auto_restart_policy: None,
5680-
cpu_platform: None,
5671+
boot_disk: Nullable(Some(disks[0].identity.id.into())),
5672+
auto_restart_policy: Nullable(None),
5673+
cpu_platform: Nullable(None),
56815674
ncpus: InstanceCpuCount::try_from(2).unwrap(),
56825675
memory: ByteCount::from_gibibytes_u32(4),
56835676
},
@@ -6654,9 +6647,9 @@ async fn test_can_start_instance_with_cpu_platform(
66546647
&client,
66556648
&instance.identity.id,
66566649
params::InstanceUpdate {
6657-
boot_disk: None,
6658-
auto_restart_policy: None,
6659-
cpu_platform: Some(InstanceCpuPlatform::AmdTurin),
6650+
boot_disk: Nullable(None),
6651+
auto_restart_policy: Nullable(None),
6652+
cpu_platform: Nullable(Some(InstanceCpuPlatform::AmdTurin)),
66606653
ncpus: InstanceCpuCount::try_from(1).unwrap(),
66616654
memory: ByteCount::from_gibibytes_u32(4),
66626655
},

nexus/types/src/external_api/params.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,8 +1306,8 @@ pub struct InstanceUpdate {
13061306

13071307
/// Name or ID of the disk the instance should be instructed to boot from.
13081308
///
1309-
/// If not provided, unset the instance's boot disk.
1310-
pub boot_disk: Option<NameOrId>,
1309+
/// A null value unsets the boot disk.
1310+
pub boot_disk: Nullable<NameOrId>,
13111311

13121312
/// Sets the auto-restart policy for this instance.
13131313
///
@@ -1323,11 +1323,11 @@ pub struct InstanceUpdate {
13231323
/// configurable through other mechanisms, such as on a per-project basis.
13241324
/// In that case, any configured default policy will be used if this is
13251325
/// `null`.
1326-
pub auto_restart_policy: Option<InstanceAutoRestartPolicy>,
1326+
pub auto_restart_policy: Nullable<InstanceAutoRestartPolicy>,
13271327

13281328
/// The CPU platform to be used for this instance. If this is `null`, the
13291329
/// instance requires no particular CPU platform.
1330-
pub cpu_platform: Option<InstanceCpuPlatform>,
1330+
pub cpu_platform: Nullable<InstanceCpuPlatform>,
13311331
}
13321332

13331333
#[inline]

openapi/nexus.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20842,7 +20842,7 @@
2084220842
},
2084320843
"boot_disk": {
2084420844
"nullable": true,
20845-
"description": "Name or ID of the disk the instance should be instructed to boot from.\n\nIf not provided, unset the instance's boot disk.",
20845+
"description": "Name or ID of the disk the instance should be instructed to boot from.\n\nA null value unsets the boot disk.",
2084620846
"allOf": [
2084720847
{
2084820848
"$ref": "#/components/schemas/NameOrId"
@@ -20876,6 +20876,9 @@
2087620876
}
2087720877
},
2087820878
"required": [
20879+
"auto_restart_policy",
20880+
"boot_disk",
20881+
"cpu_platform",
2087920882
"memory",
2088020883
"ncpus"
2088120884
]

0 commit comments

Comments
 (0)