1- use std:: { collections:: HashMap , path:: Path } ;
1+ use std:: { collections:: HashMap , path:: Path , sync :: Arc } ;
22
33use anyhow:: Context ;
44use spin_common:: ui:: quoted_path;
@@ -10,6 +10,8 @@ mod lockfile;
1010
1111use definition:: WorldName ;
1212
13+ use crate :: Targets ;
14+
1315/// A fully realised deployment environment, e.g. Spin 2.7,
1416/// SpinKube 3.1, Fermyon Cloud. The `TargetEnvironment` provides a mapping
1517/// from the Spin trigger types supported in the environment to the Component Model worlds
@@ -23,18 +25,47 @@ pub struct TargetEnvironment {
2325 unknown_capabilities : Vec < String > ,
2426}
2527
28+ pub ( crate ) struct RealisedTargets {
29+ default : Vec < Arc < TargetEnvironment > > ,
30+ overrides : HashMap < String , Vec < Arc < TargetEnvironment > > > ,
31+ }
32+
33+ impl RealisedTargets {
34+ pub fn iter ( & self ) -> impl Iterator < Item = & Arc < TargetEnvironment > > {
35+ self . default
36+ . iter ( )
37+ . chain ( self . overrides . values ( ) . flat_map ( |v| v. iter ( ) ) )
38+ }
39+
40+ pub fn get ( & self , component_id : & str ) -> & [ Arc < TargetEnvironment > ] {
41+ self . overrides . get ( component_id) . unwrap_or ( & self . default )
42+ }
43+ }
44+
2645impl TargetEnvironment {
2746 /// Loads the specified list of environments. This fetches all required
2847 /// environment definitions from their references, and then chases packages
2948 /// references until the entire target environment is fully loaded.
3049 /// The function also caches registry references in the application directory,
3150 /// to avoid loading from the network when the app is validated again.
32- pub async fn load_all (
33- env_ids : & [ TargetEnvironmentRef ] ,
51+ pub async fn load_all < ' a > (
52+ targets : Targets < ' a > ,
3453 cache_root : Option < std:: path:: PathBuf > ,
3554 app_dir : & std:: path:: Path ,
36- ) -> anyhow:: Result < Vec < Self > > {
37- env_loader:: load_environments ( env_ids, cache_root, app_dir) . await
55+ ) -> anyhow:: Result < RealisedTargets > {
56+ let env_ids = targets. all_refs ( ) ;
57+ let envs = env_loader:: load_environments ( & env_ids, cache_root, app_dir) . await ?;
58+
59+ let env = |id : & TargetEnvironmentRef | envs[ id] . clone ( ) ;
60+
61+ let default = targets. default . iter ( ) . map ( env) . collect ( ) ;
62+ let overrides = targets
63+ . overrides
64+ . iter ( )
65+ . map ( |( c, ids) | ( c. clone ( ) , ids. iter ( ) . map ( env) . collect ( ) ) )
66+ . collect ( ) ;
67+
68+ Ok ( RealisedTargets { default, overrides } )
3869 }
3970
4071 /// The environment name for UI purposes
@@ -236,32 +267,54 @@ mod test {
236267 }
237268
238269 /// Build an environment using the given WIT that maps the "s" trigger
239- /// to the "spin:test/simple@1.0.0" world (and denies all other triggers).
240- fn target_simple_world ( wit_path : & Path ) -> TargetEnvironment {
241- let candidate_worlds = load_simple_world ( wit_path, "spin:test/simple@1.0.0" ) ;
270+ /// to the given world (and denies all other triggers).
271+ fn target_world_unarced ( env_name : & str , wit_path : & Path , world : & str ) -> TargetEnvironment {
272+ let candidate_worlds = load_simple_world ( wit_path, world ) ;
242273
243274 TargetEnvironment {
244- name : "test" . to_owned ( ) ,
275+ name : env_name . to_owned ( ) ,
245276 trigger_worlds : [ ( "s" . to_owned ( ) , candidate_worlds) ] . into_iter ( ) . collect ( ) ,
246277 trigger_capabilities : Default :: default ( ) ,
247278 unknown_trigger : UnknownTrigger :: Deny ,
248279 unknown_capabilities : Default :: default ( ) ,
249280 }
250281 }
251282
283+ /// Build an environment using the given WIT that maps the "s" trigger
284+ /// to the "spin:test/simple@1.0.0" world (and denies all other triggers).
285+ fn target_simple_world_unarced ( wit_path : & Path ) -> TargetEnvironment {
286+ target_world_unarced ( "test" , wit_path, "spin:test/simple@1.0.0" )
287+ }
288+
289+ /// Build an environment using the given WIT that maps the "s" trigger
290+ /// to the "spin:test/simple@1.0.0" world (and denies all other triggers).
291+ fn target_simple_world ( wit_path : & Path ) -> Arc < TargetEnvironment > {
292+ Arc :: new ( target_simple_world_unarced ( wit_path) )
293+ }
294+
295+ /// Build an environment using the given WIT that maps the "s" trigger
296+ /// to the "spin:test/not-so-simple@1.0.0" world (and denies all other triggers).
297+ fn target_not_so_simple_world ( wit_path : & Path ) -> Arc < TargetEnvironment > {
298+ Arc :: new ( target_world_unarced (
299+ "test-nss" ,
300+ wit_path,
301+ "spin:test/not-so-simple@1.0.0" ,
302+ ) )
303+ }
304+
252305 /// Build an environment using the given WIT that maps all triggers to
253306 /// the "spin:test/simple-import-only@1.0.0" world. (This isn't a very realistic example
254307 /// because a fallback world would usually be imports-only.)
255- fn target_import_only_forgiving ( wit_path : & Path ) -> TargetEnvironment {
308+ fn target_import_only_forgiving ( wit_path : & Path ) -> Arc < TargetEnvironment > {
256309 let candidate_worlds = load_simple_world ( wit_path, "spin:test/simple-import-only@1.0.0" ) ;
257310
258- TargetEnvironment {
311+ Arc :: new ( TargetEnvironment {
259312 name : "test" . to_owned ( ) ,
260313 trigger_worlds : [ ] . into_iter ( ) . collect ( ) ,
261314 trigger_capabilities : Default :: default ( ) ,
262315 unknown_trigger : UnknownTrigger :: Allow ( candidate_worlds) ,
263316 unknown_capabilities : Default :: default ( ) ,
264- }
317+ } )
265318 }
266319
267320 #[ tokio:: test]
@@ -292,6 +345,146 @@ mod test {
292345 ) ;
293346 }
294347
348+ #[ tokio:: test]
349+ async fn can_override_validation_at_component_level ( ) {
350+ let wit_path = PathBuf :: from ( SIMPLE_WIT_DIR ) ;
351+
352+ let wit_text = tokio:: fs:: read_to_string ( wit_path. join ( "world.wit" ) )
353+ . await
354+ . unwrap ( ) ;
355+ let wasm1 = generate_dummy_component ( & wit_text, "spin:test/simple@1.0.0" ) ;
356+ let wasm2 = generate_dummy_component ( & wit_text, "spin:test/not-so-simple@1.0.0" ) ;
357+
358+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
359+ std:: fs:: write ( temp_dir. path ( ) . join ( "wasm1" ) , wasm1) . unwrap ( ) ;
360+ std:: fs:: write ( temp_dir. path ( ) . join ( "wasm2" ) , wasm2) . unwrap ( ) ;
361+
362+ let env1 = target_simple_world ( & wit_path) ;
363+ let env2 = target_not_so_simple_world ( & wit_path) ;
364+
365+ // This would normally be derived from the manifest
366+ let targets = RealisedTargets {
367+ default : vec ! [ env1] ,
368+ overrides : [ ( "nss" . to_string ( ) , vec ! [ env2] ) ] . into_iter ( ) . collect ( ) ,
369+ } ;
370+
371+ let manifest = spin_manifest:: manifest_from_str (
372+ r#"
373+ spin_manifest_version = 2
374+
375+ [application]
376+ name = "test"
377+ targets = ["test"] # default: maps to env1 as per `targets`
378+
379+ [[trigger.s]]
380+ route = "/1"
381+ component = { source = "wasm1" }
382+
383+ [[trigger.s]]
384+ route = "/2"
385+ component = "nss"
386+
387+ [component.nss]
388+ source = "wasm2"
389+ targets = ["test-nss"] # override: maps to env2 as per `targets`
390+ "# ,
391+ )
392+ . unwrap ( ) ;
393+
394+ let application = crate :: ApplicationToValidate :: new ( manifest, temp_dir. path ( ) )
395+ . await
396+ . unwrap ( ) ;
397+
398+ let validation = crate :: validate_application_against_environments ( & application, & targets)
399+ . await
400+ . unwrap ( ) ;
401+ assert ! (
402+ validation. errors( ) . is_empty( ) ,
403+ "{}" ,
404+ validation
405+ . errors( )
406+ . iter( )
407+ . map( |e| e. to_string( ) )
408+ . collect:: <Vec <_>>( )
409+ . join( "\n " )
410+ ) ;
411+ assert ! ( validation. is_ok( ) ) ;
412+ }
413+
414+ #[ tokio:: test]
415+ async fn override_at_component_level_bad_component_is_caught ( ) {
416+ let wit_path = PathBuf :: from ( SIMPLE_WIT_DIR ) ;
417+
418+ let wit_text = tokio:: fs:: read_to_string ( wit_path. join ( "world.wit" ) )
419+ . await
420+ . unwrap ( ) ;
421+ let wasm1 = generate_dummy_component ( & wit_text, "spin:test/simple@1.0.0" ) ;
422+ let wasm2 = generate_dummy_component ( & wit_text, "spin:test/not-so-simple@1.0.0" ) ;
423+
424+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
425+ std:: fs:: write ( temp_dir. path ( ) . join ( "wasm1" ) , wasm1) . unwrap ( ) ;
426+ std:: fs:: write ( temp_dir. path ( ) . join ( "wasm2" ) , wasm2) . unwrap ( ) ;
427+
428+ let env1 = target_simple_world ( & wit_path) ;
429+ let env2 = target_not_so_simple_world ( & wit_path) ;
430+
431+ // This would normally be derived from the manifest
432+ let targets = RealisedTargets {
433+ default : vec ! [ env2] ,
434+ overrides : [ ( "nss" . to_string ( ) , vec ! [ env1] ) ] . into_iter ( ) . collect ( ) ,
435+ } ;
436+
437+ let manifest = spin_manifest:: manifest_from_str (
438+ r#"
439+ spin_manifest_version = 2
440+
441+ [application]
442+ name = "test"
443+ targets = ["nss-test"] # default: maps to env2 as per `targets`
444+
445+ [[trigger.s]]
446+ route = "/1"
447+ component = { source = "wasm1" }
448+
449+ [[trigger.s]]
450+ route = "/2"
451+ component = "nss"
452+
453+ [component.nss]
454+ source = "wasm2"
455+ targets = ["test"] # override: maps to env1 as per `targets`
456+ "# ,
457+ )
458+ . unwrap ( ) ;
459+
460+ let application = crate :: ApplicationToValidate :: new ( manifest, temp_dir. path ( ) )
461+ . await
462+ . unwrap ( ) ;
463+
464+ let validation = crate :: validate_application_against_environments ( & application, & targets)
465+ . await
466+ . unwrap ( ) ;
467+
468+ let errs = validation. errors ( ) ;
469+ assert ! ( !errs. is_empty( ) ) ;
470+
471+ let err = errs[ 0 ] . to_string ( ) ;
472+ assert ! (
473+ err. contains( "Component nss (file \" wasm2\" ) can't run in environment test" ) ,
474+ "unexpected error {err}"
475+ ) ;
476+ assert ! (
477+ err. contains( "requires imports named" ) ,
478+ "unexpected error {err}"
479+ ) ;
480+ assert ! (
481+ err. contains( "spin:test/evil@1.0.0" ) ,
482+ "unexpected error {err}"
483+ ) ;
484+
485+ assert ! ( !validation. is_ok( ) ) ;
486+ }
487+
295488 #[ tokio:: test]
296489 async fn can_validate_component_for_unknown_trigger ( ) {
297490 let wit_path = PathBuf :: from ( SIMPLE_WIT_DIR ) ;
@@ -335,14 +528,15 @@ mod test {
335528 . unwrap ( ) ;
336529 let wasm = generate_dummy_component ( & wit_text, "spin:test/simple@1.0.0" ) ;
337530
338- let mut env = target_simple_world ( & wit_path) ;
531+ let mut env = target_simple_world_unarced ( & wit_path) ;
339532 env. trigger_capabilities . insert (
340533 "s" . to_owned ( ) ,
341534 vec ! [
342535 "local_spline_reticulation" . to_owned( ) ,
343536 "nice_cup_of_tea" . to_owned( ) ,
344537 ] ,
345538 ) ;
539+ let env = Arc :: new ( env) ;
346540
347541 assert ! ( env. supports_trigger_type( & "s" . to_owned( ) ) ) ;
348542 assert ! ( !env. supports_trigger_type( & "t" . to_owned( ) ) ) ;
0 commit comments