@@ -18,18 +18,155 @@ pub struct DeviceTarget {
1818 pub name : String ,
1919
2020 #[ serde( default ) ]
21- pub apps : HashMap < Uuid , AppTarget > ,
21+ pub apps : AppTargetMap ,
22+ }
23+
24+ #[ derive( Clone , Debug , Default ) ]
25+ pub struct AppTargetMap ( HashMap < Uuid , AppTarget > ) ;
26+
27+ impl Deref for AppTargetMap {
28+ type Target = HashMap < Uuid , AppTarget > ;
29+ fn deref ( & self ) -> & Self :: Target {
30+ & self . 0
31+ }
32+ }
33+
34+ impl DerefMut for AppTargetMap {
35+ fn deref_mut ( & mut self ) -> & mut Self :: Target {
36+ & mut self . 0
37+ }
38+ }
39+
40+ impl IntoIterator for AppTargetMap {
41+ type Item = ( Uuid , AppTarget ) ;
42+ type IntoIter = std:: collections:: hash_map:: IntoIter < Uuid , AppTarget > ;
43+
44+ fn into_iter ( self ) -> Self :: IntoIter {
45+ self . 0 . into_iter ( )
46+ }
47+ }
48+ impl < ' de > Deserialize < ' de > for AppTargetMap {
49+ fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
50+ where
51+ D : serde:: Deserializer < ' de > ,
52+ {
53+ #[ derive( Deserialize , Clone , Debug ) ]
54+ struct RemoteAppTarget {
55+ pub id : u32 ,
56+ pub name : String ,
57+ #[ serde( default ) ]
58+ pub is_host : bool ,
59+ #[ serde( default ) ]
60+ pub releases : ReleaseTargetMap ,
61+ }
62+
63+ let mut apps: HashMap < Uuid , RemoteAppTarget > = HashMap :: deserialize ( deserializer) ?;
64+
65+ // validate that there is only one hostapp, and it has the appropriate metadata
66+ let hostapps: HashMap < Uuid , RemoteAppTarget > = apps
67+ . iter ( )
68+ . filter ( |( _, app) | app. is_host )
69+ . map ( |( uuid, app) | ( uuid. clone ( ) , app. clone ( ) ) )
70+ . collect ( ) ;
71+
72+ if hostapps. len ( ) > 1 {
73+ return Err ( serde:: de:: Error :: custom (
74+ "only one target hostapp is allowed" ,
75+ ) ) ;
76+ }
77+
78+ if let Some ( ( uuid, app) ) = hostapps. into_iter ( ) . last ( ) {
79+ if app. releases . is_empty ( ) {
80+ return Err ( serde:: de:: Error :: custom (
81+ "the hostapp must have at least one target release" ,
82+ ) ) ;
83+ }
84+
85+ let release = app. releases . values ( ) . last ( ) . unwrap ( ) ;
86+ let hostapp = release. services . values ( ) . find ( |svc| {
87+ svc. labels
88+ . get ( "io.balena.image.class" )
89+ . map ( |value| value == "hostapp" )
90+ . is_some ( )
91+ } ) ;
92+
93+ if hostapp. is_none_or ( |svc| {
94+ !svc. labels
95+ . contains_key ( "io.balena.private.hostapp.board-rev" )
96+ } ) {
97+ // The target OS may not have a board revision as it may be
98+ // before v6.1.18. We ignore the hostapp in that case
99+ apps. remove ( & uuid) ;
100+ }
101+ }
102+
103+ let apps = apps
104+ . into_iter ( )
105+ . map ( |( app_uuid, app) | {
106+ let RemoteAppTarget {
107+ id,
108+ name,
109+ releases,
110+ is_host,
111+ } = app;
112+ if !is_host {
113+ (
114+ app_uuid,
115+ AppTarget :: User ( UserAppTarget { id, name, releases } ) ,
116+ )
117+ } else {
118+ // we have already validated that the release exists
119+ let ( release_uuid, release) = releases. into_iter ( ) . last ( ) . unwrap ( ) ;
120+ let mut service = release
121+ . services
122+ . into_values ( )
123+ . find ( |s| {
124+ s. labels
125+ . get ( "io.balena.image.class" )
126+ . map ( |v| v == "hostapp" )
127+ . is_some ( )
128+ } )
129+ . unwrap ( ) ;
130+
131+ let board_rev = service
132+ . labels
133+ . remove ( "io.balena.private.hostapp.board-rev" )
134+ . unwrap ( ) ;
135+
136+ (
137+ app_uuid,
138+ AppTarget :: Host ( HostAppTarget {
139+ release_uuid,
140+ image : service. image ,
141+ board_rev,
142+ } ) ,
143+ )
144+ }
145+ } )
146+ . collect ( ) ;
147+
148+ Ok ( AppTargetMap ( apps) )
149+ }
150+ }
151+
152+ #[ derive( Clone , Debug ) ]
153+ pub enum AppTarget {
154+ User ( UserAppTarget ) ,
155+ Host ( HostAppTarget ) ,
156+ }
157+
158+ #[ derive( Clone , Debug ) ]
159+ pub struct HostAppTarget {
160+ pub release_uuid : Uuid ,
161+ pub image : ImageUri ,
162+ pub board_rev : String ,
22163}
23164
24165/// Target app as defined by the remote backend
25166#[ derive( Deserialize , Clone , Debug ) ]
26- pub struct AppTarget {
167+ pub struct UserAppTarget {
27168 pub id : u32 ,
28169 pub name : String ,
29-
30- #[ serde( default ) ]
31- pub is_host : bool ,
32-
33170 #[ serde( default ) ]
34171 pub releases : ReleaseTargetMap ,
35172}
@@ -94,6 +231,9 @@ pub struct ReleaseTarget {
94231pub struct ServiceTarget {
95232 pub id : u32 ,
96233 pub image : ImageUri ,
234+
235+ #[ serde( default ) ]
236+ pub labels : HashMap < String , String > ,
97237}
98238
99239// FIXME: add remaining fields
@@ -139,4 +279,43 @@ mod tests {
139279 let release = serde_json:: from_value :: < ReleaseTargetMap > ( json) ;
140280 assert ! ( release. is_err( ) ) ;
141281 }
282+
283+ #[ test]
284+ fn test_rejects_target_apps_with_more_than_one_hostapp ( ) {
285+ let json = json ! ( {
286+ "app-one" : {
287+ "id" : 1 ,
288+ "name" : "ubuntu" ,
289+ "is_host" : true ,
290+ } ,
291+ "app-two" : {
292+ "id" : 2 ,
293+ "name" : "fedora" ,
294+ "is_host" : true ,
295+ }
296+ } ) ;
297+
298+ let apps = serde_json:: from_value :: < AppTargetMap > ( json) ;
299+ assert ! ( apps. is_err_and( |e| e. to_string( ) == "only one target hostapp is allowed" ) ) ;
300+ }
301+
302+ #[ test]
303+ fn test_rejects_target_apps_with_no_releases ( ) {
304+ let json = json ! ( {
305+ "app-one" : {
306+ "id" : 1 ,
307+ "name" : "ubuntu" ,
308+ "is_host" : true ,
309+ "releases" : { }
310+ } ,
311+
312+ } ) ;
313+
314+ let apps = serde_json:: from_value :: < AppTargetMap > ( json) ;
315+ assert ! (
316+ apps. is_err_and(
317+ |e| e. to_string( ) == "the hostapp must have at least one target release"
318+ )
319+ ) ;
320+ }
142321}
0 commit comments