@@ -69,7 +69,7 @@ impl BlueprintPlanner {
69
69
return status;
70
70
} ;
71
71
let ( target, parent) = & * loaded;
72
- status. blueprint_id = parent. id ;
72
+ status. parent_id = parent. id ;
73
73
74
74
// Get the inventory most recently seen by the collection
75
75
// background task. The value is `Copy`, so with the deref
@@ -105,7 +105,7 @@ impl BlueprintPlanner {
105
105
}
106
106
} ;
107
107
108
- // Assemble the planning context.
108
+ // Assemble and adjust the planning context.
109
109
let input =
110
110
match PlanningInputFromDb :: assemble ( opctx, & self . datastore ) . await {
111
111
Ok ( input) => input,
@@ -155,72 +155,71 @@ impl BlueprintPlanner {
155
155
156
156
// Compare the new blueprint to its parent.
157
157
let summary = blueprint. diff_since_blueprint ( & parent) ;
158
- if summary. has_changes ( ) {
158
+ if !summary. has_changes ( ) {
159
+ // Blueprint is unchanged, do nothing.
159
160
info ! (
160
161
& opctx. log,
161
- "planning produced new blueprint " ;
162
+ "blueprint unchanged from current target " ;
162
163
"parent_blueprint_id" => %parent. id,
163
- "blueprint_id" => %blueprint. id,
164
164
) ;
165
- status. blueprint_id = blueprint. id ;
165
+ return status;
166
+ }
166
167
167
- // Save it.
168
- match self . datastore . blueprint_insert ( opctx, & blueprint) . await {
169
- Ok ( ( ) ) => ( ) ,
170
- Err ( error) => {
171
- error ! (
172
- & opctx. log,
173
- "can't save blueprint" ;
174
- "error" => %error,
175
- "blueprint_id" => %blueprint. id,
176
- ) ;
177
- status. error = Some ( format ! (
178
- "can't save blueprint {}: {}" ,
179
- blueprint. id, error
180
- ) ) ;
181
- return status;
182
- }
183
- }
168
+ // We have a fresh blueprint.
169
+ info ! (
170
+ & opctx. log,
171
+ "planning produced new blueprint" ;
172
+ "parent_blueprint_id" => %parent. id,
173
+ "blueprint_id" => %blueprint. id,
174
+ ) ;
175
+ status. blueprint_id = Some ( blueprint. id ) ;
184
176
185
- // Make it the current target.
186
- let target = BlueprintTarget {
187
- target_id : blueprint. id ,
188
- enabled : target. enabled ,
189
- time_made_target : Utc :: now ( ) ,
190
- } ;
191
- match self
192
- . datastore
193
- . blueprint_target_set_current ( opctx, target)
194
- . await
195
- {
196
- Ok ( ( ) ) => ( ) ,
197
- Err ( error) => {
198
- warn ! (
199
- & opctx. log,
200
- "can't make blueprint the current target" ;
201
- "error" => %error,
202
- "blueprint_id" => %blueprint. id
203
- ) ;
204
- status. error = Some ( format ! (
205
- "can't make blueprint {} the current target: {}" ,
206
- blueprint. id, error,
207
- ) ) ;
208
- return status;
209
- }
177
+ // Save it.
178
+ match self . datastore . blueprint_insert ( opctx, & blueprint) . await {
179
+ Ok ( ( ) ) => ( ) ,
180
+ Err ( error) => {
181
+ error ! (
182
+ & opctx. log,
183
+ "can't save blueprint" ;
184
+ "error" => %error,
185
+ "blueprint_id" => %blueprint. id,
186
+ ) ;
187
+ status. error = Some ( format ! (
188
+ "can't save blueprint {}: {}" ,
189
+ blueprint. id, error
190
+ ) ) ;
191
+ return status;
210
192
}
193
+ }
211
194
212
- // Notify watchers that we have a new target.
213
- self . tx_blueprint . send_replace ( Some ( Arc :: new ( ( target, blueprint) ) ) ) ;
214
- } else {
215
- // Blueprint is unchanged, do nothing.
216
- info ! (
217
- & opctx. log,
218
- "blueprint unchanged from current target" ;
219
- "parent_blueprint_id" => %parent. id,
220
- ) ;
221
- status. unchanged = true ;
195
+ // Try to make it the current target.
196
+ let target = BlueprintTarget {
197
+ target_id : blueprint. id ,
198
+ enabled : target. enabled ,
199
+ time_made_target : Utc :: now ( ) ,
200
+ } ;
201
+ match self . datastore . blueprint_target_set_current ( opctx, target) . await {
202
+ Ok ( ( ) ) => {
203
+ status. new_target = true ;
204
+ }
205
+ Err ( error) => {
206
+ warn ! (
207
+ & opctx. log,
208
+ "can't make blueprint the current target" ;
209
+ "error" => %error,
210
+ "blueprint_id" => %blueprint. id
211
+ ) ;
212
+ status. error = Some ( format ! (
213
+ "can't make blueprint {} the current target: {}" ,
214
+ blueprint. id, error,
215
+ ) ) ;
216
+ return status;
217
+ }
222
218
}
223
219
220
+ // Notify watchers that we have a new target.
221
+ self . tx_blueprint . send_replace ( Some ( Arc :: new ( ( target, blueprint) ) ) ) ;
222
+
224
223
status
225
224
}
226
225
}
@@ -233,3 +232,100 @@ impl BackgroundTask for BlueprintPlanner {
233
232
Box :: pin ( async move { json ! ( self . plan( opctx) . await ) } )
234
233
}
235
234
}
235
+
236
+ #[ cfg( test) ]
237
+ mod test {
238
+ use super :: * ;
239
+ use crate :: app:: background:: tasks:: blueprint_load:: TargetBlueprintLoader ;
240
+ use crate :: app:: background:: tasks:: inventory_collection:: InventoryCollector ;
241
+ use nexus_test_utils_macros:: nexus_test;
242
+
243
+ type ControlPlaneTestContext =
244
+ nexus_test_utils:: ControlPlaneTestContext < crate :: Server > ;
245
+
246
+ #[ nexus_test( server = crate :: Server ) ]
247
+ async fn test_blueprint_planner ( cptestctx : & ControlPlaneTestContext ) {
248
+ // Set up the test context.
249
+ let nexus = & cptestctx. server . server_context ( ) . nexus ;
250
+ let datastore = nexus. datastore ( ) ;
251
+ let opctx = OpContext :: for_tests (
252
+ cptestctx. logctx . log . clone ( ) ,
253
+ datastore. clone ( ) ,
254
+ ) ;
255
+
256
+ // Spin up the blueprint loader background task.
257
+ let mut loader = TargetBlueprintLoader :: new ( datastore. clone ( ) ) ;
258
+ let mut rx_loader = loader. watcher ( ) ;
259
+ loader. activate ( & opctx) . await ;
260
+ let ( _initial_target, initial_blueprint) = & * rx_loader
261
+ . borrow_and_update ( )
262
+ . clone ( )
263
+ . expect ( "no initial blueprint" ) ;
264
+
265
+ // Spin up the inventory collector background task.
266
+ let resolver = internal_dns_resolver:: Resolver :: new_from_addrs (
267
+ cptestctx. logctx . log . clone ( ) ,
268
+ & [ cptestctx. internal_dns . dns_server . local_address ( ) ] ,
269
+ )
270
+ . unwrap ( ) ;
271
+ let mut collector = InventoryCollector :: new (
272
+ datastore. clone ( ) ,
273
+ resolver. clone ( ) ,
274
+ "test_planner" ,
275
+ 1 ,
276
+ false ,
277
+ ) ;
278
+ let rx_collector = collector. watcher ( ) ;
279
+ collector. activate ( & opctx) . await ;
280
+
281
+ // Finally, spin up the planner background task.
282
+ let mut planner = BlueprintPlanner :: new (
283
+ datastore. clone ( ) ,
284
+ false ,
285
+ rx_collector,
286
+ rx_loader. clone ( ) ,
287
+ ) ;
288
+ let _rx_planner = planner. watcher ( ) ;
289
+
290
+ // On activation, the planner should run successfully and generate
291
+ // a new target blueprint.
292
+ let status = serde_json:: from_value :: < BlueprintPlannerStatus > (
293
+ planner. activate ( & opctx) . await ,
294
+ )
295
+ . unwrap ( ) ;
296
+ assert ! ( !status. disabled) ;
297
+ assert ! ( status. error. is_none( ) ) ;
298
+ assert_eq ! ( status. parent_id, initial_blueprint. id) ;
299
+ let blueprint_id = status. blueprint_id . unwrap ( ) ;
300
+ assert_ne ! ( blueprint_id, initial_blueprint. id) ;
301
+ assert ! ( status. new_target) ;
302
+
303
+ // Load and check the new target blueprint.
304
+ loader. activate ( & opctx) . await ;
305
+ let ( target, blueprint) = & * rx_loader
306
+ . borrow_and_update ( )
307
+ . clone ( )
308
+ . expect ( "failed to load blueprint" ) ;
309
+ assert_eq ! ( target. target_id, blueprint. id) ;
310
+ assert_eq ! ( target. target_id, blueprint_id) ;
311
+ assert ! (
312
+ blueprint. diff_since_blueprint( initial_blueprint) . has_changes( )
313
+ ) ;
314
+
315
+ // Planning again should not change the plan.
316
+ let status = serde_json:: from_value :: < BlueprintPlannerStatus > (
317
+ planner. activate ( & opctx) . await ,
318
+ )
319
+ . unwrap ( ) ;
320
+ assert_eq ! (
321
+ status,
322
+ BlueprintPlannerStatus {
323
+ disabled: false ,
324
+ error: None ,
325
+ parent_id: blueprint_id,
326
+ blueprint_id: None ,
327
+ new_target: false ,
328
+ }
329
+ ) ;
330
+ }
331
+ }
0 commit comments